@codedroid/mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +11 -0
- package/.mcp.json +8 -0
- package/LICENSE.aura-mcp-connect +21 -0
- package/README.md +184 -2
- package/gemini-extension.json +14 -0
- package/package.json +44 -4
- package/skills/codedroid-phone/SKILL.md +92 -0
- package/skills/test-on-device/SKILL.md +73 -0
- package/src/adb-tool.js +200 -0
- package/src/daemon-transport.js +30 -0
- package/src/daemon.js +422 -0
- package/src/host.js +110 -0
- package/src/index.js +261 -0
- package/src/proxy-core.js +294 -0
- package/src/relay-transport.js +190 -0
- package/src/skill.js +39 -0
- package/src/status-tool.js +49 -0
- package/src/stdio-shim.js +146 -0
- package/src/version.js +2 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RelayPhoneTransport — an MCP SDK `Transport` to CodeDroid on the phone, through the CodeDroid
|
|
3
|
+
* server's relay.
|
|
4
|
+
*
|
|
5
|
+
* 1. Read this computer's pairing (host.js): the server, the host token, the phone.
|
|
6
|
+
* 2. Open ws(s)://<server>/v1/mcp/connect with the token, plus X-CodeDroid-Client and
|
|
7
|
+
* X-CodeDroid-Bridge, from which the server builds the phone's hello. The phone keeps its own
|
|
8
|
+
* socket to the server, so neither side needs the other's address.
|
|
9
|
+
* 3. The first frame is the answer: {type:"approved"} (after the user taps Allow, the first time),
|
|
10
|
+
* {type:"denied", reason}, or, from the server, {type:"unavailable", reason}.
|
|
11
|
+
* 4. Then MCP: one JSON-RPC message per text frame, both ways.
|
|
12
|
+
*
|
|
13
|
+
* A library: failures surface through start() rejecting or onclose/onerror, never process.exit().
|
|
14
|
+
*/
|
|
15
|
+
import WebSocket from "ws";
|
|
16
|
+
import { PAIRING_STEPS, pairedDevice, readHost } from "./host.js";
|
|
17
|
+
import { VERSION } from "./version.js";
|
|
18
|
+
|
|
19
|
+
export class PhoneUnavailableError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "PhoneUnavailableError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class PhoneDeniedError extends Error {
|
|
27
|
+
constructor(reason) {
|
|
28
|
+
super(`The phone refused this computer${reason ? `: ${reason}` : "."}`);
|
|
29
|
+
this.name = "PhoneDeniedError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The relay's WebSocket URL for a normalized [server] (host.js): https → wss, http → ws. */
|
|
34
|
+
export function connectUrl(server) {
|
|
35
|
+
const url = new URL(`${server}/v1/mcp/connect`);
|
|
36
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
37
|
+
return url.href;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A socket error's reason. Node leaves the message empty when every address failed (localhost: ::1 and 127.0.0.1). */
|
|
41
|
+
function errorText(e) {
|
|
42
|
+
return e.message || e.errors?.map((inner) => inner.message).join("; ") || e.code || String(e);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class RelayPhoneTransport {
|
|
46
|
+
/**
|
|
47
|
+
* @param {object} [options]
|
|
48
|
+
* @param {Record<string, string | undefined>} [options.env]
|
|
49
|
+
* @param {number} [options.approvalTimeoutMs] the first time, a person taps Allow on the phone
|
|
50
|
+
* @param {() => {server: string, host_id: string, token: string, device: {id: string, model: string | null}} | null} [options.host]
|
|
51
|
+
* this computer's pairing, null when it has none
|
|
52
|
+
* @param {(msg: string) => void} [options.log]
|
|
53
|
+
*/
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.onmessage = undefined;
|
|
56
|
+
this.onclose = undefined;
|
|
57
|
+
this.onerror = undefined;
|
|
58
|
+
this.device = null;
|
|
59
|
+
|
|
60
|
+
this._env = options.env ?? process.env;
|
|
61
|
+
this._approvalTimeoutMs = options.approvalTimeoutMs ?? 130_000;
|
|
62
|
+
this._host = options.host ?? readHost;
|
|
63
|
+
this._log = options.log ?? (() => {});
|
|
64
|
+
this._ws = null;
|
|
65
|
+
this._open = false;
|
|
66
|
+
this._closed = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async start() {
|
|
70
|
+
const host = this._host();
|
|
71
|
+
if (!host) throw new PhoneUnavailableError(`This computer is not paired with a phone. ${PAIRING_STEPS}`);
|
|
72
|
+
this.device = pairedDevice(host);
|
|
73
|
+
this._log(`Phone ${host.device?.id} (${host.device?.model ?? "unknown model"}) through ${host.server}`);
|
|
74
|
+
await this._handshake(host);
|
|
75
|
+
this._log("Approved by the phone");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_handshake(host) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const ws = new WebSocket(connectUrl(host.server), {
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${host.token}`,
|
|
83
|
+
// Only what the user set: one daemon serves every MCP client here, so a guess from its
|
|
84
|
+
// environment would name whichever client happened to start it.
|
|
85
|
+
"X-CodeDroid-Client": this._env.CODEDROID_MCP_CLIENT?.trim() ?? "",
|
|
86
|
+
"X-CodeDroid-Bridge": VERSION,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
this._ws = ws;
|
|
90
|
+
|
|
91
|
+
// Settles the handshake once: resolves without an error, otherwise drops the socket.
|
|
92
|
+
let pending = true;
|
|
93
|
+
const settle = (error) => {
|
|
94
|
+
if (!pending) return;
|
|
95
|
+
pending = false;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
if (!error) {
|
|
98
|
+
// Before resolving: a frame right behind the answer is already the phone's.
|
|
99
|
+
this._open = true;
|
|
100
|
+
resolve();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
ws.terminate();
|
|
104
|
+
reject(error);
|
|
105
|
+
};
|
|
106
|
+
const timer = setTimeout(
|
|
107
|
+
() =>
|
|
108
|
+
settle(
|
|
109
|
+
new PhoneUnavailableError(
|
|
110
|
+
`The phone did not answer within ${Math.round(this._approvalTimeoutMs / 1000)} s. If it asked ` +
|
|
111
|
+
"whether to allow this computer, tap Allow, then try again.",
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
this._approvalTimeoutMs,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
ws.on("unexpected-response", (req, res) => {
|
|
118
|
+
res.resume();
|
|
119
|
+
settle(
|
|
120
|
+
new PhoneUnavailableError(
|
|
121
|
+
res.statusCode === 401 || res.statusCode === 403
|
|
122
|
+
? `This computer's pairing was removed or is no longer valid. Pair it again. ${PAIRING_STEPS}`
|
|
123
|
+
: `The CodeDroid server at ${host.server} refused the connection (HTTP ${res.statusCode}).`,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
ws.on("message", (data) => {
|
|
128
|
+
const line = data.toString("utf8");
|
|
129
|
+
if (this._open) return void this._deliver(line);
|
|
130
|
+
if (!pending) return;
|
|
131
|
+
let reply = null;
|
|
132
|
+
try {
|
|
133
|
+
reply = JSON.parse(line);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// Not a handshake answer: refused below.
|
|
136
|
+
}
|
|
137
|
+
if (reply?.type === "approved") settle(null);
|
|
138
|
+
else if (reply?.type === "denied") settle(new PhoneDeniedError(reply.reason));
|
|
139
|
+
else if (reply?.type === "unavailable") {
|
|
140
|
+
settle(new PhoneUnavailableError(reply.reason || "The phone is not connected to CodeDroid."));
|
|
141
|
+
} else {
|
|
142
|
+
settle(new PhoneUnavailableError("The phone answered something other than a handshake; update codedroid-mcp."));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
ws.on("error", (e) => {
|
|
146
|
+
if (this._open) this.onerror?.(e);
|
|
147
|
+
else settle(new PhoneUnavailableError(`Could not reach the CodeDroid server at ${host.server}: ${errorText(e)}`));
|
|
148
|
+
});
|
|
149
|
+
ws.on("close", () => {
|
|
150
|
+
if (this._open) return void this._handleClosed();
|
|
151
|
+
settle(
|
|
152
|
+
new PhoneUnavailableError(
|
|
153
|
+
"The CodeDroid server closed the connection before the phone answered. Check that CodeDroid is open " +
|
|
154
|
+
"on the phone and online, then try again.",
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_deliver(line) {
|
|
162
|
+
let message;
|
|
163
|
+
try {
|
|
164
|
+
message = JSON.parse(line);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
this.onerror?.(new Error(`Failed to parse a frame from the phone: ${e.message}`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.onmessage?.(message);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {import("@modelcontextprotocol/sdk/types.js").JSONRPCMessage} message */
|
|
173
|
+
async send(message) {
|
|
174
|
+
const ws = this._ws;
|
|
175
|
+
if (!this._open || ws.readyState !== WebSocket.OPEN) throw new Error("Phone connection is not open");
|
|
176
|
+
await new Promise((resolve, reject) => ws.send(JSON.stringify(message), (e) => (e ? reject(e) : resolve())));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async close() {
|
|
180
|
+
this._ws?.close(1000);
|
|
181
|
+
this._handleClosed();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_handleClosed() {
|
|
185
|
+
if (this._closed) return;
|
|
186
|
+
this._closed = true;
|
|
187
|
+
this._open = false;
|
|
188
|
+
this.onclose?.();
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/skill.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent skills that ship with this package: `codedroid-phone` (driving the phone through
|
|
3
|
+
* CodeDroid) and `test-on-device` (the developer loop: build, install, drive, check, report).
|
|
4
|
+
*
|
|
5
|
+
* - The Claude Code plugin (plugin/claude) carries them already; `skill install` is for using the
|
|
6
|
+
* MCP server without the plugin: Claude Code (~/.claude/skills), Codex (--codex: ~/.codex/skills),
|
|
7
|
+
* or any directory (--dir).
|
|
8
|
+
* - Change together: the skills name tools, so a renamed tool means editing them
|
|
9
|
+
* (test/skill.test.mjs checks every name they use exists).
|
|
10
|
+
*
|
|
11
|
+
* Forked from aura-mcp-connect's skill.js (MIT, Copyright (c) 2026 Dinesh Kumar C; see
|
|
12
|
+
* LICENSE.aura-mcp-connect).
|
|
13
|
+
*/
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import os from "os";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
|
|
19
|
+
export const SKILLS = ["codedroid-phone", "test-on-device"];
|
|
20
|
+
|
|
21
|
+
export const SKILLS_SOURCE = fileURLToPath(new URL("../skills/", import.meta.url));
|
|
22
|
+
|
|
23
|
+
export function skillSource(name) {
|
|
24
|
+
return path.join(SKILLS_SOURCE, name, "SKILL.md");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function defaultSkillsDir({ codex = false } = {}) {
|
|
28
|
+
return codex ? path.join(os.homedir(), ".codex", "skills") : path.join(os.homedir(), ".claude", "skills");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Copies every skill to <dir>/<name>/SKILL.md, overwriting older copies; returns the written paths. */
|
|
32
|
+
export function installSkills({ dir = defaultSkillsDir() } = {}) {
|
|
33
|
+
return SKILLS.map((name) => {
|
|
34
|
+
const target = path.join(dir, name, "SKILL.md");
|
|
35
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
36
|
+
fs.copyFileSync(skillSource(name), target);
|
|
37
|
+
return target;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codedroid_status — whether CodeDroid on the phone is connected, and if not, what to do about it.
|
|
3
|
+
*
|
|
4
|
+
* Served by the daemon itself, so it answers when the phone cannot: the one tool an agent can
|
|
5
|
+
* call to learn that this computer is not paired, the phone is offline, or the user has not
|
|
6
|
+
* tapped Allow yet. Calling it also tries to connect again.
|
|
7
|
+
*/
|
|
8
|
+
import { pairedDevice, readHost } from "./host.js";
|
|
9
|
+
import { VERSION } from "./version.js";
|
|
10
|
+
|
|
11
|
+
const DESCRIPTION =
|
|
12
|
+
"Whether CodeDroid on the Android phone is connected to this computer (the phone's device id and model, " +
|
|
13
|
+
"the CodeDroid server), and if not, why and how to fix it. Also retries the connection. Once connected " +
|
|
14
|
+
"it returns the phone's instructions for driving it.";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import("./daemon.js").PhoneManager} phoneManager
|
|
18
|
+
* @param {{host?: () => object | null}} [deps] this computer's pairing, for the phone it points at
|
|
19
|
+
*/
|
|
20
|
+
export function createStatusTool(phoneManager, { host = readHost } = {}) {
|
|
21
|
+
return {
|
|
22
|
+
definition: {
|
|
23
|
+
name: "codedroid_status",
|
|
24
|
+
title: "CodeDroid connection",
|
|
25
|
+
description: DESCRIPTION,
|
|
26
|
+
inputSchema: { type: "object", properties: {} },
|
|
27
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
28
|
+
},
|
|
29
|
+
async handler() {
|
|
30
|
+
let error = null;
|
|
31
|
+
let client = null;
|
|
32
|
+
try {
|
|
33
|
+
client = await phoneManager.getClient();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
error = e.message;
|
|
36
|
+
}
|
|
37
|
+
const status = {
|
|
38
|
+
connected: !error,
|
|
39
|
+
bridge: VERSION,
|
|
40
|
+
// Not connected: the phone this computer is paired with now, which a new pairing may have changed.
|
|
41
|
+
device: client ? phoneManager.device : pairedDevice(host()),
|
|
42
|
+
...(error ? { problem: error } : {}),
|
|
43
|
+
// A session that opened before the phone connected never got these at its handshake.
|
|
44
|
+
...(client?.getInstructions() ? { instructions: client.getInstructions() } : {}),
|
|
45
|
+
};
|
|
46
|
+
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }], isError: false };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio-shim — the `codedroid-mcp` stdio entry point MCP clients launch. It
|
|
3
|
+
* proxies to the shared local daemon, auto-starting it when needed, so every
|
|
4
|
+
* client on this computer shares one connection to the phone.
|
|
5
|
+
*
|
|
6
|
+
* IDE ── stdio ── this shim ── HTTP ── daemon ── relay ── CodeDroid on the phone
|
|
7
|
+
*
|
|
8
|
+
* Forked from aura-mcp-connect (MIT, Copyright (c) 2026 Dinesh Kumar C; see
|
|
9
|
+
* LICENSE.aura-mcp-connect).
|
|
10
|
+
*
|
|
11
|
+
* Any number of shims (one per IDE/window) share the one phone connection.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "child_process";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { buildProxyServer } from "./proxy-core.js";
|
|
18
|
+
import { DEFAULT_PORT } from "./daemon.js";
|
|
19
|
+
import { daemonTransport } from "./daemon-transport.js";
|
|
20
|
+
import { VERSION } from "./version.js";
|
|
21
|
+
|
|
22
|
+
/** The daemon's /health on [port], or null when no CodeDroid daemon answers there. */
|
|
23
|
+
export async function fetchHealth(port, timeoutMs = 1500) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) return null;
|
|
31
|
+
const body = await res.json();
|
|
32
|
+
return body?.service === "codedroid-mcp-daemon" ? body : null;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return null;
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function ensureDaemonRunning(port, log, { enableAdbTool = true } = {}) {
|
|
41
|
+
const existing = await fetchHealth(port);
|
|
42
|
+
if (existing) {
|
|
43
|
+
// A daemon's tool set is fixed at startup, so an already-running one can't
|
|
44
|
+
// be changed by a later client. Say so, or the user has nothing to go on.
|
|
45
|
+
if (enableAdbTool !== existing.adbToolEnabled) {
|
|
46
|
+
log(
|
|
47
|
+
`A daemon is already running on port ${port} with the adb tool ` +
|
|
48
|
+
`${existing.adbToolEnabled ? "on" : "off"}; CODEDROID_MCP_ADB had no effect. ` +
|
|
49
|
+
`Stop that daemon (codedroid-mcp stop) and let this client restart it.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
log(`Daemon not running — starting it on port ${port}...`);
|
|
56
|
+
const indexPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
57
|
+
// Env (and therefore CODEDROID_MCP_ADB) is inherited — deliberately not
|
|
58
|
+
// filtered here, since that inheritance is what lets a client arm the daemon
|
|
59
|
+
// it auto-starts. The argv is fixed, so a CLI flag could never reach here.
|
|
60
|
+
const child = spawn(process.execPath, [indexPath, "daemon", `--port=${port}`], {
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: "ignore",
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
});
|
|
65
|
+
child.unref();
|
|
66
|
+
|
|
67
|
+
const deadline = Date.now() + 20_000;
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
if (await fetchHealth(port)) {
|
|
70
|
+
log("Daemon is up.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
74
|
+
}
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Could not start the CodeDroid MCP daemon on port ${port} within 20s. ` +
|
|
77
|
+
`Try running it manually to see why: codedroid-mcp daemon --port=${port}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run the stdio shim until the IDE closes stdin or the daemon goes away.
|
|
83
|
+
* @param {object} [options]
|
|
84
|
+
* @param {number} [options.port]
|
|
85
|
+
* @param {(msg: string) => void} [options.log] stderr logger
|
|
86
|
+
* @param {boolean} [options.enableAdbTool] Only used to warn when an already-
|
|
87
|
+
* running daemon can't honour the request — the shim never registers the
|
|
88
|
+
* tool itself, it forwards through to whichever daemon owns it.
|
|
89
|
+
*/
|
|
90
|
+
export async function runStdioShim({
|
|
91
|
+
port = DEFAULT_PORT,
|
|
92
|
+
log = () => {},
|
|
93
|
+
enableAdbTool = true,
|
|
94
|
+
} = {}) {
|
|
95
|
+
await ensureDaemonRunning(port, log, { enableAdbTool });
|
|
96
|
+
|
|
97
|
+
const client = new Client({ name: "codedroid-mcp-shim", version: VERSION });
|
|
98
|
+
const httpTransport = daemonTransport(port);
|
|
99
|
+
// Long initialize budget: the daemon may be doing the first phone connect
|
|
100
|
+
// (including the on-phone approval tap) behind this call.
|
|
101
|
+
await client.connect(httpTransport, { timeout: 150_000 });
|
|
102
|
+
|
|
103
|
+
const server = await buildProxyServer({
|
|
104
|
+
getClient: async () => client,
|
|
105
|
+
name: "codedroid",
|
|
106
|
+
version: VERSION,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Pass phone/daemon notifications through to the IDE.
|
|
110
|
+
client.fallbackNotificationHandler = async (notification) => {
|
|
111
|
+
try {
|
|
112
|
+
await server.notification(notification);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
/* session closing — drop */
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
client.onclose = () => {
|
|
119
|
+
log("Daemon connection closed — exiting so the IDE can reconnect.");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const stdioTransport = new StdioServerTransport();
|
|
124
|
+
let closing = false;
|
|
125
|
+
const shutdown = async () => {
|
|
126
|
+
if (closing) return;
|
|
127
|
+
closing = true;
|
|
128
|
+
// IDE closed our stdin — release the daemon session and leave cleanly.
|
|
129
|
+
client.onclose = undefined;
|
|
130
|
+
try {
|
|
131
|
+
await client.close();
|
|
132
|
+
} catch (e) {
|
|
133
|
+
/* already gone */
|
|
134
|
+
}
|
|
135
|
+
process.exit(0);
|
|
136
|
+
};
|
|
137
|
+
server.onclose = shutdown;
|
|
138
|
+
// The SDK's stdio transport does not close itself at EOF. Without these hooks the
|
|
139
|
+
// client eventually kills the shim and its daemon session is left behind.
|
|
140
|
+
process.stdin.once("end", shutdown);
|
|
141
|
+
process.once("SIGTERM", shutdown);
|
|
142
|
+
process.once("SIGINT", shutdown);
|
|
143
|
+
|
|
144
|
+
await server.connect(stdioTransport);
|
|
145
|
+
log(`Bridging stdio ↔ daemon on port ${port}.`);
|
|
146
|
+
}
|
package/src/version.js
ADDED