@stupidloud/codegraph 0.9.5 → 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.
- package/README.md +252 -116
- package/dist/bin/codegraph.js +52 -82
- package/dist/bin/codegraph.js.map +1 -1
- package/dist/context/formatter.d.ts.map +1 -1
- package/dist/context/formatter.js +25 -6
- package/dist/context/formatter.js.map +1 -1
- package/dist/context/index.d.ts +22 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +257 -6
- package/dist/context/index.js.map +1 -1
- package/dist/context/markers.d.ts +19 -0
- package/dist/context/markers.d.ts.map +1 -0
- package/dist/context/markers.js +22 -0
- package/dist/context/markers.js.map +1 -0
- package/dist/db/queries.d.ts +88 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +251 -7
- package/dist/db/queries.js.map +1 -1
- package/dist/db/sqlite-adapter.d.ts +7 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +3 -0
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +6 -20
- package/dist/directory.js.map +1 -1
- package/dist/extraction/generated-detection.d.ts +30 -0
- package/dist/extraction/generated-detection.d.ts.map +1 -0
- package/dist/extraction/generated-detection.js +80 -0
- package/dist/extraction/generated-detection.js.map +1 -0
- package/dist/extraction/grammars.d.ts +17 -1
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +65 -1
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +15 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +206 -98
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +45 -0
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/csharp.d.ts.map +1 -1
- package/dist/extraction/languages/csharp.js +2 -1
- package/dist/extraction/languages/csharp.js.map +1 -1
- package/dist/extraction/languages/go.d.ts.map +1 -1
- package/dist/extraction/languages/go.js +18 -2
- package/dist/extraction/languages/go.js.map +1 -1
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +2 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/java.d.ts.map +1 -1
- package/dist/extraction/languages/java.js +6 -0
- package/dist/extraction/languages/java.js.map +1 -1
- package/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/dist/extraction/languages/kotlin.js +6 -0
- package/dist/extraction/languages/kotlin.js.map +1 -1
- package/dist/extraction/languages/objc.d.ts +3 -0
- package/dist/extraction/languages/objc.d.ts.map +1 -0
- package/dist/extraction/languages/objc.js +133 -0
- package/dist/extraction/languages/objc.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +48 -0
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
- package/dist/extraction/mybatis-extractor.js +198 -0
- package/dist/extraction/mybatis-extractor.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +14 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +84 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +681 -20
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/vue-extractor.d.ts +15 -0
- package/dist/extraction/vue-extractor.d.ts.map +1 -1
- package/dist/extraction/vue-extractor.js +88 -0
- package/dist/extraction/vue-extractor.js.map +1 -1
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/dist/extraction/wasm-runtime-flags.js +1 -0
- package/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +5 -2
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +66 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -1
- package/dist/index.js.map +1 -1
- package/dist/installer/config-writer.d.ts +7 -8
- package/dist/installer/config-writer.d.ts.map +1 -1
- package/dist/installer/config-writer.js +7 -27
- package/dist/installer/config-writer.js.map +1 -1
- package/dist/installer/index.d.ts +3 -20
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +8 -39
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/instructions-template.d.ts +11 -21
- package/dist/installer/instructions-template.d.ts.map +1 -1
- package/dist/installer/instructions-template.js +12 -56
- package/dist/installer/instructions-template.js.map +1 -1
- package/dist/installer/targets/antigravity.d.ts +57 -0
- package/dist/installer/targets/antigravity.d.ts.map +1 -0
- package/dist/installer/targets/antigravity.js +308 -0
- package/dist/installer/targets/antigravity.js.map +1 -0
- package/dist/installer/targets/claude.d.ts +10 -1
- package/dist/installer/targets/claude.d.ts.map +1 -1
- package/dist/installer/targets/claude.js +25 -40
- package/dist/installer/targets/claude.js.map +1 -1
- package/dist/installer/targets/codex.d.ts.map +1 -1
- package/dist/installer/targets/codex.js +15 -13
- package/dist/installer/targets/codex.js.map +1 -1
- package/dist/installer/targets/cursor.d.ts.map +1 -1
- package/dist/installer/targets/cursor.js +9 -38
- package/dist/installer/targets/cursor.js.map +1 -1
- package/dist/installer/targets/gemini.d.ts +26 -0
- package/dist/installer/targets/gemini.d.ts.map +1 -0
- package/dist/installer/targets/gemini.js +167 -0
- package/dist/installer/targets/gemini.js.map +1 -0
- package/dist/installer/targets/hermes.d.ts.map +1 -1
- package/dist/installer/targets/hermes.js +57 -3
- package/dist/installer/targets/hermes.js.map +1 -1
- package/dist/installer/targets/kiro.d.ts +27 -0
- package/dist/installer/targets/kiro.d.ts.map +1 -0
- package/dist/installer/targets/kiro.js +178 -0
- package/dist/installer/targets/kiro.js.map +1 -0
- package/dist/installer/targets/opencode.d.ts.map +1 -1
- package/dist/installer/targets/opencode.js +15 -13
- package/dist/installer/targets/opencode.js.map +1 -1
- package/dist/installer/targets/registry.d.ts.map +1 -1
- package/dist/installer/targets/registry.js +6 -0
- package/dist/installer/targets/registry.js.map +1 -1
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +3 -2
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/installer/targets/types.d.ts +1 -16
- package/dist/installer/targets/types.d.ts.map +1 -1
- package/dist/mcp/daemon-paths.d.ts +46 -0
- package/dist/mcp/daemon-paths.d.ts.map +1 -0
- package/dist/mcp/daemon-paths.js +125 -0
- package/dist/mcp/daemon-paths.js.map +1 -0
- package/dist/mcp/daemon.d.ts +161 -0
- package/dist/mcp/daemon.d.ts.map +1 -0
- package/dist/mcp/daemon.js +403 -0
- package/dist/mcp/daemon.js.map +1 -0
- package/dist/mcp/engine.d.ts +105 -0
- package/dist/mcp/engine.d.ts.map +1 -0
- package/dist/mcp/engine.js +270 -0
- package/dist/mcp/engine.js.map +1 -0
- package/dist/mcp/index.d.ts +67 -53
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +315 -388
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/proxy.d.ts +81 -0
- package/dist/mcp/proxy.d.ts.map +1 -0
- package/dist/mcp/proxy.js +510 -0
- package/dist/mcp/proxy.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +21 -21
- package/dist/mcp/session.d.ts +77 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +294 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/tools.d.ts +160 -14
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1622 -322
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts +111 -29
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +181 -71
- package/dist/mcp/transport.js.map +1 -1
- package/dist/mcp/version.d.ts +19 -0
- package/dist/mcp/version.d.ts.map +1 -0
- package/dist/mcp/version.js +71 -0
- package/dist/mcp/version.js.map +1 -0
- package/dist/resolution/callback-synthesizer.d.ts +10 -0
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
- package/dist/resolution/callback-synthesizer.js +1300 -0
- package/dist/resolution/callback-synthesizer.js.map +1 -0
- package/dist/resolution/frameworks/csharp.d.ts.map +1 -1
- package/dist/resolution/frameworks/csharp.js +36 -8
- package/dist/resolution/frameworks/csharp.js.map +1 -1
- package/dist/resolution/frameworks/drupal.d.ts.map +1 -1
- package/dist/resolution/frameworks/drupal.js +44 -12
- package/dist/resolution/frameworks/drupal.js.map +1 -1
- package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
- package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
- package/dist/resolution/frameworks/expo-modules.js +143 -0
- package/dist/resolution/frameworks/expo-modules.js.map +1 -0
- package/dist/resolution/frameworks/express.d.ts.map +1 -1
- package/dist/resolution/frameworks/express.js +102 -19
- package/dist/resolution/frameworks/express.js.map +1 -1
- package/dist/resolution/frameworks/fabric.d.ts +3 -0
- package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
- package/dist/resolution/frameworks/fabric.js +354 -0
- package/dist/resolution/frameworks/fabric.js.map +1 -0
- package/dist/resolution/frameworks/go.d.ts.map +1 -1
- package/dist/resolution/frameworks/go.js +6 -3
- package/dist/resolution/frameworks/go.js.map +1 -1
- package/dist/resolution/frameworks/index.d.ts +5 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +25 -1
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/dist/resolution/frameworks/java.js +339 -12
- package/dist/resolution/frameworks/java.js.map +1 -1
- package/dist/resolution/frameworks/laravel.d.ts.map +1 -1
- package/dist/resolution/frameworks/laravel.js +17 -8
- package/dist/resolution/frameworks/laravel.js.map +1 -1
- package/dist/resolution/frameworks/nestjs.d.ts.map +1 -1
- package/dist/resolution/frameworks/nestjs.js +324 -0
- package/dist/resolution/frameworks/nestjs.js.map +1 -1
- package/dist/resolution/frameworks/play.d.ts +19 -0
- package/dist/resolution/frameworks/play.d.ts.map +1 -0
- package/dist/resolution/frameworks/play.js +111 -0
- package/dist/resolution/frameworks/play.js.map +1 -0
- package/dist/resolution/frameworks/python.d.ts.map +1 -1
- package/dist/resolution/frameworks/python.js +134 -16
- package/dist/resolution/frameworks/python.js.map +1 -1
- package/dist/resolution/frameworks/react-native.d.ts +3 -0
- package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
- package/dist/resolution/frameworks/react-native.js +360 -0
- package/dist/resolution/frameworks/react-native.js.map +1 -0
- package/dist/resolution/frameworks/react.d.ts.map +1 -1
- package/dist/resolution/frameworks/react.js +96 -3
- package/dist/resolution/frameworks/react.js.map +1 -1
- package/dist/resolution/frameworks/ruby.d.ts.map +1 -1
- package/dist/resolution/frameworks/ruby.js +106 -2
- package/dist/resolution/frameworks/ruby.js.map +1 -1
- package/dist/resolution/frameworks/rust.d.ts.map +1 -1
- package/dist/resolution/frameworks/rust.js +102 -5
- package/dist/resolution/frameworks/rust.js.map +1 -1
- package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
- package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift-objc.js +252 -0
- package/dist/resolution/frameworks/swift-objc.js.map +1 -0
- package/dist/resolution/frameworks/swift.d.ts.map +1 -1
- package/dist/resolution/frameworks/swift.js +30 -6
- package/dist/resolution/frameworks/swift.js.map +1 -1
- package/dist/resolution/go-module.d.ts +26 -0
- package/dist/resolution/go-module.d.ts.map +1 -0
- package/dist/resolution/go-module.js +78 -0
- package/dist/resolution/go-module.js.map +1 -0
- package/dist/resolution/import-resolver.d.ts +28 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +617 -5
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +11 -0
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +156 -3
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +212 -0
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/swift-objc-bridge.d.ts +134 -0
- package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
- package/dist/resolution/swift-objc-bridge.js +256 -0
- package/dist/resolution/swift-objc-bridge.js.map +1 -0
- package/dist/resolution/types.d.ts +44 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.d.ts +48 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -0
- package/dist/resolution/workspace-packages.js +208 -0
- package/dist/resolution/workspace-packages.js.map +1 -0
- package/dist/search/query-utils.d.ts +18 -0
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +30 -0
- package/dist/search/query-utils.js.map +1 -1
- package/dist/sync/git-hooks.d.ts.map +1 -1
- package/dist/sync/git-hooks.js +2 -0
- package/dist/sync/git-hooks.js.map +1 -1
- package/dist/sync/index.d.ts +3 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +8 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/watcher.d.ts +212 -8
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +465 -51
- package/dist/sync/watcher.js.map +1 -1
- package/dist/sync/worktree.d.ts +54 -0
- package/dist/sync/worktree.d.ts.map +1 -0
- package/dist/sync/worktree.js +137 -0
- package/dist/sync/worktree.js.map +1 -0
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/scripts/agent-eval/arms-F.sh +21 -0
- package/scripts/agent-eval/arms-matrix.sh +37 -0
- package/scripts/agent-eval/bench-readme.sh +28 -0
- package/scripts/agent-eval/bench-why-repo.sh +22 -0
- package/scripts/agent-eval/block-read-hook.sh +19 -0
- package/scripts/agent-eval/hook-settings.json +15 -0
- package/scripts/agent-eval/itrun.sh +24 -11
- package/scripts/agent-eval/parse-arms.mjs +116 -0
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -0
- package/scripts/agent-eval/probe-context.mjs +21 -0
- package/scripts/agent-eval/probe-explore.mjs +40 -0
- package/scripts/agent-eval/probe-node.mjs +20 -0
- package/scripts/agent-eval/probe-sweep.mjs +119 -0
- package/scripts/agent-eval/probe-trace.mjs +20 -0
- package/scripts/agent-eval/run-arms.sh +56 -0
- package/scripts/agent-eval/seq-matrix.mjs +137 -0
- package/scripts/npm-sdk.js +75 -0
- package/scripts/pack-npm.sh +25 -1
- package/scripts/prepare-release.mjs +270 -0
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,55 +68,53 @@ 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
|
|
55
|
-
const
|
|
74
|
+
const child_process_1 = require("child_process");
|
|
75
|
+
const directory_1 = require("../directory");
|
|
56
76
|
const transport_1 = require("./transport");
|
|
57
|
-
const
|
|
58
|
-
const
|
|
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");
|
|
59
82
|
const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
|
|
60
83
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
|
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.
|
|
81
87
|
*/
|
|
82
|
-
const
|
|
83
|
-
name: 'codegraph',
|
|
84
|
-
version: '0.1.0',
|
|
85
|
-
};
|
|
88
|
+
const DEFAULT_PPID_POLL_MS = 5000;
|
|
86
89
|
/**
|
|
87
|
-
*
|
|
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).
|
|
88
94
|
*/
|
|
89
|
-
const
|
|
95
|
+
const DAEMON_INTERNAL_ENV = 'CODEGRAPH_DAEMON_INTERNAL';
|
|
90
96
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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.
|
|
93
100
|
*/
|
|
94
|
-
const
|
|
101
|
+
const TAKEOVER_MAX_RETRIES = 5;
|
|
102
|
+
const TAKEOVER_RETRY_DELAY_MS = 100;
|
|
95
103
|
/**
|
|
96
|
-
* How
|
|
97
|
-
*
|
|
98
|
-
* (
|
|
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.
|
|
99
109
|
*/
|
|
100
|
-
|
|
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;
|
|
101
118
|
/**
|
|
102
119
|
* Resolve the PPID watchdog poll interval from an env override. A value of
|
|
103
120
|
* `0` disables the watchdog entirely (escape hatch for embedded scenarios
|
|
@@ -129,271 +146,170 @@ function parseHostPpid(raw) {
|
|
|
129
146
|
return null;
|
|
130
147
|
return parsed;
|
|
131
148
|
}
|
|
132
|
-
/**
|
|
133
|
-
function
|
|
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)
|
|
178
|
+
return null;
|
|
134
179
|
try {
|
|
135
|
-
|
|
136
|
-
return true;
|
|
180
|
+
return fs.realpathSync(root);
|
|
137
181
|
}
|
|
138
182
|
catch {
|
|
139
|
-
return
|
|
183
|
+
return root;
|
|
140
184
|
}
|
|
141
185
|
}
|
|
142
186
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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.
|
|
146
196
|
*/
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
157
231
|
}
|
|
158
232
|
/**
|
|
159
233
|
* MCP Server for CodeGraph
|
|
160
234
|
*
|
|
161
235
|
* Implements the Model Context Protocol to expose CodeGraph
|
|
162
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.
|
|
163
242
|
*/
|
|
164
243
|
class MCPServer {
|
|
165
|
-
transport;
|
|
166
|
-
cg = null;
|
|
167
|
-
toolHandler;
|
|
168
244
|
projectPath;
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
|
180
252
|
// baseline, even if start() runs after a fork-style reparent.
|
|
181
253
|
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
254
|
hostPpid = parseHostPpid(process.env[wasm_runtime_flags_1.HOST_PPID_ENV]);
|
|
187
|
-
|
|
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.
|
|
255
|
+
// Idempotency guard for stop().
|
|
191
256
|
stopped = false;
|
|
257
|
+
mode = 'unstarted';
|
|
192
258
|
constructor(projectPath) {
|
|
193
259
|
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
260
|
}
|
|
198
261
|
/**
|
|
199
|
-
* Start the MCP server
|
|
262
|
+
* Start the MCP server.
|
|
200
263
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
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.
|
|
281
270
|
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
271
|
+
* On any unexpected failure in step 4 we transparently fall back to direct
|
|
272
|
+
* mode — a misbehaving daemon must never block a session from starting.
|
|
284
273
|
*/
|
|
285
|
-
async
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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;
|
|
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();
|
|
311
279
|
}
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
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();
|
|
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');
|
|
333
284
|
}
|
|
334
|
-
|
|
335
|
-
|
|
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');
|
|
336
290
|
}
|
|
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
291
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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;
|
|
354
300
|
}
|
|
355
301
|
catch (err) {
|
|
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.
|
|
356
304
|
const msg = err instanceof Error ? err.message : String(err);
|
|
357
|
-
process.stderr.write(`[CodeGraph MCP]
|
|
358
|
-
|
|
359
|
-
await this.tryInitializeDefault(target);
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
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');
|
|
305
|
+
process.stderr.write(`[CodeGraph MCP] Proxy path failed (${msg}); falling back to direct mode.\n`);
|
|
306
|
+
return this.startDirect('proxy path threw');
|
|
393
307
|
}
|
|
394
308
|
}
|
|
395
309
|
/**
|
|
396
|
-
* Stop the server
|
|
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.
|
|
397
313
|
*/
|
|
398
314
|
stop() {
|
|
399
315
|
if (this.stopped)
|
|
@@ -403,148 +319,159 @@ class MCPServer {
|
|
|
403
319
|
clearInterval(this.ppidWatchdog);
|
|
404
320
|
this.ppidWatchdog = null;
|
|
405
321
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
322
|
+
if (this.daemon) {
|
|
323
|
+
void this.daemon.stop('stop()');
|
|
324
|
+
// Daemon.stop calls process.exit; nothing else to do.
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (this.session) {
|
|
328
|
+
this.session.stop();
|
|
329
|
+
this.session = null;
|
|
330
|
+
}
|
|
331
|
+
if (this.engine) {
|
|
332
|
+
this.engine.stop();
|
|
333
|
+
this.engine = null;
|
|
412
334
|
}
|
|
413
|
-
this.transport.stop();
|
|
414
335
|
process.exit(0);
|
|
415
336
|
}
|
|
416
|
-
/**
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
}
|
|
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`);
|
|
451
341
|
}
|
|
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,
|
|
346
|
+
});
|
|
347
|
+
if (this.projectPath) {
|
|
348
|
+
// Background init so the initialize response stays fast (#172).
|
|
349
|
+
void this.engine.ensureInitialized(this.projectPath);
|
|
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();
|
|
452
360
|
}
|
|
453
361
|
/**
|
|
454
|
-
*
|
|
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}).
|
|
455
369
|
*/
|
|
456
|
-
async
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
});
|
|
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);
|
|
508
392
|
}
|
|
393
|
+
process.stderr.write('[CodeGraph daemon] Could not acquire the daemon lock; exiting.\n');
|
|
394
|
+
process.exit(0);
|
|
509
395
|
}
|
|
510
396
|
/**
|
|
511
|
-
*
|
|
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.
|
|
512
403
|
*/
|
|
513
|
-
async
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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 });
|
|
426
|
+
}
|
|
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());
|
|
518
431
|
}
|
|
519
432
|
/**
|
|
520
|
-
*
|
|
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.
|
|
521
436
|
*/
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (!params || !params.name) {
|
|
525
|
-
this.transport.sendError(request.id, transport_1.ErrorCodes.InvalidParams, 'Missing tool name');
|
|
437
|
+
installPpidWatchdog() {
|
|
438
|
+
if (this.mode !== 'direct')
|
|
526
439
|
return;
|
|
527
|
-
|
|
528
|
-
|
|
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}`);
|
|
440
|
+
const pollMs = parsePpidPollMs(process.env.CODEGRAPH_PPID_POLL_MS);
|
|
441
|
+
if (pollMs <= 0)
|
|
534
442
|
return;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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();
|
|
541
456
|
}
|
|
542
457
|
}
|
|
543
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
|
+
}
|
|
544
466
|
// Export for use in CLI
|
|
545
467
|
var transport_2 = require("./transport");
|
|
546
468
|
Object.defineProperty(exports, "StdioTransport", { enumerable: true, get: function () { return transport_2.StdioTransport; } });
|
|
547
|
-
var
|
|
548
|
-
Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return
|
|
549
|
-
Object.defineProperty(exports, "ToolHandler", { enumerable: true, get: function () { return
|
|
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; } });
|
|
550
477
|
//# sourceMappingURL=index.js.map
|