@jobshimo/browser-link 0.7.9 → 0.7.12
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/dist/bridge/ipc-client.d.ts +50 -0
- package/dist/bridge/ipc-client.js +247 -0
- package/dist/bridge/ipc-client.js.map +1 -0
- package/dist/bridge/{client.d.ts → proxy.d.ts} +1 -50
- package/dist/bridge/proxy.js +231 -0
- package/dist/bridge/proxy.js.map +1 -0
- package/dist/bridge/ws-bridge.d.ts +26 -0
- package/dist/bridge/ws-bridge.js +213 -0
- package/dist/bridge/ws-bridge.js.map +1 -0
- package/dist/commands/about.d.ts +2 -0
- package/dist/commands/about.js +5 -0
- package/dist/commands/about.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -207
- package/dist/server.js.map +1 -1
- package/dist/ui/screens/about.js +1 -1
- package/dist/ui/screens/about.js.map +1 -1
- package/package.json +1 -1
- package/dist/bridge/client.js +0 -470
- package/dist/bridge/client.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Raised when the proxy cannot complete the IPC handshake. */
|
|
2
|
+
export declare class HandshakeError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export interface ConnectOptions {
|
|
6
|
+
host?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
/** How long to wait for a hello-ack after sending the hello. */
|
|
9
|
+
handshakeTimeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ConnectionInfo {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
version: string;
|
|
14
|
+
}
|
|
15
|
+
/** Minimal IPC client. Owns the TCP socket, handles framing, runs the
|
|
16
|
+
* handshake, dispatches mcp.response frames back to whoever sent the
|
|
17
|
+
* matching mcp.request. */
|
|
18
|
+
export declare class IpcClient {
|
|
19
|
+
private socket;
|
|
20
|
+
private buffer;
|
|
21
|
+
private nextRequestId;
|
|
22
|
+
private pendingRequests;
|
|
23
|
+
private closeListeners;
|
|
24
|
+
private notificationListeners;
|
|
25
|
+
private closed;
|
|
26
|
+
/** Open the TCP connection, perform the handshake, and resolve with the
|
|
27
|
+
* session info from the primary's hello-ack. On any handshake failure,
|
|
28
|
+
* rejects with HandshakeError and tears down the socket. */
|
|
29
|
+
connect(token: string, opts?: ConnectOptions): Promise<ConnectionInfo>;
|
|
30
|
+
private handshakeReceiver;
|
|
31
|
+
/** Forward a JSON-RPC request as an mcp.request frame. Resolves with the
|
|
32
|
+
* primary's JSON-RPC response payload. Rejects if the socket closes. */
|
|
33
|
+
sendMcpRequest(jsonRpcPayload: unknown): Promise<unknown>;
|
|
34
|
+
/** Forward a JSON-RPC notification as an mcp.notification frame. Fire-
|
|
35
|
+
* and-forget — notifications have no response in JSON-RPC. */
|
|
36
|
+
sendMcpNotification(jsonRpcPayload: unknown): void;
|
|
37
|
+
/** Register a callback invoked when the IPC connection drops. The
|
|
38
|
+
* `reason` argument distinguishes a primary-closing broadcast from a
|
|
39
|
+
* plain remote close so callers can decide to re-elect vs exit. */
|
|
40
|
+
onClose(cb: (reason: 'remote' | 'local' | 'primary-closing') => void): void;
|
|
41
|
+
/** Register a callback for unsolicited notifications from the primary
|
|
42
|
+
* (e.g. tools/list_changed in the future). */
|
|
43
|
+
onNotification(cb: (payload: unknown) => void): void;
|
|
44
|
+
/** Close the IPC connection. */
|
|
45
|
+
disconnect(): Promise<void>;
|
|
46
|
+
private ingest;
|
|
47
|
+
private handleFrame;
|
|
48
|
+
private onSocketClose;
|
|
49
|
+
private notifyClose;
|
|
50
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { connect } from 'node:net';
|
|
2
|
+
import { IPC_HOST, IPC_PORT, IPC_PROTOCOL_VERSION, encodeFrame, parseFrame, } from './protocol.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bare IPC client used by the proxy. Owns the TCP socket, handles framing,
|
|
5
|
+
* runs the hello handshake, and dispatches mcp.response frames back to the
|
|
6
|
+
* caller that sent the matching mcp.request. The proxy wiring (stdin/stdout
|
|
7
|
+
* pipe + reelect loop) lives in proxy.ts.
|
|
8
|
+
*/
|
|
9
|
+
function log(msg) {
|
|
10
|
+
console.error(`[browser-link proxy] ${msg}`);
|
|
11
|
+
}
|
|
12
|
+
/** Raised when the proxy cannot complete the IPC handshake. */
|
|
13
|
+
export class HandshakeError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'HandshakeError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Minimal IPC client. Owns the TCP socket, handles framing, runs the
|
|
20
|
+
* handshake, dispatches mcp.response frames back to whoever sent the
|
|
21
|
+
* matching mcp.request. */
|
|
22
|
+
export class IpcClient {
|
|
23
|
+
socket = null;
|
|
24
|
+
buffer = '';
|
|
25
|
+
nextRequestId = 1;
|
|
26
|
+
pendingRequests = new Map();
|
|
27
|
+
closeListeners = [];
|
|
28
|
+
notificationListeners = [];
|
|
29
|
+
closed = false;
|
|
30
|
+
/** Open the TCP connection, perform the handshake, and resolve with the
|
|
31
|
+
* session info from the primary's hello-ack. On any handshake failure,
|
|
32
|
+
* rejects with HandshakeError and tears down the socket. */
|
|
33
|
+
async connect(token, opts = {}) {
|
|
34
|
+
const host = opts.host ?? IPC_HOST;
|
|
35
|
+
const port = opts.port ?? IPC_PORT;
|
|
36
|
+
const handshakeTimeoutMs = opts.handshakeTimeoutMs ?? 4000;
|
|
37
|
+
const socket = await new Promise((resolve, reject) => {
|
|
38
|
+
const s = connect({ host, port });
|
|
39
|
+
s.once('connect', () => {
|
|
40
|
+
resolve(s);
|
|
41
|
+
});
|
|
42
|
+
s.once('error', (err) => {
|
|
43
|
+
reject(err);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
this.socket = socket;
|
|
47
|
+
socket.on('data', (chunk) => {
|
|
48
|
+
this.ingest(chunk);
|
|
49
|
+
});
|
|
50
|
+
socket.on('close', () => {
|
|
51
|
+
this.onSocketClose('remote');
|
|
52
|
+
});
|
|
53
|
+
socket.on('error', (err) => {
|
|
54
|
+
log(`Socket error: ${err.message}`);
|
|
55
|
+
});
|
|
56
|
+
// Send hello, wait for ack or reject or timeout.
|
|
57
|
+
const ack = await new Promise((resolve, reject) => {
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
reject(new HandshakeError('Handshake timed out — primary did not reply.'));
|
|
60
|
+
}, handshakeTimeoutMs);
|
|
61
|
+
const onFirstFrame = (frame) => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
if (frame.kind === 'hello-ack') {
|
|
64
|
+
resolve({ sessionId: frame.sessionId, version: frame.version });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (frame.kind === 'hello-reject') {
|
|
68
|
+
reject(new HandshakeError(`Primary rejected: ${frame.reason}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
reject(new HandshakeError(`Unexpected first frame: ${frame.kind}`));
|
|
72
|
+
};
|
|
73
|
+
this.handshakeReceiver = onFirstFrame;
|
|
74
|
+
try {
|
|
75
|
+
socket.write(encodeFrame({ kind: 'hello', version: IPC_PROTOCOL_VERSION, token }));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
}
|
|
81
|
+
}).catch((err) => {
|
|
82
|
+
void this.disconnect().catch(() => {
|
|
83
|
+
/* ignore */
|
|
84
|
+
});
|
|
85
|
+
throw err;
|
|
86
|
+
});
|
|
87
|
+
this.handshakeReceiver = null;
|
|
88
|
+
log(`Handshake ok — session=${ack.sessionId}`);
|
|
89
|
+
return ack;
|
|
90
|
+
}
|
|
91
|
+
handshakeReceiver = null;
|
|
92
|
+
/** Forward a JSON-RPC request as an mcp.request frame. Resolves with the
|
|
93
|
+
* primary's JSON-RPC response payload. Rejects if the socket closes. */
|
|
94
|
+
sendMcpRequest(jsonRpcPayload) {
|
|
95
|
+
const socket = this.socket;
|
|
96
|
+
if (!socket || this.closed) {
|
|
97
|
+
return Promise.reject(new Error('Proxy is not connected.'));
|
|
98
|
+
}
|
|
99
|
+
const requestId = this.nextRequestId++;
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
this.pendingRequests.set(requestId, resolve);
|
|
102
|
+
const onCloseHandler = () => {
|
|
103
|
+
if (this.pendingRequests.delete(requestId)) {
|
|
104
|
+
reject(new Error('Primary connection closed while a request was in flight.'));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.onClose(onCloseHandler);
|
|
108
|
+
try {
|
|
109
|
+
socket.write(encodeFrame({ kind: 'mcp.request', requestId, payload: jsonRpcPayload }));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
this.pendingRequests.delete(requestId);
|
|
113
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Forward a JSON-RPC notification as an mcp.notification frame. Fire-
|
|
118
|
+
* and-forget — notifications have no response in JSON-RPC. */
|
|
119
|
+
sendMcpNotification(jsonRpcPayload) {
|
|
120
|
+
if (!this.socket || this.closed)
|
|
121
|
+
return;
|
|
122
|
+
try {
|
|
123
|
+
this.socket.write(encodeFrame({ kind: 'mcp.notification', payload: jsonRpcPayload }));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* socket gone; close handler will fire */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Register a callback invoked when the IPC connection drops. The
|
|
130
|
+
* `reason` argument distinguishes a primary-closing broadcast from a
|
|
131
|
+
* plain remote close so callers can decide to re-elect vs exit. */
|
|
132
|
+
onClose(cb) {
|
|
133
|
+
this.closeListeners.push(cb);
|
|
134
|
+
}
|
|
135
|
+
/** Register a callback for unsolicited notifications from the primary
|
|
136
|
+
* (e.g. tools/list_changed in the future). */
|
|
137
|
+
onNotification(cb) {
|
|
138
|
+
this.notificationListeners.push(cb);
|
|
139
|
+
}
|
|
140
|
+
/** Close the IPC connection. */
|
|
141
|
+
disconnect() {
|
|
142
|
+
if (this.closed)
|
|
143
|
+
return Promise.resolve();
|
|
144
|
+
this.closed = true;
|
|
145
|
+
if (this.socket) {
|
|
146
|
+
const s = this.socket;
|
|
147
|
+
this.socket = null;
|
|
148
|
+
try {
|
|
149
|
+
s.end();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
/* ignore */
|
|
153
|
+
}
|
|
154
|
+
s.destroy();
|
|
155
|
+
// Fire close listeners with reason=local (we initiated it).
|
|
156
|
+
this.notifyClose('local');
|
|
157
|
+
}
|
|
158
|
+
return Promise.resolve();
|
|
159
|
+
}
|
|
160
|
+
ingest(chunk) {
|
|
161
|
+
this.buffer += chunk.toString('utf8');
|
|
162
|
+
let nl;
|
|
163
|
+
while ((nl = this.buffer.indexOf('\n')) >= 0) {
|
|
164
|
+
const line = this.buffer.slice(0, nl);
|
|
165
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
166
|
+
if (line.length === 0)
|
|
167
|
+
continue;
|
|
168
|
+
const frame = parseFrame(line);
|
|
169
|
+
if (!frame) {
|
|
170
|
+
log('Invalid frame from primary; dropping.');
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
this.handleFrame(frame);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
handleFrame(frame) {
|
|
177
|
+
if (this.handshakeReceiver) {
|
|
178
|
+
this.handshakeReceiver(frame);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
switch (frame.kind) {
|
|
182
|
+
case 'mcp.response': {
|
|
183
|
+
const resolve = this.pendingRequests.get(frame.requestId);
|
|
184
|
+
if (resolve) {
|
|
185
|
+
this.pendingRequests.delete(frame.requestId);
|
|
186
|
+
resolve(frame.payload);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
case 'mcp.notification': {
|
|
191
|
+
for (const cb of this.notificationListeners)
|
|
192
|
+
cb(frame.payload);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
case 'ping': {
|
|
196
|
+
try {
|
|
197
|
+
this.socket?.write(encodeFrame({ kind: 'pong' }));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
/* socket gone */
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
case 'pong': {
|
|
205
|
+
// Future: we can send our own pings if needed. Today the primary
|
|
206
|
+
// initiates the heartbeat; pong from us → primary.
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
case 'primary-closing': {
|
|
210
|
+
log(`Primary signalled close (${frame.reason ?? 'no reason'}).`);
|
|
211
|
+
this.notifyClose('primary-closing');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
onSocketClose(reason) {
|
|
219
|
+
if (this.closed)
|
|
220
|
+
return;
|
|
221
|
+
this.closed = true;
|
|
222
|
+
log('Socket closed by primary.');
|
|
223
|
+
this.notifyClose(reason);
|
|
224
|
+
}
|
|
225
|
+
notifyClose(reason) {
|
|
226
|
+
// Reject every in-flight request first so callers don't hang.
|
|
227
|
+
for (const resolve of this.pendingRequests.values()) {
|
|
228
|
+
// We resolve with a JSON-RPC error envelope so the MCP client gets
|
|
229
|
+
// a tool error instead of a stuck promise.
|
|
230
|
+
resolve({
|
|
231
|
+
jsonrpc: '2.0',
|
|
232
|
+
id: null,
|
|
233
|
+
error: { code: -32000, message: 'browser-link primary disconnected.' },
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
this.pendingRequests.clear();
|
|
237
|
+
for (const cb of this.closeListeners.splice(0)) {
|
|
238
|
+
try {
|
|
239
|
+
cb(reason);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
log(`Close listener threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=ipc-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipc-client.js","sourceRoot":"","sources":["../../src/bridge/ipc-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,54 +1,5 @@
|
|
|
1
1
|
import { type Interface } from 'node:readline';
|
|
2
|
-
|
|
3
|
-
export declare class HandshakeError extends Error {
|
|
4
|
-
constructor(message: string);
|
|
5
|
-
}
|
|
6
|
-
export interface ConnectOptions {
|
|
7
|
-
host?: string;
|
|
8
|
-
port?: number;
|
|
9
|
-
/** How long to wait for a hello-ack after sending the hello. */
|
|
10
|
-
handshakeTimeoutMs?: number;
|
|
11
|
-
}
|
|
12
|
-
export interface ConnectionInfo {
|
|
13
|
-
sessionId: string;
|
|
14
|
-
version: string;
|
|
15
|
-
}
|
|
16
|
-
/** Minimal IPC client. Owns the TCP socket, handles framing, runs the
|
|
17
|
-
* handshake, dispatches mcp.response frames back to whoever sent the
|
|
18
|
-
* matching mcp.request. */
|
|
19
|
-
export declare class IpcClient {
|
|
20
|
-
private socket;
|
|
21
|
-
private buffer;
|
|
22
|
-
private nextRequestId;
|
|
23
|
-
private pendingRequests;
|
|
24
|
-
private closeListeners;
|
|
25
|
-
private notificationListeners;
|
|
26
|
-
private closed;
|
|
27
|
-
/** Open the TCP connection, perform the handshake, and resolve with the
|
|
28
|
-
* session info from the primary's hello-ack. On any handshake failure,
|
|
29
|
-
* rejects with HandshakeError and tears down the socket. */
|
|
30
|
-
connect(token: string, opts?: ConnectOptions): Promise<ConnectionInfo>;
|
|
31
|
-
private handshakeReceiver;
|
|
32
|
-
/** Forward a JSON-RPC request as an mcp.request frame. Resolves with the
|
|
33
|
-
* primary's JSON-RPC response payload. Rejects if the socket closes. */
|
|
34
|
-
sendMcpRequest(jsonRpcPayload: unknown): Promise<unknown>;
|
|
35
|
-
/** Forward a JSON-RPC notification as an mcp.notification frame. Fire-
|
|
36
|
-
* and-forget — notifications have no response in JSON-RPC. */
|
|
37
|
-
sendMcpNotification(jsonRpcPayload: unknown): void;
|
|
38
|
-
/** Register a callback invoked when the IPC connection drops. The
|
|
39
|
-
* `reason` argument distinguishes a primary-closing broadcast from a
|
|
40
|
-
* plain remote close so callers can decide to re-elect vs exit. */
|
|
41
|
-
onClose(cb: (reason: 'remote' | 'local' | 'primary-closing') => void): void;
|
|
42
|
-
/** Register a callback for unsolicited notifications from the primary
|
|
43
|
-
* (e.g. tools/list_changed in the future). */
|
|
44
|
-
onNotification(cb: (payload: unknown) => void): void;
|
|
45
|
-
/** Close the IPC connection. */
|
|
46
|
-
disconnect(): Promise<void>;
|
|
47
|
-
private ingest;
|
|
48
|
-
private handleFrame;
|
|
49
|
-
private onSocketClose;
|
|
50
|
-
private notifyClose;
|
|
51
|
-
}
|
|
2
|
+
import { IpcClient } from './ipc-client.js';
|
|
52
3
|
export interface ProxyOptions {
|
|
53
4
|
input?: NodeJS.ReadableStream;
|
|
54
5
|
output?: NodeJS.WritableStream;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { IpcClient } from './ipc-client.js';
|
|
3
|
+
import { readToken } from './token.js';
|
|
4
|
+
/**
|
|
5
|
+
* Proxy wrapper around IpcClient. Pipes MCP frames between the parent's
|
|
6
|
+
* stdin/stdout and the IPC bridge to the primary browser-link process.
|
|
7
|
+
*
|
|
8
|
+
* runProxy() owns the line buffer on the input side, the JSON-RPC framing,
|
|
9
|
+
* the optional auto-reelect loop (survives IPC drops by reconnecting to a
|
|
10
|
+
* fresh primary), and the stop()/closed handle the caller waits on.
|
|
11
|
+
*/
|
|
12
|
+
function log(msg) {
|
|
13
|
+
console.error(`[browser-link proxy] ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
/** Read the token from disk and connect to the running primary, then plug
|
|
16
|
+
* stdin → IPC mcp.request and IPC mcp.response → stdout.
|
|
17
|
+
*
|
|
18
|
+
* When `autoReelect: true` is passed, the proxy survives IPC drops by
|
|
19
|
+
* waiting for a fresh primary to appear at the same port. During the
|
|
20
|
+
* wait, incoming JSON-RPC requests get an immediate error response so
|
|
21
|
+
* the MCP client never hangs on a missing reply. */
|
|
22
|
+
export async function runProxy(opts = {}) {
|
|
23
|
+
const input = opts.input ?? process.stdin;
|
|
24
|
+
const output = opts.output ?? process.stdout;
|
|
25
|
+
const autoReelect = opts.autoReelect === true;
|
|
26
|
+
const reelectTimeoutMs = opts.reelectTimeoutMs ?? 5000;
|
|
27
|
+
const reelectIntervalMs = opts.reelectIntervalMs ?? 200;
|
|
28
|
+
const initialToken = opts.token ?? readToken();
|
|
29
|
+
if (!initialToken) {
|
|
30
|
+
throw new Error('Multi-agent token not found. The primary browser-link instance is not running with multi-agent enabled.');
|
|
31
|
+
}
|
|
32
|
+
let client = new IpcClient();
|
|
33
|
+
await client.connect(initialToken, { host: opts.host, port: opts.port });
|
|
34
|
+
/* When the IPC drops AND autoReelect is on, we enter "reconnecting"
|
|
35
|
+
* mode. While reconnecting, the data pipe stays attached but requests
|
|
36
|
+
* are answered immediately with an error envelope so the MCP client
|
|
37
|
+
* does not stall on a missing reply.
|
|
38
|
+
*
|
|
39
|
+
* The flags live behind a getter pair on purpose: any read pattern
|
|
40
|
+
* TS can statically narrow (let-binding, object literal property)
|
|
41
|
+
* gets refined after the first `if (flag) return` and stays narrowed
|
|
42
|
+
* across the subsequent `await reconnectLoop(...)`, even though
|
|
43
|
+
* `stop()` can flip the value concurrently. TS does NOT narrow the
|
|
44
|
+
* return type of a function call, which is exactly the safety net
|
|
45
|
+
* we need for the post-await re-check. */
|
|
46
|
+
const flags = { stopped: false, reconnecting: false };
|
|
47
|
+
const isStopped = () => flags.stopped;
|
|
48
|
+
const isReconnecting = () => flags.reconnecting;
|
|
49
|
+
let closedResolve = () => {
|
|
50
|
+
/* replaced synchronously in the Promise executor below */
|
|
51
|
+
};
|
|
52
|
+
const closed = new Promise((resolve) => {
|
|
53
|
+
closedResolve = resolve;
|
|
54
|
+
});
|
|
55
|
+
const fail = (reason) => {
|
|
56
|
+
if (isStopped())
|
|
57
|
+
return;
|
|
58
|
+
flags.stopped = true;
|
|
59
|
+
input.off('data', onData);
|
|
60
|
+
if (opts.onClose) {
|
|
61
|
+
try {
|
|
62
|
+
opts.onClose(reason);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
log(`onClose handler threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
closedResolve();
|
|
69
|
+
};
|
|
70
|
+
// Set up the input pipe. We can't use readline.createInterface on a raw
|
|
71
|
+
// stream that has already been data-event-attached, but we can use a
|
|
72
|
+
// simple line buffer like the server side.
|
|
73
|
+
let lineBuffer = '';
|
|
74
|
+
const onData = (chunk) => {
|
|
75
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
76
|
+
lineBuffer += text;
|
|
77
|
+
let nl;
|
|
78
|
+
while ((nl = lineBuffer.indexOf('\n')) >= 0) {
|
|
79
|
+
const line = lineBuffer.slice(0, nl).replace(/\r$/, '');
|
|
80
|
+
lineBuffer = lineBuffer.slice(nl + 1);
|
|
81
|
+
if (line.length === 0)
|
|
82
|
+
continue;
|
|
83
|
+
handleLine(line);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const handleLine = (line) => {
|
|
87
|
+
let msg;
|
|
88
|
+
try {
|
|
89
|
+
msg = JSON.parse(line);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
output.write(JSON.stringify({
|
|
93
|
+
jsonrpc: '2.0',
|
|
94
|
+
id: null,
|
|
95
|
+
error: { code: -32700, message: 'Parse error in proxy input.' },
|
|
96
|
+
}) + '\n');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (typeof msg !== 'object')
|
|
100
|
+
return;
|
|
101
|
+
if (msg.id === undefined || msg.id === null) {
|
|
102
|
+
// Notification — fire and forget when connected; drop during reconnect.
|
|
103
|
+
if (!isReconnecting())
|
|
104
|
+
client.sendMcpNotification(msg);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (isReconnecting()) {
|
|
108
|
+
// Fail fast so the MCP client can decide to retry.
|
|
109
|
+
output.write(JSON.stringify({
|
|
110
|
+
jsonrpc: '2.0',
|
|
111
|
+
id: msg.id,
|
|
112
|
+
error: {
|
|
113
|
+
code: -32001,
|
|
114
|
+
message: 'browser-link bridge temporarily unavailable (primary just closed; reconnecting).',
|
|
115
|
+
},
|
|
116
|
+
}) + '\n');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Capture the id NOW; by the time the catch fires the closure scope
|
|
120
|
+
// still has `msg`, but TS narrowing of `msg` inside the catch is
|
|
121
|
+
// weaker than reading the field upfront.
|
|
122
|
+
const incomingId = msg.id;
|
|
123
|
+
client
|
|
124
|
+
.sendMcpRequest(msg)
|
|
125
|
+
.then((responsePayload) => {
|
|
126
|
+
output.write(JSON.stringify(responsePayload) + '\n');
|
|
127
|
+
})
|
|
128
|
+
.catch((err) => {
|
|
129
|
+
output.write(JSON.stringify({
|
|
130
|
+
jsonrpc: '2.0',
|
|
131
|
+
id: incomingId,
|
|
132
|
+
error: { code: -32000, message: err instanceof Error ? err.message : String(err) },
|
|
133
|
+
}) + '\n');
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
input.on('data', onData);
|
|
137
|
+
const wireCloseHandler = (c) => {
|
|
138
|
+
// The onClose contract is sync (returns void). The body needs awaits
|
|
139
|
+
// for the reconnect loop, so we kick off an async IIFE inside.
|
|
140
|
+
c.onClose((reason) => {
|
|
141
|
+
void (async () => {
|
|
142
|
+
if (isStopped())
|
|
143
|
+
return;
|
|
144
|
+
if (!autoReelect) {
|
|
145
|
+
fail(reason);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Enter reconnect mode and try to find a new primary.
|
|
149
|
+
flags.reconnecting = true;
|
|
150
|
+
opts.onReelectStart?.();
|
|
151
|
+
log(`Primary closed (${reason}); reelect window opened for ${reelectTimeoutMs}ms.`);
|
|
152
|
+
const newClient = await reconnectLoop(opts.host, opts.port, reelectTimeoutMs, reelectIntervalMs);
|
|
153
|
+
// `stop()` may have run during the await above and flipped
|
|
154
|
+
// `flags.stopped`. The `isStopped()` call dodges TS narrowing
|
|
155
|
+
// from the check at the top of this IIFE so the runtime guard
|
|
156
|
+
// stays real.
|
|
157
|
+
if (isStopped()) {
|
|
158
|
+
if (newClient)
|
|
159
|
+
await newClient.disconnect();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!newClient) {
|
|
163
|
+
log('Reelect window exhausted; closing proxy.');
|
|
164
|
+
opts.onReelectExhausted?.();
|
|
165
|
+
flags.reconnecting = false;
|
|
166
|
+
fail(reason);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Hot-swap clients. The old one is already torn down by its close
|
|
170
|
+
// handler chain; we just install the new one and wire it up.
|
|
171
|
+
log('Reconnected to new primary.');
|
|
172
|
+
opts.onReelectSuccess?.();
|
|
173
|
+
client = newClient;
|
|
174
|
+
flags.reconnecting = false;
|
|
175
|
+
wireCloseHandler(client);
|
|
176
|
+
})();
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
wireCloseHandler(client);
|
|
180
|
+
const stop = async () => {
|
|
181
|
+
flags.stopped = true;
|
|
182
|
+
input.off('data', onData);
|
|
183
|
+
await client.disconnect();
|
|
184
|
+
closedResolve();
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
get client() {
|
|
188
|
+
return client;
|
|
189
|
+
},
|
|
190
|
+
stop,
|
|
191
|
+
closed,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Repeatedly try to connect to the IPC port until success or budget
|
|
195
|
+
* expires. Re-reads the token on every attempt because a new primary
|
|
196
|
+
* rotates it on startup. Returns the connected client, or null on
|
|
197
|
+
* timeout. */
|
|
198
|
+
async function reconnectLoop(host, port, totalBudgetMs, intervalMs) {
|
|
199
|
+
const deadline = Date.now() + totalBudgetMs;
|
|
200
|
+
// Generous handshake budget — Windows under load can take >200ms for a
|
|
201
|
+
// local socket round-trip and a too-tight ceiling makes reconnects flap.
|
|
202
|
+
const handshakeTimeoutMs = Math.min(1500, Math.max(800, intervalMs * 4));
|
|
203
|
+
while (Date.now() < deadline) {
|
|
204
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
205
|
+
const token = readToken();
|
|
206
|
+
if (!token)
|
|
207
|
+
continue;
|
|
208
|
+
const c = new IpcClient();
|
|
209
|
+
try {
|
|
210
|
+
await c.connect(token, { host, port, handshakeTimeoutMs });
|
|
211
|
+
return c;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
try {
|
|
215
|
+
await c.disconnect();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
/* ignore */
|
|
219
|
+
}
|
|
220
|
+
// Retry until deadline.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
/** Convenience: open a readline-style line iterator over a stream. Currently
|
|
226
|
+
* unused (we use a hand-rolled line buffer for symmetry with the server)
|
|
227
|
+
* but exported for future tooling. */
|
|
228
|
+
export function readlines(stream) {
|
|
229
|
+
return createInterface({ input: stream, crlfDelay: Infinity });
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bridge/proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
+
import type { BridgeEventLog } from './events.js';
|
|
3
|
+
export declare const WS_HOST = "127.0.0.1";
|
|
4
|
+
export declare const WS_PORT = 17529;
|
|
5
|
+
export interface TabSession {
|
|
6
|
+
tabId: string;
|
|
7
|
+
url: string;
|
|
8
|
+
title: string;
|
|
9
|
+
ws: WebSocket;
|
|
10
|
+
}
|
|
11
|
+
export interface PendingRequest {
|
|
12
|
+
resolve: (result: unknown) => void;
|
|
13
|
+
reject: (err: Error) => void;
|
|
14
|
+
timeout: NodeJS.Timeout;
|
|
15
|
+
}
|
|
16
|
+
export declare function isAddrInUse(err: unknown): boolean;
|
|
17
|
+
/** Bring up the WebSocket bridge for the Chrome extension. Resolves only
|
|
18
|
+
* after the server is listening, so the caller can fail fast (and refuse
|
|
19
|
+
* to expose the MCP transport) when the port is taken or any other bind
|
|
20
|
+
* error happens. */
|
|
21
|
+
export declare function startWsBridge(tabs: Map<string, TabSession>, pendingRequests: Map<string, PendingRequest>, events: BridgeEventLog): Promise<WebSocketServer>;
|
|
22
|
+
/** Build the callback that sends a tool.request frame to a specific tab and
|
|
23
|
+
* resolves with the matching tool.response (or rejects on timeout). Kept here
|
|
24
|
+
* because it touches the same `tabs` / `pendingRequests` maps the bridge
|
|
25
|
+
* owns. */
|
|
26
|
+
export declare function sendToolRequest(tabs: Map<string, TabSession>, pendingRequests: Map<string, PendingRequest>, tabId: string, id: string, tool: string, params: unknown, timeoutMs: number): Promise<unknown>;
|