@stupidloud/codegraph 0.8.1 → 0.9.9

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 (341) hide show
  1. package/README.md +319 -152
  2. package/dist/bin/codegraph.d.ts +4 -0
  3. package/dist/bin/codegraph.d.ts.map +1 -1
  4. package/dist/bin/codegraph.js +354 -90
  5. package/dist/bin/codegraph.js.map +1 -1
  6. package/dist/bin/node-version-check.d.ts +17 -0
  7. package/dist/bin/node-version-check.d.ts.map +1 -1
  8. package/dist/bin/node-version-check.js +37 -0
  9. package/dist/bin/node-version-check.js.map +1 -1
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +1 -11
  12. package/dist/config.js.map +1 -1
  13. package/dist/context/formatter.d.ts.map +1 -1
  14. package/dist/context/formatter.js +25 -6
  15. package/dist/context/formatter.js.map +1 -1
  16. package/dist/context/index.d.ts +22 -0
  17. package/dist/context/index.d.ts.map +1 -1
  18. package/dist/context/index.js +257 -6
  19. package/dist/context/index.js.map +1 -1
  20. package/dist/context/markers.d.ts +19 -0
  21. package/dist/context/markers.d.ts.map +1 -0
  22. package/dist/context/markers.js +22 -0
  23. package/dist/context/markers.js.map +1 -0
  24. package/dist/db/index.d.ts +30 -1
  25. package/dist/db/index.d.ts.map +1 -1
  26. package/dist/db/index.js +75 -25
  27. package/dist/db/index.js.map +1 -1
  28. package/dist/db/queries.d.ts +104 -0
  29. package/dist/db/queries.d.ts.map +1 -1
  30. package/dist/db/queries.js +328 -31
  31. package/dist/db/queries.js.map +1 -1
  32. package/dist/db/sqlite-adapter.d.ts +24 -23
  33. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  34. package/dist/db/sqlite-adapter.js +54 -174
  35. package/dist/db/sqlite-adapter.js.map +1 -1
  36. package/dist/directory.d.ts.map +1 -1
  37. package/dist/directory.js +6 -20
  38. package/dist/directory.js.map +1 -1
  39. package/dist/extraction/generated-detection.d.ts +30 -0
  40. package/dist/extraction/generated-detection.d.ts.map +1 -0
  41. package/dist/extraction/generated-detection.js +80 -0
  42. package/dist/extraction/generated-detection.js.map +1 -0
  43. package/dist/extraction/grammars.d.ts +23 -1
  44. package/dist/extraction/grammars.d.ts.map +1 -1
  45. package/dist/extraction/grammars.js +107 -3
  46. package/dist/extraction/grammars.js.map +1 -1
  47. package/dist/extraction/index.d.ts +22 -14
  48. package/dist/extraction/index.d.ts.map +1 -1
  49. package/dist/extraction/index.js +272 -183
  50. package/dist/extraction/index.js.map +1 -1
  51. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  52. package/dist/extraction/languages/c-cpp.js +45 -0
  53. package/dist/extraction/languages/c-cpp.js.map +1 -1
  54. package/dist/extraction/languages/csharp.d.ts.map +1 -1
  55. package/dist/extraction/languages/csharp.js +2 -1
  56. package/dist/extraction/languages/csharp.js.map +1 -1
  57. package/dist/extraction/languages/go.d.ts.map +1 -1
  58. package/dist/extraction/languages/go.js +18 -2
  59. package/dist/extraction/languages/go.js.map +1 -1
  60. package/dist/extraction/languages/index.d.ts.map +1 -1
  61. package/dist/extraction/languages/index.js +6 -0
  62. package/dist/extraction/languages/index.js.map +1 -1
  63. package/dist/extraction/languages/java.d.ts.map +1 -1
  64. package/dist/extraction/languages/java.js +6 -0
  65. package/dist/extraction/languages/java.js.map +1 -1
  66. package/dist/extraction/languages/kotlin.d.ts.map +1 -1
  67. package/dist/extraction/languages/kotlin.js +6 -0
  68. package/dist/extraction/languages/kotlin.js.map +1 -1
  69. package/dist/extraction/languages/lua.d.ts +3 -0
  70. package/dist/extraction/languages/lua.d.ts.map +1 -0
  71. package/dist/extraction/languages/lua.js +150 -0
  72. package/dist/extraction/languages/lua.js.map +1 -0
  73. package/dist/extraction/languages/luau.d.ts +3 -0
  74. package/dist/extraction/languages/luau.d.ts.map +1 -0
  75. package/dist/extraction/languages/luau.js +37 -0
  76. package/dist/extraction/languages/luau.js.map +1 -0
  77. package/dist/extraction/languages/objc.d.ts +3 -0
  78. package/dist/extraction/languages/objc.d.ts.map +1 -0
  79. package/dist/extraction/languages/objc.js +133 -0
  80. package/dist/extraction/languages/objc.js.map +1 -0
  81. package/dist/extraction/mybatis-extractor.d.ts +48 -0
  82. package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
  83. package/dist/extraction/mybatis-extractor.js +198 -0
  84. package/dist/extraction/mybatis-extractor.js.map +1 -0
  85. package/dist/extraction/tree-sitter-types.d.ts +14 -0
  86. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  87. package/dist/extraction/tree-sitter.d.ts +84 -0
  88. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  89. package/dist/extraction/tree-sitter.js +715 -16
  90. package/dist/extraction/tree-sitter.js.map +1 -1
  91. package/dist/extraction/vue-extractor.d.ts +15 -0
  92. package/dist/extraction/vue-extractor.d.ts.map +1 -1
  93. package/dist/extraction/vue-extractor.js +88 -0
  94. package/dist/extraction/vue-extractor.js.map +1 -1
  95. package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
  96. package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
  97. package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
  98. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
  99. package/dist/extraction/wasm-runtime-flags.js +106 -0
  100. package/dist/extraction/wasm-runtime-flags.js.map +1 -0
  101. package/dist/graph/traversal.d.ts.map +1 -1
  102. package/dist/graph/traversal.js +76 -38
  103. package/dist/graph/traversal.js.map +1 -1
  104. package/dist/index.d.ts +77 -8
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +133 -19
  107. package/dist/index.js.map +1 -1
  108. package/dist/installer/config-writer.d.ts +7 -8
  109. package/dist/installer/config-writer.d.ts.map +1 -1
  110. package/dist/installer/config-writer.js +7 -27
  111. package/dist/installer/config-writer.js.map +1 -1
  112. package/dist/installer/index.d.ts +51 -16
  113. package/dist/installer/index.d.ts.map +1 -1
  114. package/dist/installer/index.js +120 -29
  115. package/dist/installer/index.js.map +1 -1
  116. package/dist/installer/instructions-template.d.ts +11 -21
  117. package/dist/installer/instructions-template.d.ts.map +1 -1
  118. package/dist/installer/instructions-template.js +12 -56
  119. package/dist/installer/instructions-template.js.map +1 -1
  120. package/dist/installer/targets/antigravity.d.ts +57 -0
  121. package/dist/installer/targets/antigravity.d.ts.map +1 -0
  122. package/dist/installer/targets/antigravity.js +308 -0
  123. package/dist/installer/targets/antigravity.js.map +1 -0
  124. package/dist/installer/targets/claude.d.ts +26 -1
  125. package/dist/installer/targets/claude.d.ts.map +1 -1
  126. package/dist/installer/targets/claude.js +118 -40
  127. package/dist/installer/targets/claude.js.map +1 -1
  128. package/dist/installer/targets/codex.d.ts.map +1 -1
  129. package/dist/installer/targets/codex.js +15 -13
  130. package/dist/installer/targets/codex.js.map +1 -1
  131. package/dist/installer/targets/cursor.d.ts.map +1 -1
  132. package/dist/installer/targets/cursor.js +61 -36
  133. package/dist/installer/targets/cursor.js.map +1 -1
  134. package/dist/installer/targets/gemini.d.ts +26 -0
  135. package/dist/installer/targets/gemini.d.ts.map +1 -0
  136. package/dist/installer/targets/gemini.js +167 -0
  137. package/dist/installer/targets/gemini.js.map +1 -0
  138. package/dist/installer/targets/hermes.d.ts +18 -0
  139. package/dist/installer/targets/hermes.d.ts.map +1 -0
  140. package/dist/installer/targets/hermes.js +359 -0
  141. package/dist/installer/targets/hermes.js.map +1 -0
  142. package/dist/installer/targets/kiro.d.ts +27 -0
  143. package/dist/installer/targets/kiro.d.ts.map +1 -0
  144. package/dist/installer/targets/kiro.js +178 -0
  145. package/dist/installer/targets/kiro.js.map +1 -0
  146. package/dist/installer/targets/opencode.d.ts.map +1 -1
  147. package/dist/installer/targets/opencode.js +15 -13
  148. package/dist/installer/targets/opencode.js.map +1 -1
  149. package/dist/installer/targets/registry.d.ts.map +1 -1
  150. package/dist/installer/targets/registry.js +8 -0
  151. package/dist/installer/targets/registry.js.map +1 -1
  152. package/dist/installer/targets/shared.d.ts.map +1 -1
  153. package/dist/installer/targets/shared.js +3 -2
  154. package/dist/installer/targets/shared.js.map +1 -1
  155. package/dist/installer/targets/types.d.ts +1 -16
  156. package/dist/installer/targets/types.d.ts.map +1 -1
  157. package/dist/mcp/daemon-paths.d.ts +46 -0
  158. package/dist/mcp/daemon-paths.d.ts.map +1 -0
  159. package/dist/mcp/daemon-paths.js +125 -0
  160. package/dist/mcp/daemon-paths.js.map +1 -0
  161. package/dist/mcp/daemon.d.ts +161 -0
  162. package/dist/mcp/daemon.d.ts.map +1 -0
  163. package/dist/mcp/daemon.js +403 -0
  164. package/dist/mcp/daemon.js.map +1 -0
  165. package/dist/mcp/engine.d.ts +105 -0
  166. package/dist/mcp/engine.d.ts.map +1 -0
  167. package/dist/mcp/engine.js +270 -0
  168. package/dist/mcp/engine.js.map +1 -0
  169. package/dist/mcp/index.d.ts +70 -52
  170. package/dist/mcp/index.d.ts.map +1 -1
  171. package/dist/mcp/index.js +355 -331
  172. package/dist/mcp/index.js.map +1 -1
  173. package/dist/mcp/proxy.d.ts +81 -0
  174. package/dist/mcp/proxy.d.ts.map +1 -0
  175. package/dist/mcp/proxy.js +510 -0
  176. package/dist/mcp/proxy.js.map +1 -0
  177. package/dist/mcp/server-instructions.d.ts +1 -1
  178. package/dist/mcp/server-instructions.d.ts.map +1 -1
  179. package/dist/mcp/server-instructions.js +21 -21
  180. package/dist/mcp/session.d.ts +77 -0
  181. package/dist/mcp/session.d.ts.map +1 -0
  182. package/dist/mcp/session.js +294 -0
  183. package/dist/mcp/session.js.map +1 -0
  184. package/dist/mcp/tools.d.ts +171 -15
  185. package/dist/mcp/tools.d.ts.map +1 -1
  186. package/dist/mcp/tools.js +1714 -298
  187. package/dist/mcp/tools.js.map +1 -1
  188. package/dist/mcp/transport.d.ts +111 -29
  189. package/dist/mcp/transport.d.ts.map +1 -1
  190. package/dist/mcp/transport.js +181 -71
  191. package/dist/mcp/transport.js.map +1 -1
  192. package/dist/mcp/version.d.ts +19 -0
  193. package/dist/mcp/version.d.ts.map +1 -0
  194. package/dist/mcp/version.js +71 -0
  195. package/dist/mcp/version.js.map +1 -0
  196. package/dist/resolution/callback-synthesizer.d.ts +10 -0
  197. package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
  198. package/dist/resolution/callback-synthesizer.js +1300 -0
  199. package/dist/resolution/callback-synthesizer.js.map +1 -0
  200. package/dist/resolution/frameworks/csharp.d.ts.map +1 -1
  201. package/dist/resolution/frameworks/csharp.js +36 -8
  202. package/dist/resolution/frameworks/csharp.js.map +1 -1
  203. package/dist/resolution/frameworks/drupal.d.ts +51 -0
  204. package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
  205. package/dist/resolution/frameworks/drupal.js +367 -0
  206. package/dist/resolution/frameworks/drupal.js.map +1 -0
  207. package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
  208. package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
  209. package/dist/resolution/frameworks/expo-modules.js +143 -0
  210. package/dist/resolution/frameworks/expo-modules.js.map +1 -0
  211. package/dist/resolution/frameworks/express.d.ts.map +1 -1
  212. package/dist/resolution/frameworks/express.js +102 -19
  213. package/dist/resolution/frameworks/express.js.map +1 -1
  214. package/dist/resolution/frameworks/fabric.d.ts +3 -0
  215. package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
  216. package/dist/resolution/frameworks/fabric.js +354 -0
  217. package/dist/resolution/frameworks/fabric.js.map +1 -0
  218. package/dist/resolution/frameworks/go.d.ts.map +1 -1
  219. package/dist/resolution/frameworks/go.js +6 -3
  220. package/dist/resolution/frameworks/go.js.map +1 -1
  221. package/dist/resolution/frameworks/index.d.ts +6 -0
  222. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  223. package/dist/resolution/frameworks/index.js +29 -1
  224. package/dist/resolution/frameworks/index.js.map +1 -1
  225. package/dist/resolution/frameworks/java.d.ts.map +1 -1
  226. package/dist/resolution/frameworks/java.js +339 -12
  227. package/dist/resolution/frameworks/java.js.map +1 -1
  228. package/dist/resolution/frameworks/laravel.d.ts.map +1 -1
  229. package/dist/resolution/frameworks/laravel.js +17 -8
  230. package/dist/resolution/frameworks/laravel.js.map +1 -1
  231. package/dist/resolution/frameworks/nestjs.d.ts.map +1 -1
  232. package/dist/resolution/frameworks/nestjs.js +324 -0
  233. package/dist/resolution/frameworks/nestjs.js.map +1 -1
  234. package/dist/resolution/frameworks/play.d.ts +19 -0
  235. package/dist/resolution/frameworks/play.d.ts.map +1 -0
  236. package/dist/resolution/frameworks/play.js +111 -0
  237. package/dist/resolution/frameworks/play.js.map +1 -0
  238. package/dist/resolution/frameworks/python.d.ts.map +1 -1
  239. package/dist/resolution/frameworks/python.js +134 -16
  240. package/dist/resolution/frameworks/python.js.map +1 -1
  241. package/dist/resolution/frameworks/react-native.d.ts +3 -0
  242. package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
  243. package/dist/resolution/frameworks/react-native.js +360 -0
  244. package/dist/resolution/frameworks/react-native.js.map +1 -0
  245. package/dist/resolution/frameworks/react.d.ts.map +1 -1
  246. package/dist/resolution/frameworks/react.js +96 -3
  247. package/dist/resolution/frameworks/react.js.map +1 -1
  248. package/dist/resolution/frameworks/ruby.d.ts.map +1 -1
  249. package/dist/resolution/frameworks/ruby.js +106 -2
  250. package/dist/resolution/frameworks/ruby.js.map +1 -1
  251. package/dist/resolution/frameworks/rust.d.ts.map +1 -1
  252. package/dist/resolution/frameworks/rust.js +102 -5
  253. package/dist/resolution/frameworks/rust.js.map +1 -1
  254. package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
  255. package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
  256. package/dist/resolution/frameworks/swift-objc.js +252 -0
  257. package/dist/resolution/frameworks/swift-objc.js.map +1 -0
  258. package/dist/resolution/frameworks/swift.d.ts.map +1 -1
  259. package/dist/resolution/frameworks/swift.js +30 -6
  260. package/dist/resolution/frameworks/swift.js.map +1 -1
  261. package/dist/resolution/go-module.d.ts +26 -0
  262. package/dist/resolution/go-module.d.ts.map +1 -0
  263. package/dist/resolution/go-module.js +78 -0
  264. package/dist/resolution/go-module.js.map +1 -0
  265. package/dist/resolution/import-resolver.d.ts +28 -0
  266. package/dist/resolution/import-resolver.d.ts.map +1 -1
  267. package/dist/resolution/import-resolver.js +617 -5
  268. package/dist/resolution/import-resolver.js.map +1 -1
  269. package/dist/resolution/index.d.ts +11 -0
  270. package/dist/resolution/index.d.ts.map +1 -1
  271. package/dist/resolution/index.js +196 -10
  272. package/dist/resolution/index.js.map +1 -1
  273. package/dist/resolution/lru-cache.d.ts +24 -0
  274. package/dist/resolution/lru-cache.d.ts.map +1 -0
  275. package/dist/resolution/lru-cache.js +62 -0
  276. package/dist/resolution/lru-cache.js.map +1 -0
  277. package/dist/resolution/name-matcher.d.ts.map +1 -1
  278. package/dist/resolution/name-matcher.js +212 -0
  279. package/dist/resolution/name-matcher.js.map +1 -1
  280. package/dist/resolution/swift-objc-bridge.d.ts +134 -0
  281. package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
  282. package/dist/resolution/swift-objc-bridge.js +256 -0
  283. package/dist/resolution/swift-objc-bridge.js.map +1 -0
  284. package/dist/resolution/types.d.ts +44 -0
  285. package/dist/resolution/types.d.ts.map +1 -1
  286. package/dist/resolution/workspace-packages.d.ts +48 -0
  287. package/dist/resolution/workspace-packages.d.ts.map +1 -0
  288. package/dist/resolution/workspace-packages.js +208 -0
  289. package/dist/resolution/workspace-packages.js.map +1 -0
  290. package/dist/search/query-utils.d.ts +18 -0
  291. package/dist/search/query-utils.d.ts.map +1 -1
  292. package/dist/search/query-utils.js +30 -0
  293. package/dist/search/query-utils.js.map +1 -1
  294. package/dist/sync/git-hooks.d.ts.map +1 -1
  295. package/dist/sync/git-hooks.js +2 -0
  296. package/dist/sync/git-hooks.js.map +1 -1
  297. package/dist/sync/index.d.ts +3 -1
  298. package/dist/sync/index.d.ts.map +1 -1
  299. package/dist/sync/index.js +8 -1
  300. package/dist/sync/index.js.map +1 -1
  301. package/dist/sync/watcher.d.ts +214 -12
  302. package/dist/sync/watcher.d.ts.map +1 -1
  303. package/dist/sync/watcher.js +467 -55
  304. package/dist/sync/watcher.js.map +1 -1
  305. package/dist/sync/worktree.d.ts +54 -0
  306. package/dist/sync/worktree.d.ts.map +1 -0
  307. package/dist/sync/worktree.js +137 -0
  308. package/dist/sync/worktree.js.map +1 -0
  309. package/dist/types.d.ts +9 -1
  310. package/dist/types.d.ts.map +1 -1
  311. package/dist/types.js +14 -0
  312. package/dist/types.js.map +1 -1
  313. package/dist/utils.js +1 -1
  314. package/package.json +2 -2
  315. package/scripts/add-lang/bench.sh +60 -0
  316. package/scripts/add-lang/check-grammar.mjs +75 -0
  317. package/scripts/add-lang/dump-ast.mjs +103 -0
  318. package/scripts/add-lang/verify-extraction.mjs +70 -0
  319. package/scripts/agent-eval/arms-F.sh +21 -0
  320. package/scripts/agent-eval/arms-matrix.sh +37 -0
  321. package/scripts/agent-eval/bench-readme.sh +28 -0
  322. package/scripts/agent-eval/bench-why-repo.sh +22 -0
  323. package/scripts/agent-eval/block-read-hook.sh +19 -0
  324. package/scripts/agent-eval/hook-settings.json +15 -0
  325. package/scripts/agent-eval/itrun.sh +24 -11
  326. package/scripts/agent-eval/parse-arms.mjs +116 -0
  327. package/scripts/agent-eval/parse-bench-readme.mjs +84 -0
  328. package/scripts/agent-eval/probe-context.mjs +21 -0
  329. package/scripts/agent-eval/probe-explore.mjs +40 -0
  330. package/scripts/agent-eval/probe-node.mjs +20 -0
  331. package/scripts/agent-eval/probe-sweep.mjs +119 -0
  332. package/scripts/agent-eval/probe-trace.mjs +20 -0
  333. package/scripts/agent-eval/run-arms.sh +56 -0
  334. package/scripts/agent-eval/seq-matrix.mjs +137 -0
  335. package/scripts/build-bundle.sh +118 -0
  336. package/scripts/npm-sdk.js +75 -0
  337. package/scripts/npm-shim.js +246 -0
  338. package/scripts/pack-npm.sh +119 -0
  339. package/scripts/prepare-release.mjs +270 -0
  340. package/scripts/patch-tree-sitter-dart.js +0 -112
  341. package/scripts/release.sh +0 -68
package/dist/mcp/index.js CHANGED
@@ -14,6 +14,25 @@
14
14
  * const server = new MCPServer('/path/to/project');
15
15
  * await server.start();
16
16
  * ```
17
+ *
18
+ * Runtime modes (decided in {@link MCPServer.start}):
19
+ *
20
+ * - **Direct** — one process serves one MCP client over stdio. The pre-#411
21
+ * behavior; used when the user opts out (`CODEGRAPH_NO_DAEMON=1`), no
22
+ * `.codegraph/` is reachable, or the daemon machinery fails for any reason.
23
+ * - **Proxy** — what an MCP host actually talks to when sharing is on: a thin
24
+ * stdio↔socket pipe to the shared daemon. The proxy carries the #277 PPID
25
+ * watchdog, so a SIGKILL'd host reaps its proxy promptly. See {@link ./proxy.ts}.
26
+ * - **Daemon** — a *detached* background process (its own session/process
27
+ * group) that serves N proxies over a Unix-domain socket / named pipe,
28
+ * sharing one CodeGraph + watcher + SQLite handle. Spawned on demand; never a
29
+ * child of any host, so it survives individual sessions and is reaped by
30
+ * client-refcount + idle timeout. See {@link ./daemon.ts} and issue #411.
31
+ *
32
+ * The detached-daemon + always-proxy split is the fix for the review finding
33
+ * that the original in-process daemon (a) was the first host's child, so closing
34
+ * that terminal severed every other client, and (b) disabled the PPID watchdog,
35
+ * regressing #277 (orphaned daemons on host SIGKILL).
17
36
  */
18
37
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
38
  if (k2 === undefined) k2 = k;
@@ -49,405 +68,410 @@ var __importStar = (this && this.__importStar) || (function () {
49
68
  };
50
69
  })();
51
70
  Object.defineProperty(exports, "__esModule", { value: true });
52
- exports.ToolHandler = exports.tools = exports.StdioTransport = exports.MCPServer = void 0;
71
+ exports.CodeGraphPackageVersion = exports.Daemon = exports.ToolHandler = exports.tools = exports.StdioTransport = exports.MCPServer = void 0;
72
+ const fs = __importStar(require("fs"));
53
73
  const path = __importStar(require("path"));
54
- const index_1 = __importStar(require("../index"));
55
- const sync_1 = require("../sync");
74
+ const child_process_1 = require("child_process");
75
+ const directory_1 = require("../directory");
56
76
  const transport_1 = require("./transport");
57
- const tools_1 = require("./tools");
58
- const server_instructions_1 = require("./server-instructions");
77
+ const engine_1 = require("./engine");
78
+ const session_1 = require("./session");
79
+ const daemon_1 = require("./daemon");
80
+ const proxy_1 = require("./proxy");
81
+ const daemon_paths_1 = require("./daemon-paths");
82
+ const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
59
83
  /**
60
- * Convert a file:// URI to a filesystem path.
61
- * Handles URL encoding and Windows drive letter paths.
84
+ * How often to poll `process.ppid` to detect parent process death (see #277).
85
+ * 5s is a deliberate trade-off: the failure mode being guarded against is rare
86
+ * (parent SIGKILL'd), and longer poll = less wakeup overhead while idle.
62
87
  */
63
- function fileUriToPath(uri) {
64
- try {
65
- const url = new URL(uri);
66
- let filePath = decodeURIComponent(url.pathname);
67
- // On Windows, file:///C:/path produces pathname /C:/path strip leading /
68
- if (process.platform === 'win32' && /^\/[a-zA-Z]:/.test(filePath)) {
69
- filePath = filePath.slice(1);
70
- }
71
- return path.resolve(filePath);
72
- }
73
- catch {
74
- // Fallback for non-standard URIs
75
- return uri.replace(/^file:\/\/\/?/, '');
76
- }
77
- }
88
+ const DEFAULT_PPID_POLL_MS = 5000;
89
+ /**
90
+ * Env var that marks a process as the *detached daemon* itself (set by
91
+ * {@link spawnDetachedDaemon} when it re-invokes the CLI). Without it a
92
+ * `serve --mcp` invocation is a launcher that connects-or-spawns; with it, the
93
+ * process IS the daemon and must never try to spawn another (infinite spawn).
94
+ */
95
+ const DAEMON_INTERNAL_ENV = 'CODEGRAPH_DAEMON_INTERNAL';
78
96
  /**
79
- * MCP Server Info
97
+ * Retries for the detached daemon arbitrating the O_EXCL lock against a racing
98
+ * sibling. Tiny — the lock resolves on the first round in practice; the retries
99
+ * only cover clearing a genuinely stale (dead-pid) lockfile.
80
100
  */
81
- const SERVER_INFO = {
82
- name: 'codegraph',
83
- version: '0.1.0',
84
- };
101
+ const TAKEOVER_MAX_RETRIES = 5;
102
+ const TAKEOVER_RETRY_DELAY_MS = 100;
85
103
  /**
86
- * MCP Protocol Version
104
+ * How long a launcher waits for a freshly-spawned daemon to bind its socket
105
+ * before giving up and running in-process. The daemon binds the socket *before*
106
+ * the (backgrounded) engine/grammar warm-up, so this only needs to cover node
107
+ * process startup. 60 × 100ms = 6s of headroom for a cold/slow box; on the
108
+ * common path the socket appears within a few rounds.
87
109
  */
88
- const PROTOCOL_VERSION = '2024-11-05';
110
+ // Poll finely (25ms) so the proxy attaches the instant the freshly-spawned
111
+ // daemon binds, instead of waiting up to a coarse 100ms after — shaves the
112
+ // cold-start handshake (the window the headless agent races). Same ~6s total
113
+ // give-up budget (240 × 25ms), just finer granularity; socket-connect probes
114
+ // are cheap. Paired with deferring the CodeGraph load (engine.ts) off the bind
115
+ // path, this narrows the "No such tool available" race window.
116
+ const DAEMON_CONNECT_MAX_RETRIES = 240;
117
+ const DAEMON_CONNECT_RETRY_DELAY_MS = 25;
89
118
  /**
90
- * How long to wait for the client's `roots/list` response before giving up
91
- * and falling back to the process cwd.
119
+ * Resolve the PPID watchdog poll interval from an env override. A value of
120
+ * `0` disables the watchdog entirely (escape hatch for embedded scenarios
121
+ * where the parent legitimately re-parents the server on purpose). Anything
122
+ * non-numeric or negative falls back to the default.
92
123
  */
93
- const ROOTS_LIST_TIMEOUT_MS = 5000;
124
+ function parsePpidPollMs(raw) {
125
+ if (raw === undefined || raw === '')
126
+ return DEFAULT_PPID_POLL_MS;
127
+ const parsed = Number(raw);
128
+ if (!Number.isFinite(parsed))
129
+ return DEFAULT_PPID_POLL_MS;
130
+ if (parsed < 0)
131
+ return DEFAULT_PPID_POLL_MS;
132
+ return Math.floor(parsed);
133
+ }
94
134
  /**
95
- * Extract the first usable filesystem path from a `roots/list` result.
96
- * Shape per MCP spec: `{ roots: [{ uri: "file:///path", name?: string }] }`.
97
- * Returns null if the result is empty or malformed.
135
+ * Parse the host PID propagated across the `--liftoff-only` re-exec
136
+ * ({@link HOST_PPID_ENV}). Returns a positive integer PID, or null when
137
+ * unset/invalid the direct-launch path, where the watchdog falls back to
138
+ * `process.ppid` divergence. PIDs of 0/1 are rejected (0 = unknown, 1 = init,
139
+ * i.e. already orphaned), so the watchdog doesn't latch onto init.
98
140
  */
99
- function firstRootPath(result) {
100
- if (!result || typeof result !== 'object')
141
+ function parseHostPpid(raw) {
142
+ if (raw === undefined || raw === '')
101
143
  return null;
102
- const roots = result.roots;
103
- if (!Array.isArray(roots) || roots.length === 0)
144
+ const parsed = Number(raw);
145
+ if (!Number.isInteger(parsed) || parsed <= 1)
104
146
  return null;
105
- const first = roots[0];
106
- if (typeof first?.uri !== 'string')
147
+ return parsed;
148
+ }
149
+ /** Whether `CODEGRAPH_NO_DAEMON` was set to a truthy value. */
150
+ function daemonOptOutSet() {
151
+ const raw = process.env.CODEGRAPH_NO_DAEMON;
152
+ if (!raw)
153
+ return false;
154
+ return raw !== '0' && raw.toLowerCase() !== 'false';
155
+ }
156
+ /** Whether this process was spawned to BE the detached daemon. */
157
+ function daemonInternalSet() {
158
+ const raw = process.env[DAEMON_INTERNAL_ENV];
159
+ return !!raw && raw !== '0' && raw.toLowerCase() !== 'false';
160
+ }
161
+ /**
162
+ * Resolve the project root the daemon machinery should key on. Returns
163
+ * `null` when no `.codegraph/` is reachable from the candidate path — in
164
+ * that case the caller must run in direct mode, since the daemon lockfile
165
+ * and socket both live under `.codegraph/`.
166
+ *
167
+ * The result is canonicalized with `realpathSync` so every client converges on
168
+ * the same socket/lock path regardless of how it expressed the path: a client
169
+ * launched with cwd under a symlink (e.g. macOS `/var` → `/private/var`, where
170
+ * spawned `process.cwd()` is already realpath'd) and one that passed a
171
+ * symlinked `rootUri` would otherwise hash to different sockets and silently
172
+ * fail to share the daemon.
173
+ */
174
+ function resolveDaemonRoot(explicitPath) {
175
+ const candidate = explicitPath ?? process.cwd();
176
+ const root = (0, directory_1.findNearestCodeGraphRoot)(candidate);
177
+ if (!root)
107
178
  return null;
108
- return fileUriToPath(first.uri);
179
+ try {
180
+ return fs.realpathSync(root);
181
+ }
182
+ catch {
183
+ return root;
184
+ }
185
+ }
186
+ /**
187
+ * Spawn the shared daemon as a fully detached background process: its own
188
+ * session/process group (so a SIGHUP/SIGINT to the launcher's terminal can't
189
+ * reach it) with stdio decoupled from the launcher (logs to
190
+ * `.codegraph/daemon.log`). Re-invokes the *same* CLI faithfully across dev and
191
+ * bundled launches by reusing `process.argv[0]` (the right node), the current
192
+ * `process.execArgv` (carries `--liftoff-only`, so the daemon never re-execs)
193
+ * and `process.argv[1]` (this script). The spawned process self-arbitrates the
194
+ * O_EXCL lock, so racing launchers may each spawn one — losers exit and every
195
+ * launcher proxies through the single winner.
196
+ */
197
+ function spawnDetachedDaemon(root) {
198
+ const scriptPath = process.argv[1];
199
+ if (!scriptPath) {
200
+ // No resolvable CLI entry point to re-invoke — let the caller fall back to
201
+ // direct mode rather than spawn something broken.
202
+ throw new Error('cannot resolve CLI script path to spawn the daemon');
203
+ }
204
+ let logFd = null;
205
+ let stdio = 'ignore';
206
+ try {
207
+ logFd = fs.openSync(path.join((0, directory_1.getCodeGraphDir)(root), 'daemon.log'), 'a');
208
+ stdio = ['ignore', logFd, logFd];
209
+ }
210
+ catch {
211
+ stdio = 'ignore'; // no log file — discard daemon output rather than fail
212
+ }
213
+ try {
214
+ const child = (0, child_process_1.spawn)(process.execPath, [...process.execArgv, scriptPath, 'serve', '--mcp', '--path', root], {
215
+ detached: true,
216
+ stdio,
217
+ windowsHide: true,
218
+ env: { ...process.env, [DAEMON_INTERNAL_ENV]: '1' },
219
+ });
220
+ child.unref();
221
+ }
222
+ finally {
223
+ // The child holds its own dup of the log fd now; the launcher doesn't need it.
224
+ if (logFd !== null) {
225
+ try {
226
+ fs.closeSync(logFd);
227
+ }
228
+ catch { /* ignore */ }
229
+ }
230
+ }
109
231
  }
110
232
  /**
111
233
  * MCP Server for CodeGraph
112
234
  *
113
235
  * Implements the Model Context Protocol to expose CodeGraph
114
236
  * functionality as tools that can be called by AI assistants.
237
+ *
238
+ * Backwards-compatible constructor and `start()` signature with the
239
+ * pre-issue-#411 implementation: callers continue to do
240
+ * `new MCPServer(path).start()`. Internally we now pick from direct / proxy /
241
+ * daemon at start time.
115
242
  */
116
243
  class MCPServer {
117
- transport;
118
- cg = null;
119
- toolHandler;
120
244
  projectPath;
121
- // In-flight background init kicked off from handleInitialize. Tracked so the
122
- // sync retry path doesn't race against it (double-opening the SQLite file).
123
- initPromise = null;
124
- // Whether the client advertised the MCP `roots` capability during initialize.
125
- // If so, and no explicit project path was given, we ask it for the workspace
126
- // root via roots/list rather than guessing from the (often wrong) cwd.
127
- clientSupportsRoots = false;
128
- // Guards the one-shot deferred resolution (roots/list or cwd) so we don't
129
- // re-issue roots/list on every tool call.
130
- rootsAttempted = false;
245
+ // Direct-mode-only state. In daemon mode the per-connection sessions live
246
+ // inside the Daemon class; in proxy mode there is no session at all.
247
+ session = null;
248
+ engine = null;
249
+ daemon = null;
250
+ ppidWatchdog = null;
251
+ // PPID watchdog baseline — captured at construction so we always have a
252
+ // baseline, even if start() runs after a fork-style reparent.
253
+ originalPpid = process.ppid;
254
+ hostPpid = parseHostPpid(process.env[wasm_runtime_flags_1.HOST_PPID_ENV]);
255
+ // Idempotency guard for stop().
256
+ stopped = false;
257
+ mode = 'unstarted';
131
258
  constructor(projectPath) {
132
259
  this.projectPath = projectPath || null;
133
- this.transport = new transport_1.StdioTransport();
134
- // Create ToolHandler eagerly — cross-project queries work even without a default project
135
- this.toolHandler = new tools_1.ToolHandler(null);
136
260
  }
137
261
  /**
138
- * Start the MCP server
262
+ * Start the MCP server.
139
263
  *
140
- * Note: CodeGraph initialization is deferred until the initialize request
141
- * is received, which includes the rootUri from the client.
142
- */
143
- async start() {
144
- // Start listening for messages immediately - don't check initialization yet
145
- // We'll get the project path from the initialize request's rootUri
146
- this.transport.start(this.handleMessage.bind(this));
147
- // Keep the process running
148
- process.on('SIGINT', () => this.stop());
149
- process.on('SIGTERM', () => this.stop());
150
- // When the parent process (Claude Code) exits, stdin closes.
151
- // Detect this and shut down gracefully to prevent orphaned processes.
152
- process.stdin.on('end', () => this.stop());
153
- process.stdin.on('close', () => this.stop());
154
- }
155
- /**
156
- * Try to initialize CodeGraph for the default project.
157
- *
158
- * Walks up parent directories to find the nearest .codegraph/ folder,
159
- * similar to how git finds .git/ directories.
264
+ * Decision order:
265
+ * 1. `CODEGRAPH_NO_DAEMON=1` direct mode (unchanged pre-#411 behavior).
266
+ * 2. `CODEGRAPH_DAEMON_INTERNAL=1` → we ARE the detached daemon; listen.
267
+ * 3. No `.codegraph/` reachable → direct mode (the daemon's lockfile and
268
+ * socket both live under `.codegraph/`).
269
+ * 4. Otherwise connect to (or spawn) the shared daemon and proxy to it.
160
270
  *
161
- * If initialization fails, the error is recorded but the server continues
162
- * to work cross-project queries and retries on subsequent tool calls
163
- * are still possible.
271
+ * On any unexpected failure in step 4 we transparently fall back to direct
272
+ * modea misbehaving daemon must never block a session from starting.
164
273
  */
165
- async tryInitializeDefault(projectPath) {
166
- // Record where we searched so a later "not initialized" error can name it.
167
- this.toolHandler.setDefaultProjectHint(projectPath);
168
- // Walk up parent directories to find nearest .codegraph/
169
- const resolvedRoot = (0, index_1.findNearestCodeGraphRoot)(projectPath);
170
- if (!resolvedRoot) {
171
- this.projectPath = projectPath;
172
- return;
274
+ async start() {
275
+ // The detached daemon process itself. Checked before the opt-out so the
276
+ // daemon honors the same env it was spawned with (it never sets NO_DAEMON).
277
+ if (daemonInternalSet()) {
278
+ return this.startDaemonProcess();
279
+ }
280
+ // Direct mode if the user opted out. Setting the env var is sufficient to
281
+ // get the pre-#411 single-process behavior.
282
+ if (daemonOptOutSet()) {
283
+ return this.startDirect('CODEGRAPH_NO_DAEMON set');
284
+ }
285
+ const root = resolveDaemonRoot(this.projectPath);
286
+ if (!root) {
287
+ // No initialized project found — daemon mode has nowhere to put its
288
+ // socket. The fresh-checkout / outside-project case; behave as before.
289
+ return this.startDirect('no .codegraph/ root found');
173
290
  }
174
- this.projectPath = resolvedRoot;
175
291
  try {
176
- this.cg = await index_1.default.open(resolvedRoot);
177
- this.toolHandler.setDefaultCodeGraph(this.cg);
178
- this.startWatching();
292
+ // Answer the MCP handshake LOCALLY (instant tool registration — no waiting
293
+ // ~600ms for the daemon to spawn+bind, which produced the cold-start race)
294
+ // and forward tool CALLS to the shared daemon, connected in the background.
295
+ // Runs until the host disconnects; the proxy installs its own watchdog and
296
+ // falls back to an in-process engine if the daemon never comes up.
297
+ this.mode = 'proxy';
298
+ await this.runProxyWithLocalHandshake(root);
299
+ return;
179
300
  }
180
301
  catch (err) {
181
- // Log the error so transient failures are diagnosable (see issue #47)
302
+ // Belt-and-braces: a throw during proxy SETUP (before the client was served)
303
+ // is still safe to recover from with a direct-mode session.
182
304
  const msg = err instanceof Error ? err.message : String(err);
183
- process.stderr.write(`[CodeGraph MCP] Failed to open project at ${resolvedRoot}: ${msg}\n`);
305
+ process.stderr.write(`[CodeGraph MCP] Proxy path failed (${msg}); falling back to direct mode.\n`);
306
+ return this.startDirect('proxy path threw');
184
307
  }
185
308
  }
186
309
  /**
187
- * Retry initialization of the default project if it previously failed.
188
- * Called lazily on tool calls that need the default project.
189
- * Re-walks parent directories each time so it picks up projects
190
- * initialized after the MCP server started.
191
- *
192
- * Awaits any in-flight background init (kicked off by handleInitialize) so
193
- * we never open the SQLite file twice concurrently.
310
+ * Stop the server. In daemon mode this triggers graceful shutdown of every
311
+ * connected session; in direct mode it mirrors the pre-#411 behavior (close
312
+ * cg, exit). Proxy mode never routes through here the proxy exits itself.
194
313
  */
195
- async retryInitIfNeeded() {
196
- // Wait for the background init started during handleInitialize, if any.
197
- if (this.initPromise) {
198
- try {
199
- await this.initPromise;
200
- }
201
- catch { /* errored init falls through to retry */ }
202
- }
203
- // Already initialized successfully
204
- if (this.toolHandler.hasDefaultCodeGraph())
314
+ stop() {
315
+ if (this.stopped)
205
316
  return;
206
- // No explicit path was given at initialize. Resolve it now, exactly once:
207
- // ask the client via roots/list (if it advertised roots), else use cwd.
208
- // Deferring to here lets a roots answer override the wrong cwd, and the
209
- // one-shot guard means we never re-issue roots/list per tool call.
210
- if (!this.projectPath && !this.rootsAttempted) {
211
- this.rootsAttempted = true;
212
- this.initPromise = (this.clientSupportsRoots
213
- ? this.initFromRoots()
214
- : this.tryInitializeDefault(process.cwd())).finally(() => { this.initPromise = null; });
215
- try {
216
- await this.initPromise;
217
- }
218
- catch { /* fall through to last-resort below */ }
219
- if (this.toolHandler.hasDefaultCodeGraph())
220
- return;
317
+ this.stopped = true;
318
+ if (this.ppidWatchdog) {
319
+ clearInterval(this.ppidWatchdog);
320
+ this.ppidWatchdog = null;
221
321
  }
222
- // Last resort: re-walk from the best candidate we have. Picks up projects
223
- // initialized after the server started, and covers clients that sent no
224
- // usable initialize signal at all.
225
- const candidate = this.projectPath ?? process.cwd();
226
- this.toolHandler.setDefaultProjectHint(candidate);
227
- const resolvedRoot = (0, index_1.findNearestCodeGraphRoot)(candidate);
228
- if (!resolvedRoot)
322
+ if (this.daemon) {
323
+ void this.daemon.stop('stop()');
324
+ // Daemon.stop calls process.exit; nothing else to do.
229
325
  return;
230
- try {
231
- // Close any previously failed instance to avoid leaking resources
232
- if (this.cg) {
233
- try {
234
- this.cg.close();
235
- }
236
- catch { /* ignore */ }
237
- this.cg = null;
238
- }
239
- this.cg = index_1.default.openSync(resolvedRoot);
240
- this.projectPath = resolvedRoot;
241
- this.toolHandler.setDefaultCodeGraph(this.cg);
242
- this.startWatching();
243
326
  }
244
- catch {
245
- // Still failing — will retry on next tool call
246
- }
247
- }
248
- /**
249
- * Resolve the project root via the MCP `roots/list` request and initialize
250
- * from the first root the client reports. Falls back to the process cwd if
251
- * the client returns no usable root or doesn't answer in time. See issue #196.
252
- */
253
- async initFromRoots() {
254
- let target = process.cwd();
255
- try {
256
- const result = await this.transport.request('roots/list', undefined, ROOTS_LIST_TIMEOUT_MS);
257
- const rootPath = firstRootPath(result);
258
- if (rootPath) {
259
- target = rootPath;
260
- }
261
- else {
262
- process.stderr.write('[CodeGraph MCP] Client returned no workspace roots; falling back to process cwd.\n');
263
- }
327
+ if (this.session) {
328
+ this.session.stop();
329
+ this.session = null;
264
330
  }
265
- catch (err) {
266
- const msg = err instanceof Error ? err.message : String(err);
267
- process.stderr.write(`[CodeGraph MCP] roots/list request failed (${msg}); falling back to process cwd.\n`);
331
+ if (this.engine) {
332
+ this.engine.stop();
333
+ this.engine = null;
268
334
  }
269
- await this.tryInitializeDefault(target);
335
+ process.exit(0);
270
336
  }
271
- /**
272
- * Start file watching on the active CodeGraph instance.
273
- * Logs sync activity to stderr for diagnostics.
274
- */
275
- startWatching() {
276
- if (!this.cg)
277
- return;
278
- // When the watcher is intentionally disabled (e.g. WSL2 /mnt drives, or
279
- // CODEGRAPH_NO_WATCH=1), say so explicitly and tell the user how to keep
280
- // the graph fresh — otherwise the silent staleness is hard to diagnose.
281
- const disabledReason = (0, sync_1.watchDisabledReason)(this.projectPath ?? process.cwd());
282
- if (disabledReason) {
283
- process.stderr.write(`[CodeGraph MCP] File watcher disabled — ${disabledReason}. ` +
284
- `The graph will not auto-update; run \`codegraph sync\` (or install the git sync hooks via \`codegraph init\`) to refresh.\n`);
285
- return;
337
+ /** Single-process stdio MCP session — the pre-issue-#411 code path. */
338
+ async startDirect(reason) {
339
+ if (reason && process.env.CODEGRAPH_MCP_DEBUG) {
340
+ process.stderr.write(`[CodeGraph MCP] Direct mode: ${reason}.\n`);
286
341
  }
287
- const started = this.cg.watch({
288
- onSyncComplete: (result) => {
289
- if (result.filesChanged > 0) {
290
- process.stderr.write(`[CodeGraph MCP] Auto-synced ${result.filesChanged} file(s) in ${result.durationMs}ms\n`);
291
- }
292
- },
293
- onSyncError: (err) => {
294
- process.stderr.write(`[CodeGraph MCP] Auto-sync error: ${err.message}\n`);
295
- },
342
+ this.engine = new engine_1.MCPEngine();
343
+ const transport = new transport_1.StdioTransport();
344
+ this.session = new session_1.MCPSession(transport, this.engine, {
345
+ explicitProjectPath: this.projectPath,
296
346
  });
297
- if (started) {
298
- process.stderr.write('[CodeGraph MCP] File watcher active graph will auto-sync on changes\n');
299
- }
300
- else {
301
- // start() can also return false when recursive fs.watch isn't supported.
302
- process.stderr.write('[CodeGraph MCP] File watcher unavailable on this platform — run `codegraph sync` to refresh the graph after changes.\n');
347
+ if (this.projectPath) {
348
+ // Background init so the initialize response stays fast (#172).
349
+ void this.engine.ensureInitialized(this.projectPath);
303
350
  }
351
+ this.session.start();
352
+ // Detect parent-process death — same logic as pre-refactor. When stdin
353
+ // closes we go through StdioTransport's `process.exit(0)` already, but
354
+ // SIGKILL of the parent doesn't reliably close stdin on Linux (#277).
355
+ process.stdin.on('end', () => this.stop());
356
+ process.stdin.on('close', () => this.stop());
357
+ this.mode = 'direct';
358
+ this.installSignalHandlers();
359
+ this.installPpidWatchdog();
304
360
  }
305
361
  /**
306
- * Stop the server
362
+ * Run as the detached shared daemon (process spawned with
363
+ * `CODEGRAPH_DAEMON_INTERNAL=1`). Arbitrate the O_EXCL lock, then either
364
+ * become the daemon (bind the socket, serve forever) or — if a live daemon
365
+ * already holds the lock — exit so we don't leak a redundant process.
366
+ *
367
+ * No PPID watchdog and no stdin handlers: the daemon is detached on purpose
368
+ * and reaps itself via client-refcount + idle timeout (see {@link Daemon}).
307
369
  */
308
- stop() {
309
- // Close all cached cross-project connections first
310
- this.toolHandler.closeAll();
311
- // Close the main CodeGraph instance
312
- if (this.cg) {
313
- this.cg.close();
314
- this.cg = null;
370
+ async startDaemonProcess() {
371
+ const root = resolveDaemonRoot(this.projectPath) ?? this.projectPath ?? process.cwd();
372
+ for (let attempt = 0; attempt < TAKEOVER_MAX_RETRIES; attempt++) {
373
+ const lock = (0, daemon_1.tryAcquireDaemonLock)(root);
374
+ if (lock.kind === 'acquired') {
375
+ const daemon = new daemon_1.Daemon(root);
376
+ await daemon.start();
377
+ this.daemon = daemon;
378
+ this.mode = 'daemon';
379
+ return; // the net.Server keeps the process alive
380
+ }
381
+ // Taken. If the holder is alive, another daemon already serves (or is
382
+ // binding) — we're redundant; exit cleanly so the launcher proxies to it.
383
+ const existing = lock.existing;
384
+ if (existing && existing.pid > 0 && (0, daemon_1.isProcessAlive)(existing.pid)) {
385
+ process.stderr.write(`[CodeGraph daemon] Another daemon (pid ${existing.pid}) already holds the lock; exiting.\n`);
386
+ process.exit(0);
387
+ }
388
+ // Holder is dead (or the record is unreadable) — clear it (pid-verified,
389
+ // so we never delete a live daemon's lock) and retry the acquire.
390
+ (0, daemon_1.clearStaleDaemonLock)(lock.pidPath, existing?.pid);
391
+ await sleep(TAKEOVER_RETRY_DELAY_MS);
315
392
  }
316
- this.transport.stop();
393
+ process.stderr.write('[CodeGraph daemon] Could not acquire the daemon lock; exiting.\n');
317
394
  process.exit(0);
318
395
  }
319
396
  /**
320
- * Handle incoming JSON-RPC messages
397
+ * Proxy mode (the common case). Serve the MCP handshake LOCALLY for instant
398
+ * tool registration, forwarding tool calls to the shared daemon — which is
399
+ * connected in the background (probed, then spawned + polled if absent) so the
400
+ * handshake never waits ~600ms on it. Runs until the host disconnects; the
401
+ * proxy falls back to an in-process engine if the daemon never binds, so this
402
+ * never wedges a session.
321
403
  */
322
- async handleMessage(message) {
323
- // Check if it's a request (has id) or notification (no id)
324
- const isRequest = 'id' in message;
325
- switch (message.method) {
326
- case 'initialize':
327
- if (isRequest) {
328
- await this.handleInitialize(message);
329
- }
330
- break;
331
- case 'initialized':
332
- // Notification that client has finished initialization
333
- // No action needed - the client is ready
334
- break;
335
- case 'tools/list':
336
- if (isRequest) {
337
- await this.handleToolsList(message);
338
- }
339
- break;
340
- case 'tools/call':
341
- if (isRequest) {
342
- await this.handleToolsCall(message);
343
- }
344
- break;
345
- case 'ping':
346
- if (isRequest) {
347
- this.transport.sendResult(message.id, {});
348
- }
349
- break;
350
- default:
351
- if (isRequest) {
352
- this.transport.sendError(message.id, transport_1.ErrorCodes.MethodNotFound, `Method not found: ${message.method}`);
353
- }
354
- }
355
- }
356
- /**
357
- * Handle initialize request
358
- */
359
- async handleInitialize(request) {
360
- const params = request.params;
361
- // Does the client support the MCP `roots` protocol? If so, and we have no
362
- // explicit path, we ask it for the workspace root after the handshake
363
- // instead of falling back to the (frequently wrong) cwd. See issue #196.
364
- this.clientSupportsRoots = !!params?.capabilities?.roots;
365
- // Explicit project signal, strongest first: a client-provided rootUri /
366
- // workspaceFolders (LSP-style, non-standard but some clients send it), else
367
- // the --path the server was launched with. cwd is NOT used here — we defer
368
- // it so a roots/list answer can win over it.
369
- let explicitPath = null;
370
- if (params?.rootUri) {
371
- explicitPath = fileUriToPath(params.rootUri);
372
- }
373
- else if (params?.workspaceFolders?.[0]?.uri) {
374
- explicitPath = fileUriToPath(params.workspaceFolders[0].uri);
375
- }
376
- else if (this.projectPath) {
377
- explicitPath = this.projectPath;
378
- }
379
- // Respond to the handshake BEFORE doing any heavy initialization. Loading
380
- // the SQLite DB and the tree-sitter WASM runtime can take many seconds on
381
- // slow filesystems (Docker Desktop VirtioFS on macOS, WSL2). Clients like
382
- // Claude Code time out the handshake at ~30s, which manifested as
383
- // "MCP tools never appear" — the child was alive and had received the
384
- // initialize but was still awaiting initGrammars(). See issue #172.
385
- //
386
- // We accept the client's protocol version but respond with our supported
387
- // version. The `instructions` field is surfaced by MCP clients in the
388
- // agent's system prompt automatically — it's the right place for the
389
- // universal tool-selection playbook, ahead of individual tool descriptions.
390
- this.transport.sendResult(request.id, {
391
- protocolVersion: PROTOCOL_VERSION,
392
- capabilities: {
393
- tools: {},
394
- },
395
- serverInfo: SERVER_INFO,
396
- instructions: server_instructions_1.SERVER_INSTRUCTIONS,
397
- });
398
- // If we know the project dir, kick off init in the background now. Tool
399
- // calls that arrive before it finishes fall through to `retryInitIfNeeded`,
400
- // which waits for this promise rather than racing it with a second open.
401
- //
402
- // If we DON'T know it (no rootUri, no --path), defer: the first tool call
403
- // resolves it via roots/list (when the client supports roots) or cwd. This
404
- // is the fix for issue #196 — clients that launch the server outside the
405
- // project and don't pass a rootUri previously got a misleading "not
406
- // initialized" error on every call.
407
- if (explicitPath) {
408
- this.initPromise = this.tryInitializeDefault(explicitPath).finally(() => {
409
- this.initPromise = null;
410
- });
411
- }
404
+ async runProxyWithLocalHandshake(root) {
405
+ const socketPath = (0, daemon_paths_1.getDaemonSocketPath)(root);
406
+ const getDaemonSocket = async () => {
407
+ // Fast path: a daemon may already be listening.
408
+ const probe = await (0, proxy_1.connectWithHello)(socketPath);
409
+ if (probe === 'version-mismatch')
410
+ return null; // definitive — serve in-process, don't poll for 6s
411
+ if (probe)
412
+ return probe;
413
+ // None reachable — spawn one (detached) and poll for its bind.
414
+ spawnDetachedDaemon(root);
415
+ for (let attempt = 0; attempt < DAEMON_CONNECT_MAX_RETRIES; attempt++) {
416
+ await sleep(DAEMON_CONNECT_RETRY_DELAY_MS);
417
+ const s = await (0, proxy_1.connectWithHello)(socketPath);
418
+ if (s === 'version-mismatch')
419
+ return null;
420
+ if (s)
421
+ return s;
422
+ }
423
+ return null; // never bound — the proxy serves this session in-process
424
+ };
425
+ await (0, proxy_1.runLocalHandshakeProxy)({ getDaemonSocket, makeEngine: () => new engine_1.MCPEngine(), root });
412
426
  }
413
- /**
414
- * Handle tools/list request
415
- */
416
- async handleToolsList(request) {
417
- await this.retryInitIfNeeded();
418
- this.transport.sendResult(request.id, {
419
- tools: this.toolHandler.getTools(),
420
- });
427
+ /** Standard SIGINT/SIGTERM handlers that route to our `stop()` (direct mode). */
428
+ installSignalHandlers() {
429
+ process.on('SIGINT', () => this.stop());
430
+ process.on('SIGTERM', () => this.stop());
421
431
  }
422
432
  /**
423
- * Handle tools/call request
433
+ * PPID watchdog (#277) — direct mode only. Daemon mode is detached on purpose
434
+ * and reaps via idle timeout; proxy mode installs its own watchdog inside
435
+ * {@link runProxy}. So this only ever runs for an in-process direct session.
424
436
  */
425
- async handleToolsCall(request) {
426
- const params = request.params;
427
- if (!params || !params.name) {
428
- this.transport.sendError(request.id, transport_1.ErrorCodes.InvalidParams, 'Missing tool name');
437
+ installPpidWatchdog() {
438
+ if (this.mode !== 'direct')
429
439
  return;
430
- }
431
- const toolName = params.name;
432
- const toolArgs = params.arguments || {};
433
- // Validate tool exists
434
- const tool = tools_1.tools.find(t => t.name === toolName);
435
- if (!tool) {
436
- this.transport.sendError(request.id, transport_1.ErrorCodes.InvalidParams, `Unknown tool: ${toolName}`);
440
+ const pollMs = parsePpidPollMs(process.env.CODEGRAPH_PPID_POLL_MS);
441
+ if (pollMs <= 0)
437
442
  return;
438
- }
439
- // If the default project isn't initialized yet, retry in case it was
440
- // initialized after the MCP server started (e.g. user ran codegraph init)
441
- await this.retryInitIfNeeded();
442
- const result = await this.toolHandler.execute(toolName, toolArgs);
443
- this.transport.sendResult(request.id, result);
443
+ this.ppidWatchdog = setInterval(() => {
444
+ const current = process.ppid;
445
+ const ppidChanged = current !== this.originalPpid;
446
+ const hostGone = this.hostPpid !== null && !(0, daemon_1.isProcessAlive)(this.hostPpid);
447
+ if (ppidChanged || hostGone) {
448
+ const reason = ppidChanged
449
+ ? `ppid ${this.originalPpid} -> ${current}`
450
+ : `host pid ${this.hostPpid} exited`;
451
+ process.stderr.write(`[CodeGraph MCP] Parent process exited (${reason}); shutting down.\n`);
452
+ this.stop();
453
+ }
454
+ }, pollMs);
455
+ this.ppidWatchdog.unref();
444
456
  }
445
457
  }
446
458
  exports.MCPServer = MCPServer;
459
+ function sleep(ms) {
460
+ // Deliberately NOT unref'd. During the daemon connect/takeover retry loop we
461
+ // may be between processes — no socket bound yet, no transport, no listener
462
+ // pinning the event loop. An unref'd timer would let Node drain the loop and
463
+ // exit silently before we get a chance to try again.
464
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
465
+ }
447
466
  // Export for use in CLI
448
467
  var transport_2 = require("./transport");
449
468
  Object.defineProperty(exports, "StdioTransport", { enumerable: true, get: function () { return transport_2.StdioTransport; } });
450
- var tools_2 = require("./tools");
451
- Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_2.tools; } });
452
- Object.defineProperty(exports, "ToolHandler", { enumerable: true, get: function () { return tools_2.ToolHandler; } });
469
+ var tools_1 = require("./tools");
470
+ Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_1.tools; } });
471
+ Object.defineProperty(exports, "ToolHandler", { enumerable: true, get: function () { return tools_1.ToolHandler; } });
472
+ // Surface a few daemon-mode bits for tests + diagnostics.
473
+ var daemon_2 = require("./daemon");
474
+ Object.defineProperty(exports, "Daemon", { enumerable: true, get: function () { return daemon_2.Daemon; } });
475
+ var version_1 = require("./version");
476
+ Object.defineProperty(exports, "CodeGraphPackageVersion", { enumerable: true, get: function () { return version_1.CodeGraphPackageVersion; } });
453
477
  //# sourceMappingURL=index.js.map