@browserbridge/bbx 1.0.0 → 1.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/README.md +6 -4
- package/package.json +53 -53
- package/packages/agent-client/src/cli-helpers.js +43 -5
- package/packages/agent-client/src/cli.js +176 -171
- package/packages/agent-client/src/client.js +66 -21
- package/packages/agent-client/src/command-registry.js +104 -69
- package/packages/agent-client/src/detect.js +162 -54
- package/packages/agent-client/src/install.js +34 -28
- package/packages/agent-client/src/mcp-config.js +40 -40
- package/packages/agent-client/src/runtime.js +41 -20
- package/packages/agent-client/src/setup-status.js +23 -30
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +573 -256
- package/packages/mcp-server/src/server.js +568 -257
- package/packages/native-host/bin/bridge-daemon.js +39 -6
- package/packages/native-host/bin/install-manifest.js +26 -4
- package/packages/native-host/bin/postinstall.js +4 -2
- package/packages/native-host/src/config.js +142 -13
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +350 -150
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +194 -29
- package/packages/native-host/src/native-host.js +154 -102
- package/packages/protocol/src/budget.js +3 -7
- package/packages/protocol/src/capabilities.js +6 -3
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +15 -11
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +242 -73
- package/packages/protocol/src/registry.js +311 -45
- package/packages/protocol/src/summary.js +260 -109
- package/packages/protocol/src/types.js +29 -4
- package/skills/browser-bridge/SKILL.md +3 -2
- package/skills/browser-bridge/agents/openai.yaml +3 -3
- package/skills/browser-bridge/references/interaction.md +34 -11
- package/skills/browser-bridge/references/patch-workflow.md +3 -0
- package/skills/browser-bridge/references/protocol.md +127 -71
- package/skills/browser-bridge/references/tailwind.md +12 -11
- package/skills/browser-bridge/references/token-efficiency.md +23 -22
- package/skills/browser-bridge/references/ui-workflows.md +8 -0
- package/CHANGELOG.md +0 -55
- package/assets/banner.jpg +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -65
- package/docs/api-reference.md +0 -157
- package/docs/cli-guide.md +0 -128
- package/docs/index.md +0 -25
- package/docs/manual-setup.md +0 -140
- package/docs/mcp-vs-cli.md +0 -258
- package/docs/publishing.md +0 -114
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -52
- package/packages/extension/assets/icon-128.png +0 -0
- package/packages/extension/assets/icon-16.png +0 -0
- package/packages/extension/assets/icon-32.png +0 -0
- package/packages/extension/assets/icon-48.png +0 -0
- package/packages/extension/src/background-helpers.js +0 -459
- package/packages/extension/src/background-routing.js +0 -91
- package/packages/extension/src/background.js +0 -3227
- package/packages/extension/src/content-script-helpers.js +0 -281
- package/packages/extension/src/content-script.js +0 -1977
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -102
- package/packages/extension/ui/offscreen.html +0 -6
- package/packages/extension/ui/offscreen.js +0 -61
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -279
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1854
- package/packages/extension/ui/ui.css +0 -1159
|
@@ -5,8 +5,16 @@ import net from 'node:net';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
installAgentFiles,
|
|
10
|
+
isSupportedTarget,
|
|
11
|
+
removeAgentFiles,
|
|
12
|
+
} from '../../agent-client/src/install.js';
|
|
13
|
+
import {
|
|
14
|
+
installMcpConfig,
|
|
15
|
+
isMcpClientName,
|
|
16
|
+
removeMcpConfig,
|
|
17
|
+
} from '../../agent-client/src/mcp-config.js';
|
|
10
18
|
import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
|
|
11
19
|
import {
|
|
12
20
|
createFailure,
|
|
@@ -19,15 +27,22 @@ import {
|
|
|
19
27
|
parseJsonLines,
|
|
20
28
|
PROTOCOL_VERSION,
|
|
21
29
|
SUPPORTED_VERSIONS,
|
|
22
|
-
validateBridgeRequest
|
|
30
|
+
validateBridgeRequest,
|
|
23
31
|
} from '../../protocol/src/index.js';
|
|
24
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
createSocketBridgeTransport,
|
|
34
|
+
formatBridgeTransport,
|
|
35
|
+
getBridgeListenTarget,
|
|
36
|
+
getBridgeTransport,
|
|
37
|
+
getSocketPath,
|
|
38
|
+
} from './config.js';
|
|
25
39
|
import { writeJsonLine } from './framing.js';
|
|
26
40
|
|
|
27
41
|
/** @typedef {import('../../protocol/src/types.js').BridgeRequest} BridgeRequest */
|
|
28
42
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallParams} SetupInstallParams */
|
|
29
43
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallResult} SetupInstallResult */
|
|
30
44
|
/** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
|
|
45
|
+
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
31
46
|
/** @typedef {import('node:net').Socket & { __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
32
47
|
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
33
48
|
/**
|
|
@@ -76,10 +91,18 @@ function getVersionNegotiationPayload(requestedVersion) {
|
|
|
76
91
|
...(localIsNewer ? { deprecated_since: latestSupported } : {}),
|
|
77
92
|
migration_hint: localIsNewer
|
|
78
93
|
? `Browser Bridge daemon is newer than the client protocol ${requestedVersion}. Restart or update the Browser Bridge CLI/npm package to ${latestSupported} or later.`
|
|
79
|
-
: `Browser Bridge daemon is older than the client protocol ${requestedVersion}. Restart or update the Browser Bridge CLI so the daemon supports ${requestedVersion}
|
|
94
|
+
: `Browser Bridge daemon is older than the client protocol ${requestedVersion}. Restart or update the Browser Bridge CLI so the daemon supports ${requestedVersion}.`,
|
|
80
95
|
};
|
|
81
96
|
}
|
|
82
97
|
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} socketPath
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
function isWindowsNamedPipePath(socketPath) {
|
|
103
|
+
return socketPath.startsWith('\\\\.\\pipe\\');
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
/**
|
|
84
107
|
* @typedef {{
|
|
85
108
|
* type?: string,
|
|
@@ -101,6 +124,7 @@ function getVersionNegotiationPayload(requestedVersion) {
|
|
|
101
124
|
export class BridgeDaemon {
|
|
102
125
|
/**
|
|
103
126
|
* @param {{
|
|
127
|
+
* transport?: BridgeTransport,
|
|
104
128
|
* socketPath?: string,
|
|
105
129
|
* listenOptions?: import('node:net').ListenOptions | null,
|
|
106
130
|
* setupStatusLoader?: () => Promise<SetupStatus>,
|
|
@@ -108,9 +132,18 @@ export class BridgeDaemon {
|
|
|
108
132
|
* logger?: Pick<Console, 'log' | 'error'>
|
|
109
133
|
* }} [options={}]
|
|
110
134
|
*/
|
|
111
|
-
constructor({
|
|
112
|
-
|
|
113
|
-
|
|
135
|
+
constructor({
|
|
136
|
+
transport = getBridgeTransport(),
|
|
137
|
+
socketPath = undefined,
|
|
138
|
+
listenOptions = null,
|
|
139
|
+
setupStatusLoader = collectSetupStatus,
|
|
140
|
+
setupInstaller = installSetupTarget,
|
|
141
|
+
logger = console,
|
|
142
|
+
} = {}) {
|
|
143
|
+
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
144
|
+
this.socketPath =
|
|
145
|
+
this.transport.type === 'socket' ? this.transport.socketPath : getSocketPath();
|
|
146
|
+
this.listenOptions = listenOptions ?? getBridgeListenTarget(this.transport);
|
|
114
147
|
this.setupStatusLoader = setupStatusLoader;
|
|
115
148
|
this.setupInstaller = setupInstaller;
|
|
116
149
|
this.logger = logger;
|
|
@@ -124,13 +157,120 @@ export class BridgeDaemon {
|
|
|
124
157
|
this.agentSockets = new Map();
|
|
125
158
|
/** @type {Map<string, PendingEntry>} */
|
|
126
159
|
this.pendingRequests = new Map();
|
|
160
|
+
/** @type {Map<ClientSocket, Set<string>>} */
|
|
161
|
+
this.pendingRequestsByOwnerSocket = new Map();
|
|
162
|
+
/** @type {Map<ClientSocket, Set<string>>} */
|
|
163
|
+
this.pendingRequestsByTargetSocket = new Map();
|
|
127
164
|
this.pendingTimeoutMs = DEFAULT_DAEMON_PENDING_TIMEOUT_MS;
|
|
128
165
|
/** @type {Record<string, unknown>[]} */
|
|
129
166
|
this.recentLog = [];
|
|
167
|
+
/** @type {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }> | null} */
|
|
168
|
+
this.connectedExtensionsCache = null;
|
|
130
169
|
/** @type {Promise<void> | null} */
|
|
131
170
|
this.stopPromise = null;
|
|
132
171
|
}
|
|
133
172
|
|
|
173
|
+
/**
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
176
|
+
invalidateConnectedExtensionsCache() {
|
|
177
|
+
this.connectedExtensionsCache = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {Map<ClientSocket, Set<string>>} index
|
|
182
|
+
* @param {ClientSocket} socket
|
|
183
|
+
* @param {string} requestId
|
|
184
|
+
* @returns {void}
|
|
185
|
+
*/
|
|
186
|
+
addPendingRequestIndex(index, socket, requestId) {
|
|
187
|
+
const requestIds = index.get(socket);
|
|
188
|
+
if (requestIds) {
|
|
189
|
+
requestIds.add(requestId);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
index.set(socket, new Set([requestId]));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {Map<ClientSocket, Set<string>>} index
|
|
197
|
+
* @param {ClientSocket} socket
|
|
198
|
+
* @param {string} requestId
|
|
199
|
+
* @returns {void}
|
|
200
|
+
*/
|
|
201
|
+
removePendingRequestIndex(index, socket, requestId) {
|
|
202
|
+
const requestIds = index.get(socket);
|
|
203
|
+
if (!requestIds) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
requestIds.delete(requestId);
|
|
207
|
+
if (requestIds.size === 0) {
|
|
208
|
+
index.delete(socket);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} requestId
|
|
214
|
+
* @param {PendingEntry} pending
|
|
215
|
+
* @returns {void}
|
|
216
|
+
*/
|
|
217
|
+
trackPendingRequest(requestId, pending) {
|
|
218
|
+
this.pendingRequests.set(requestId, pending);
|
|
219
|
+
this.addPendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
220
|
+
for (const targetSocket of pending.targets) {
|
|
221
|
+
this.addPendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {string} requestId
|
|
227
|
+
* @param {PendingEntry | undefined} [pending]
|
|
228
|
+
* @returns {PendingEntry | undefined}
|
|
229
|
+
*/
|
|
230
|
+
clearPendingRequest(requestId, pending = this.pendingRequests.get(requestId)) {
|
|
231
|
+
if (!pending) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
this.pendingRequests.delete(requestId);
|
|
235
|
+
this.removePendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
236
|
+
for (const targetSocket of pending.targets) {
|
|
237
|
+
this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
238
|
+
}
|
|
239
|
+
return pending;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {string} requestId
|
|
244
|
+
* @param {PendingEntry} pending
|
|
245
|
+
* @param {ClientSocket} targetSocket
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*/
|
|
248
|
+
removePendingTarget(requestId, pending, targetSocket) {
|
|
249
|
+
if (!pending.targets.delete(targetSocket)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @returns {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }>}
|
|
257
|
+
*/
|
|
258
|
+
getConnectedExtensionsSnapshot() {
|
|
259
|
+
if (this.connectedExtensionsCache) {
|
|
260
|
+
return this.connectedExtensionsCache;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.connectedExtensionsCache = Array.from(this.extensionSockets.entries()).map(
|
|
264
|
+
([extensionId, extSocket]) => ({
|
|
265
|
+
extensionId,
|
|
266
|
+
browserName: extSocket.__browserName ?? null,
|
|
267
|
+
profileLabel: extSocket.__profileLabel ?? null,
|
|
268
|
+
accessEnabled: extSocket.__accessEnabled ?? false,
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
return this.connectedExtensionsCache;
|
|
272
|
+
}
|
|
273
|
+
|
|
134
274
|
/**
|
|
135
275
|
* @param {ClientSocket} socket
|
|
136
276
|
* @param {DaemonMessage} message
|
|
@@ -140,10 +280,13 @@ export class BridgeDaemon {
|
|
|
140
280
|
if (message.role === 'extension') {
|
|
141
281
|
const extensionId = randomUUID();
|
|
142
282
|
socket.__extensionId = extensionId;
|
|
143
|
-
socket.__browserName =
|
|
144
|
-
|
|
283
|
+
socket.__browserName =
|
|
284
|
+
typeof message.browserName === 'string' ? message.browserName : undefined;
|
|
285
|
+
socket.__profileLabel =
|
|
286
|
+
typeof message.profileLabel === 'string' ? message.profileLabel : undefined;
|
|
145
287
|
socket.__lastActiveAt = Date.now();
|
|
146
288
|
this.extensionSockets.set(extensionId, socket);
|
|
289
|
+
this.invalidateConnectedExtensionsCache();
|
|
147
290
|
void writeJsonLine(socket, { type: 'registered', role: 'extension' });
|
|
148
291
|
return;
|
|
149
292
|
}
|
|
@@ -152,7 +295,11 @@ export class BridgeDaemon {
|
|
|
152
295
|
const clientId = message.clientId || randomUUID();
|
|
153
296
|
this.agentSockets.set(clientId, socket);
|
|
154
297
|
socket.__clientId = clientId;
|
|
155
|
-
void writeJsonLine(socket, {
|
|
298
|
+
void writeJsonLine(socket, {
|
|
299
|
+
type: 'registered',
|
|
300
|
+
role: 'agent',
|
|
301
|
+
clientId,
|
|
302
|
+
});
|
|
156
303
|
return;
|
|
157
304
|
}
|
|
158
305
|
}
|
|
@@ -160,7 +307,7 @@ export class BridgeDaemon {
|
|
|
160
307
|
* @returns {Promise<BridgeDaemon>}
|
|
161
308
|
*/
|
|
162
309
|
async start() {
|
|
163
|
-
if (!this.
|
|
310
|
+
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
164
311
|
const socketDir = path.dirname(this.socketPath);
|
|
165
312
|
await fs.promises.mkdir(socketDir, { recursive: true });
|
|
166
313
|
if (process.platform !== 'win32') {
|
|
@@ -168,8 +315,10 @@ export class BridgeDaemon {
|
|
|
168
315
|
}
|
|
169
316
|
try {
|
|
170
317
|
await fs.promises.access(this.socketPath);
|
|
171
|
-
if (await pingExistingDaemon(this.
|
|
172
|
-
throw new Error(
|
|
318
|
+
if (await pingExistingDaemon(this.transport)) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
321
|
+
);
|
|
173
322
|
}
|
|
174
323
|
this.logger.log('[daemon] Removing stale socket from previous run:', this.socketPath);
|
|
175
324
|
} catch (error) {
|
|
@@ -189,7 +338,10 @@ export class BridgeDaemon {
|
|
|
189
338
|
parseJsonLines(typedSocket, (raw) => {
|
|
190
339
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
191
340
|
void this.handleClientMessage(typedSocket, message).catch((err) => {
|
|
192
|
-
this.logger.error?.(
|
|
341
|
+
this.logger.error?.(
|
|
342
|
+
'[daemon] handler error:',
|
|
343
|
+
err instanceof Error ? err.message : String(err)
|
|
344
|
+
);
|
|
193
345
|
});
|
|
194
346
|
});
|
|
195
347
|
typedSocket.on('close', () => this.handleSocketClose(typedSocket));
|
|
@@ -203,14 +355,10 @@ export class BridgeDaemon {
|
|
|
203
355
|
this.serverAddress = server.address();
|
|
204
356
|
resolve(undefined);
|
|
205
357
|
};
|
|
206
|
-
|
|
207
|
-
server.listen(this.listenOptions, onListen);
|
|
208
|
-
} else {
|
|
209
|
-
server.listen(this.socketPath, onListen);
|
|
210
|
-
}
|
|
358
|
+
server.listen(this.listenOptions, onListen);
|
|
211
359
|
});
|
|
212
360
|
|
|
213
|
-
if (
|
|
361
|
+
if (this.transport.type === 'socket' && process.platform !== 'win32') {
|
|
214
362
|
await fs.promises.chmod(this.socketPath, 0o600);
|
|
215
363
|
}
|
|
216
364
|
|
|
@@ -241,6 +389,8 @@ export class BridgeDaemon {
|
|
|
241
389
|
clearTimeout(pending.timeoutId);
|
|
242
390
|
}
|
|
243
391
|
this.pendingRequests.clear();
|
|
392
|
+
this.pendingRequestsByOwnerSocket.clear();
|
|
393
|
+
this.pendingRequestsByTargetSocket.clear();
|
|
244
394
|
|
|
245
395
|
for (const socket of this.agentSockets.values()) {
|
|
246
396
|
socket.destroy();
|
|
@@ -251,6 +401,7 @@ export class BridgeDaemon {
|
|
|
251
401
|
socket.destroy();
|
|
252
402
|
}
|
|
253
403
|
this.extensionSockets.clear();
|
|
404
|
+
this.invalidateConnectedExtensionsCache();
|
|
254
405
|
|
|
255
406
|
if (this.server) {
|
|
256
407
|
const server = this.server;
|
|
@@ -266,7 +417,7 @@ export class BridgeDaemon {
|
|
|
266
417
|
});
|
|
267
418
|
});
|
|
268
419
|
} finally {
|
|
269
|
-
if (!this.
|
|
420
|
+
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
270
421
|
await fs.promises.rm(this.socketPath, { force: true });
|
|
271
422
|
}
|
|
272
423
|
}
|
|
@@ -314,7 +465,10 @@ export class BridgeDaemon {
|
|
|
314
465
|
|
|
315
466
|
await writeJsonLine(socket, {
|
|
316
467
|
type: 'error',
|
|
317
|
-
error: {
|
|
468
|
+
error: {
|
|
469
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
470
|
+
message: 'Unknown message type.',
|
|
471
|
+
},
|
|
318
472
|
});
|
|
319
473
|
}
|
|
320
474
|
|
|
@@ -331,8 +485,9 @@ export class BridgeDaemon {
|
|
|
331
485
|
daemon: 'ok',
|
|
332
486
|
extensionConnected: false,
|
|
333
487
|
socketPath: this.socketPath,
|
|
488
|
+
transport: formatBridgeTransport(this.transport),
|
|
334
489
|
connectedExtensions: [],
|
|
335
|
-
...getVersionNegotiationPayload(request.meta?.protocol_version)
|
|
490
|
+
...getVersionNegotiationPayload(request.meta?.protocol_version),
|
|
336
491
|
});
|
|
337
492
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
338
493
|
return;
|
|
@@ -341,7 +496,7 @@ export class BridgeDaemon {
|
|
|
341
496
|
|
|
342
497
|
if (request.method === 'log.tail') {
|
|
343
498
|
const response = createSuccess(request.id, {
|
|
344
|
-
entries: this.recentLog.slice(-DEFAULT_LOG_TAIL_LIMIT)
|
|
499
|
+
entries: this.recentLog.slice(-DEFAULT_LOG_TAIL_LIMIT),
|
|
345
500
|
});
|
|
346
501
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
347
502
|
return;
|
|
@@ -349,7 +504,7 @@ export class BridgeDaemon {
|
|
|
349
504
|
|
|
350
505
|
if (request.method === 'setup.get_status') {
|
|
351
506
|
const response = createSuccess(request.id, await this.setupStatusLoader(), {
|
|
352
|
-
method: request.method
|
|
507
|
+
method: request.method,
|
|
353
508
|
});
|
|
354
509
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
355
510
|
return;
|
|
@@ -357,9 +512,13 @@ export class BridgeDaemon {
|
|
|
357
512
|
|
|
358
513
|
if (request.method === 'setup.install') {
|
|
359
514
|
try {
|
|
360
|
-
const response = createSuccess(
|
|
361
|
-
|
|
362
|
-
|
|
515
|
+
const response = createSuccess(
|
|
516
|
+
request.id,
|
|
517
|
+
await this.setupInstaller(request.params ?? {}),
|
|
518
|
+
{
|
|
519
|
+
method: request.method,
|
|
520
|
+
}
|
|
521
|
+
);
|
|
363
522
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
364
523
|
} catch (error) {
|
|
365
524
|
const response = createFailure(
|
|
@@ -374,27 +533,43 @@ export class BridgeDaemon {
|
|
|
374
533
|
return;
|
|
375
534
|
}
|
|
376
535
|
|
|
377
|
-
const targetBrowser =
|
|
378
|
-
|
|
536
|
+
const targetBrowser =
|
|
537
|
+
typeof request.meta?.target_browser === 'string' ? request.meta.target_browser : null;
|
|
538
|
+
const targetProfile =
|
|
539
|
+
typeof request.meta?.target_profile === 'string' ? request.meta.target_profile : null;
|
|
379
540
|
const hasExplicitTarget = Boolean(targetBrowser || targetProfile);
|
|
380
541
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
} else {
|
|
393
|
-
const mostRecent = selectMostRecentlyActiveExtension(targets);
|
|
394
|
-
if (mostRecent) {
|
|
395
|
-
targets = [mostRecent];
|
|
542
|
+
/** @type {ClientSocket[]} */
|
|
543
|
+
const targets = [];
|
|
544
|
+
/** @type {ClientSocket | null} */
|
|
545
|
+
let mostRecent = null;
|
|
546
|
+
for (const extSocket of this.extensionSockets.values()) {
|
|
547
|
+
if (targetBrowser || targetProfile) {
|
|
548
|
+
if (targetBrowser && extSocket.__browserName !== targetBrowser) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (targetProfile && extSocket.__profileLabel !== targetProfile) {
|
|
552
|
+
continue;
|
|
396
553
|
}
|
|
554
|
+
targets.push(extSocket);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (extSocket.__accessEnabled) {
|
|
559
|
+
targets.push(extSocket);
|
|
560
|
+
continue;
|
|
397
561
|
}
|
|
562
|
+
|
|
563
|
+
if (
|
|
564
|
+
!mostRecent ||
|
|
565
|
+
(typeof extSocket.__lastActiveAt === 'number' ? extSocket.__lastActiveAt : 0) >
|
|
566
|
+
(typeof mostRecent.__lastActiveAt === 'number' ? mostRecent.__lastActiveAt : 0)
|
|
567
|
+
) {
|
|
568
|
+
mostRecent = extSocket;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!hasExplicitTarget && targets.length === 0 && mostRecent) {
|
|
572
|
+
targets.push(mostRecent);
|
|
398
573
|
}
|
|
399
574
|
|
|
400
575
|
if (targets.length === 0) {
|
|
@@ -409,7 +584,7 @@ export class BridgeDaemon {
|
|
|
409
584
|
return;
|
|
410
585
|
}
|
|
411
586
|
|
|
412
|
-
this.
|
|
587
|
+
this.trackPendingRequest(request.id, {
|
|
413
588
|
socket,
|
|
414
589
|
method: request.method,
|
|
415
590
|
source: typeof request.meta?.source === 'string' ? request.meta.source : '',
|
|
@@ -417,17 +592,20 @@ export class BridgeDaemon {
|
|
|
417
592
|
timeoutId: setTimeout(() => {
|
|
418
593
|
const pending = this.pendingRequests.get(request.id);
|
|
419
594
|
if (!pending) return;
|
|
420
|
-
this.
|
|
421
|
-
const response = createFailure(
|
|
422
|
-
|
|
423
|
-
|
|
595
|
+
this.clearPendingRequest(request.id, pending);
|
|
596
|
+
const response = createFailure(
|
|
597
|
+
request.id,
|
|
598
|
+
ERROR_CODES.TIMEOUT,
|
|
599
|
+
'Extension did not respond in time.'
|
|
600
|
+
);
|
|
601
|
+
void writeJsonLine(pending.socket, {
|
|
602
|
+
type: 'agent.response',
|
|
603
|
+
response,
|
|
604
|
+
});
|
|
605
|
+
}, this.pendingTimeoutMs),
|
|
424
606
|
});
|
|
425
607
|
const broadcastPayload = { type: 'extension.request', request };
|
|
426
|
-
await Promise.all(
|
|
427
|
-
targets.map(
|
|
428
|
-
(extSocket) => writeJsonLine(extSocket, broadcastPayload)
|
|
429
|
-
)
|
|
430
|
-
);
|
|
608
|
+
await Promise.all(targets.map((extSocket) => writeJsonLine(extSocket, broadcastPayload)));
|
|
431
609
|
}
|
|
432
610
|
|
|
433
611
|
/**
|
|
@@ -440,15 +618,15 @@ export class BridgeDaemon {
|
|
|
440
618
|
await writeJsonLine(socket, {
|
|
441
619
|
type: 'extension.setup_status.response',
|
|
442
620
|
requestId: message.requestId,
|
|
443
|
-
status: await this.setupStatusLoader()
|
|
621
|
+
status: await this.setupStatusLoader(),
|
|
444
622
|
});
|
|
445
623
|
} catch (error) {
|
|
446
624
|
await writeJsonLine(socket, {
|
|
447
625
|
type: 'extension.setup_status.error',
|
|
448
626
|
requestId: message.requestId,
|
|
449
627
|
error: {
|
|
450
|
-
message: error instanceof Error ? error.message : String(error)
|
|
451
|
-
}
|
|
628
|
+
message: error instanceof Error ? error.message : String(error),
|
|
629
|
+
},
|
|
452
630
|
});
|
|
453
631
|
}
|
|
454
632
|
}
|
|
@@ -459,12 +637,18 @@ export class BridgeDaemon {
|
|
|
459
637
|
* @returns {void}
|
|
460
638
|
*/
|
|
461
639
|
handleExtensionIdentity(socket, message) {
|
|
640
|
+
let changed = false;
|
|
462
641
|
if (typeof message.browserName === 'string') {
|
|
642
|
+
changed = changed || socket.__browserName !== message.browserName;
|
|
463
643
|
socket.__browserName = message.browserName;
|
|
464
644
|
}
|
|
465
645
|
if (typeof message.profileLabel === 'string') {
|
|
646
|
+
changed = changed || socket.__profileLabel !== message.profileLabel;
|
|
466
647
|
socket.__profileLabel = message.profileLabel;
|
|
467
648
|
}
|
|
649
|
+
if (changed) {
|
|
650
|
+
this.invalidateConnectedExtensionsCache();
|
|
651
|
+
}
|
|
468
652
|
}
|
|
469
653
|
|
|
470
654
|
/**
|
|
@@ -473,7 +657,13 @@ export class BridgeDaemon {
|
|
|
473
657
|
* @returns {void}
|
|
474
658
|
*/
|
|
475
659
|
handleExtensionAccessUpdate(socket, message) {
|
|
476
|
-
|
|
660
|
+
const accessEnabled = Boolean(message.accessEnabled);
|
|
661
|
+
if (socket.__accessEnabled !== accessEnabled) {
|
|
662
|
+
socket.__accessEnabled = accessEnabled;
|
|
663
|
+
this.invalidateConnectedExtensionsCache();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
socket.__accessEnabled = accessEnabled;
|
|
477
667
|
}
|
|
478
668
|
|
|
479
669
|
/**
|
|
@@ -483,9 +673,7 @@ export class BridgeDaemon {
|
|
|
483
673
|
*/
|
|
484
674
|
handleExtensionActivity(socket, message) {
|
|
485
675
|
socket.__lastActiveAt =
|
|
486
|
-
typeof message.at === 'number' && Number.isFinite(message.at)
|
|
487
|
-
? message.at
|
|
488
|
-
: Date.now();
|
|
676
|
+
typeof message.at === 'number' && Number.isFinite(message.at) ? message.at : Date.now();
|
|
489
677
|
}
|
|
490
678
|
|
|
491
679
|
/**
|
|
@@ -504,41 +692,40 @@ export class BridgeDaemon {
|
|
|
504
692
|
return;
|
|
505
693
|
}
|
|
506
694
|
|
|
507
|
-
|
|
695
|
+
this.removePendingTarget(responseMessage.id, pending, socket);
|
|
508
696
|
|
|
509
697
|
if (responseMessage.ok) {
|
|
510
698
|
clearTimeout(pending.timeoutId);
|
|
511
|
-
this.
|
|
699
|
+
this.clearPendingRequest(responseMessage.id, pending);
|
|
512
700
|
this.pushLog({
|
|
513
701
|
at: new Date().toISOString(),
|
|
514
702
|
method: responseMessage.meta?.method ?? null,
|
|
515
703
|
ok: true,
|
|
516
704
|
id: responseMessage.id,
|
|
517
|
-
source: pending.source || null
|
|
705
|
+
source: pending.source || null,
|
|
518
706
|
});
|
|
519
|
-
const response =
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
: responseMessage;
|
|
707
|
+
const response =
|
|
708
|
+
pending.method === 'health.ping'
|
|
709
|
+
? createSuccess(
|
|
710
|
+
responseMessage.id,
|
|
711
|
+
{
|
|
712
|
+
daemon: 'ok',
|
|
713
|
+
extensionConnected: true,
|
|
714
|
+
socketPath: this.socketPath,
|
|
715
|
+
transport: formatBridgeTransport(this.transport),
|
|
716
|
+
connectedExtensions: this.getConnectedExtensionsSnapshot(),
|
|
717
|
+
.../** @type {Record<string, unknown>} */ (responseMessage.result),
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
...responseMessage.meta,
|
|
721
|
+
method: responseMessage.meta?.method ?? pending.method,
|
|
722
|
+
}
|
|
723
|
+
)
|
|
724
|
+
: responseMessage;
|
|
538
725
|
|
|
539
726
|
await writeJsonLine(pending.socket, {
|
|
540
727
|
type: 'agent.response',
|
|
541
|
-
response
|
|
728
|
+
response,
|
|
542
729
|
});
|
|
543
730
|
return;
|
|
544
731
|
}
|
|
@@ -556,20 +743,33 @@ export class BridgeDaemon {
|
|
|
556
743
|
handleSocketClose(socket) {
|
|
557
744
|
if (socket.__extensionId) {
|
|
558
745
|
this.extensionSockets.delete(socket.__extensionId);
|
|
746
|
+
this.invalidateConnectedExtensionsCache();
|
|
559
747
|
}
|
|
560
748
|
|
|
561
749
|
if (socket.__clientId) {
|
|
562
750
|
this.agentSockets.delete(socket.__clientId);
|
|
563
751
|
}
|
|
564
752
|
|
|
565
|
-
|
|
566
|
-
|
|
753
|
+
const ownedRequestIds = this.pendingRequestsByOwnerSocket.get(socket);
|
|
754
|
+
if (ownedRequestIds) {
|
|
755
|
+
for (const id of ownedRequestIds) {
|
|
756
|
+
const pending = this.pendingRequests.get(id);
|
|
757
|
+
if (!pending) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
567
760
|
clearTimeout(pending.timeoutId);
|
|
568
|
-
this.
|
|
569
|
-
continue;
|
|
761
|
+
this.clearPendingRequest(id, pending);
|
|
570
762
|
}
|
|
571
|
-
|
|
572
|
-
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const targetRequestIds = this.pendingRequestsByTargetSocket.get(socket);
|
|
766
|
+
if (targetRequestIds) {
|
|
767
|
+
for (const id of targetRequestIds) {
|
|
768
|
+
const pending = this.pendingRequests.get(id);
|
|
769
|
+
if (!pending) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
this.removePendingTarget(id, pending, socket);
|
|
573
773
|
void this.finishPendingRequestIfExhausted(id, pending);
|
|
574
774
|
}
|
|
575
775
|
}
|
|
@@ -589,27 +789,29 @@ export class BridgeDaemon {
|
|
|
589
789
|
}
|
|
590
790
|
|
|
591
791
|
clearTimeout(pending.timeoutId);
|
|
592
|
-
this.
|
|
792
|
+
this.clearPendingRequest(requestId, pending);
|
|
593
793
|
|
|
594
|
-
const response =
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
794
|
+
const response =
|
|
795
|
+
pending.lastErrorResponse ??
|
|
796
|
+
createFailure(
|
|
797
|
+
requestId,
|
|
798
|
+
ERROR_CODES.EXTENSION_DISCONNECTED,
|
|
799
|
+
'Target extension disconnected before responding.',
|
|
800
|
+
null,
|
|
801
|
+
{ method: pending.method }
|
|
802
|
+
);
|
|
601
803
|
|
|
602
804
|
this.pushLog({
|
|
603
805
|
at: new Date().toISOString(),
|
|
604
806
|
method: response.meta?.method ?? pending.method ?? null,
|
|
605
807
|
ok: false,
|
|
606
808
|
id: requestId,
|
|
607
|
-
source: pending.source || null
|
|
809
|
+
source: pending.source || null,
|
|
608
810
|
});
|
|
609
811
|
|
|
610
812
|
await writeJsonLine(pending.socket, {
|
|
611
813
|
type: 'agent.response',
|
|
612
|
-
response
|
|
814
|
+
response,
|
|
613
815
|
});
|
|
614
816
|
}
|
|
615
817
|
|
|
@@ -626,37 +828,25 @@ export class BridgeDaemon {
|
|
|
626
828
|
}
|
|
627
829
|
|
|
628
830
|
/**
|
|
629
|
-
*
|
|
630
|
-
* @returns {ClientSocket | null}
|
|
631
|
-
*/
|
|
632
|
-
function selectMostRecentlyActiveExtension(sockets) {
|
|
633
|
-
if (sockets.length === 0) {
|
|
634
|
-
return null;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return sockets.reduce((best, current) => {
|
|
638
|
-
const bestAt = typeof best.__lastActiveAt === 'number' ? best.__lastActiveAt : 0;
|
|
639
|
-
const currentAt =
|
|
640
|
-
typeof current.__lastActiveAt === 'number' ? current.__lastActiveAt : 0;
|
|
641
|
-
return currentAt > bestAt ? current : best;
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Check whether a daemon is already listening on the given socket path.
|
|
831
|
+
* Check whether a daemon is already listening on the given transport.
|
|
647
832
|
* Connects, sends a health.ping, and waits up to 500 ms for a response.
|
|
648
833
|
*
|
|
649
|
-
* @param {string}
|
|
834
|
+
* @param {BridgeTransport | string} transport
|
|
650
835
|
* @returns {Promise<boolean>}
|
|
651
836
|
*/
|
|
652
|
-
async function pingExistingDaemon(
|
|
837
|
+
export async function pingExistingDaemon(transport) {
|
|
838
|
+
const resolvedTransport =
|
|
839
|
+
typeof transport === 'string' ? createSocketBridgeTransport(transport) : transport;
|
|
653
840
|
return new Promise((resolve) => {
|
|
654
841
|
const timeout = setTimeout(() => {
|
|
655
842
|
socket.destroy();
|
|
656
843
|
resolve(false);
|
|
657
844
|
}, DAEMON_EXISTING_SOCKET_PING_TIMEOUT_MS);
|
|
658
845
|
|
|
659
|
-
const socket =
|
|
846
|
+
const socket =
|
|
847
|
+
resolvedTransport.type === 'tcp'
|
|
848
|
+
? net.createConnection({ host: resolvedTransport.host, port: resolvedTransport.port })
|
|
849
|
+
: net.createConnection(resolvedTransport.socketPath);
|
|
660
850
|
socket.once('error', () => {
|
|
661
851
|
clearTimeout(timeout);
|
|
662
852
|
resolve(false);
|
|
@@ -679,16 +869,18 @@ async function pingExistingDaemon(socketPath) {
|
|
|
679
869
|
});
|
|
680
870
|
|
|
681
871
|
socket.once('connect', () => {
|
|
682
|
-
socket.write(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
872
|
+
socket.write(
|
|
873
|
+
`${JSON.stringify({
|
|
874
|
+
type: 'agent.request',
|
|
875
|
+
request: {
|
|
876
|
+
id: 'ping_probe',
|
|
877
|
+
method: 'health.ping',
|
|
878
|
+
tab_id: null,
|
|
879
|
+
params: {},
|
|
880
|
+
meta: { protocol_version: PROTOCOL_VERSION, token_budget: null },
|
|
881
|
+
},
|
|
882
|
+
})}\n`
|
|
883
|
+
);
|
|
692
884
|
});
|
|
693
885
|
});
|
|
694
886
|
}
|
|
@@ -724,7 +916,7 @@ export async function installSetupTarget(
|
|
|
724
916
|
installMcpConfig,
|
|
725
917
|
isMcpClientName,
|
|
726
918
|
removeMcpConfig,
|
|
727
|
-
cwd: process.cwd()
|
|
919
|
+
cwd: process.cwd(),
|
|
728
920
|
}
|
|
729
921
|
) {
|
|
730
922
|
/** @type {SetupInstallDeps} */
|
|
@@ -734,14 +926,21 @@ export async function installSetupTarget(
|
|
|
734
926
|
if (!resolvedDeps.isMcpClientName(normalized.target)) {
|
|
735
927
|
throw new Error(`Unsupported MCP client "${normalized.target}".`);
|
|
736
928
|
}
|
|
737
|
-
const paths =
|
|
738
|
-
|
|
739
|
-
|
|
929
|
+
const paths =
|
|
930
|
+
normalized.action === 'uninstall'
|
|
931
|
+
? await resolvedDeps.removeMcpConfig(normalized.target, {
|
|
932
|
+
global: true,
|
|
933
|
+
})
|
|
934
|
+
: [
|
|
935
|
+
await resolvedDeps.installMcpConfig(normalized.target, {
|
|
936
|
+
global: true,
|
|
937
|
+
}),
|
|
938
|
+
];
|
|
740
939
|
return {
|
|
741
940
|
action: normalized.action,
|
|
742
941
|
kind: 'mcp',
|
|
743
942
|
target: normalized.target,
|
|
744
|
-
paths
|
|
943
|
+
paths,
|
|
745
944
|
};
|
|
746
945
|
}
|
|
747
946
|
|
|
@@ -749,21 +948,22 @@ export async function installSetupTarget(
|
|
|
749
948
|
throw new Error(`Unsupported skill target "${normalized.target}".`);
|
|
750
949
|
}
|
|
751
950
|
|
|
752
|
-
const paths =
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
951
|
+
const paths =
|
|
952
|
+
normalized.action === 'uninstall'
|
|
953
|
+
? await resolvedDeps.removeAgentFiles({
|
|
954
|
+
targets: [normalized.target],
|
|
955
|
+
projectPath: resolvedDeps.cwd,
|
|
956
|
+
global: true,
|
|
957
|
+
})
|
|
958
|
+
: await resolvedDeps.installAgentFiles({
|
|
959
|
+
targets: [normalized.target],
|
|
960
|
+
projectPath: resolvedDeps.cwd,
|
|
961
|
+
global: true,
|
|
962
|
+
});
|
|
763
963
|
return {
|
|
764
964
|
action: normalized.action,
|
|
765
965
|
kind: 'skill',
|
|
766
966
|
target: normalized.target,
|
|
767
|
-
paths
|
|
967
|
+
paths,
|
|
768
968
|
};
|
|
769
969
|
}
|