@colbymchenry/codegraph-darwin-arm64 0.9.4 → 0.9.6

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