@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.
Files changed (72) hide show
  1. package/README.md +6 -4
  2. package/package.json +53 -53
  3. package/packages/agent-client/src/cli-helpers.js +43 -5
  4. package/packages/agent-client/src/cli.js +176 -171
  5. package/packages/agent-client/src/client.js +66 -21
  6. package/packages/agent-client/src/command-registry.js +104 -69
  7. package/packages/agent-client/src/detect.js +162 -54
  8. package/packages/agent-client/src/install.js +34 -28
  9. package/packages/agent-client/src/mcp-config.js +40 -40
  10. package/packages/agent-client/src/runtime.js +41 -20
  11. package/packages/agent-client/src/setup-status.js +23 -30
  12. package/packages/mcp-server/src/bin.js +57 -5
  13. package/packages/mcp-server/src/handlers.js +573 -256
  14. package/packages/mcp-server/src/server.js +568 -257
  15. package/packages/native-host/bin/bridge-daemon.js +39 -6
  16. package/packages/native-host/bin/install-manifest.js +26 -4
  17. package/packages/native-host/bin/postinstall.js +4 -2
  18. package/packages/native-host/src/config.js +142 -13
  19. package/packages/native-host/src/daemon-process.js +396 -0
  20. package/packages/native-host/src/daemon.js +350 -150
  21. package/packages/native-host/src/framing.js +131 -11
  22. package/packages/native-host/src/install-manifest.js +194 -29
  23. package/packages/native-host/src/native-host.js +154 -102
  24. package/packages/protocol/src/budget.js +3 -7
  25. package/packages/protocol/src/capabilities.js +6 -3
  26. package/packages/protocol/src/defaults.js +1 -0
  27. package/packages/protocol/src/errors.js +15 -11
  28. package/packages/protocol/src/payload-cost.js +19 -6
  29. package/packages/protocol/src/protocol.js +242 -73
  30. package/packages/protocol/src/registry.js +311 -45
  31. package/packages/protocol/src/summary.js +260 -109
  32. package/packages/protocol/src/types.js +29 -4
  33. package/skills/browser-bridge/SKILL.md +3 -2
  34. package/skills/browser-bridge/agents/openai.yaml +3 -3
  35. package/skills/browser-bridge/references/interaction.md +34 -11
  36. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  37. package/skills/browser-bridge/references/protocol.md +127 -71
  38. package/skills/browser-bridge/references/tailwind.md +12 -11
  39. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  40. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  41. package/CHANGELOG.md +0 -55
  42. package/assets/banner.jpg +0 -0
  43. package/assets/logo.png +0 -0
  44. package/assets/logo.svg +0 -65
  45. package/docs/api-reference.md +0 -157
  46. package/docs/cli-guide.md +0 -128
  47. package/docs/index.md +0 -25
  48. package/docs/manual-setup.md +0 -140
  49. package/docs/mcp-vs-cli.md +0 -258
  50. package/docs/publishing.md +0 -114
  51. package/docs/quickstart.md +0 -104
  52. package/docs/troubleshooting.md +0 -59
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -52
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -459
  60. package/packages/extension/src/background-routing.js +0 -91
  61. package/packages/extension/src/background.js +0 -3227
  62. package/packages/extension/src/content-script-helpers.js +0 -281
  63. package/packages/extension/src/content-script.js +0 -1977
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -102
  66. package/packages/extension/ui/offscreen.html +0 -6
  67. package/packages/extension/ui/offscreen.js +0 -61
  68. package/packages/extension/ui/popup.html +0 -35
  69. package/packages/extension/ui/popup.js +0 -279
  70. package/packages/extension/ui/sidepanel.html +0 -102
  71. package/packages/extension/ui/sidepanel.js +0 -1854
  72. 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 { installAgentFiles, isSupportedTarget, removeAgentFiles } from '../../agent-client/src/install.js';
9
- import { installMcpConfig, isMcpClientName, removeMcpConfig } from '../../agent-client/src/mcp-config.js';
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 { getSocketPath } from './config.js';
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({ socketPath = getSocketPath(), listenOptions = null, setupStatusLoader = collectSetupStatus, setupInstaller = installSetupTarget, logger = console } = {}) {
112
- this.socketPath = socketPath;
113
- this.listenOptions = listenOptions;
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 = typeof message.browserName === 'string' ? message.browserName : undefined;
144
- socket.__profileLabel = typeof message.profileLabel === 'string' ? message.profileLabel : undefined;
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, { type: 'registered', role: 'agent', clientId });
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.listenOptions) {
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.socketPath)) {
172
- throw new Error(`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`);
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?.('[daemon] handler error:', err instanceof Error ? err.message : String(err));
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
- if (this.listenOptions) {
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 (!this.listenOptions && process.platform !== 'win32') {
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.listenOptions) {
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: { code: ERROR_CODES.INVALID_REQUEST, message: 'Unknown message type.' }
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(request.id, await this.setupInstaller(request.params ?? {}), {
361
- method: request.method
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 = typeof request.meta?.target_browser === 'string' ? request.meta.target_browser : null;
378
- const targetProfile = typeof request.meta?.target_profile === 'string' ? request.meta.target_profile : null;
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
- let targets = Array.from(this.extensionSockets.values());
382
- if (targetBrowser || targetProfile) {
383
- targets = targets.filter((extSocket) => {
384
- if (targetBrowser && extSocket.__browserName !== targetBrowser) return false;
385
- if (targetProfile && extSocket.__profileLabel !== targetProfile) return false;
386
- return true;
387
- });
388
- } else {
389
- const enabled = targets.filter((extSocket) => extSocket.__accessEnabled);
390
- if (enabled.length > 0) {
391
- targets = enabled;
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.pendingRequests.set(request.id, {
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.pendingRequests.delete(request.id);
421
- const response = createFailure(request.id, ERROR_CODES.TIMEOUT, 'Extension did not respond in time.');
422
- void writeJsonLine(pending.socket, { type: 'agent.response', response });
423
- }, this.pendingTimeoutMs)
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
- socket.__accessEnabled = Boolean(message.accessEnabled);
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
- pending.targets.delete(socket);
695
+ this.removePendingTarget(responseMessage.id, pending, socket);
508
696
 
509
697
  if (responseMessage.ok) {
510
698
  clearTimeout(pending.timeoutId);
511
- this.pendingRequests.delete(responseMessage.id);
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 = pending.method === 'health.ping'
520
- ? createSuccess(responseMessage.id, {
521
- daemon: 'ok',
522
- extensionConnected: true,
523
- socketPath: this.socketPath,
524
- connectedExtensions: Array.from(this.extensionSockets.entries()).map(
525
- ([_id, extSocket]) => ({
526
- extensionId: _id,
527
- browserName: extSocket.__browserName ?? null,
528
- profileLabel: extSocket.__profileLabel ?? null,
529
- accessEnabled: extSocket.__accessEnabled ?? false
530
- })
531
- ),
532
- .../** @type {Record<string, unknown>} */ (responseMessage.result)
533
- }, {
534
- ...responseMessage.meta,
535
- method: responseMessage.meta?.method ?? pending.method
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
- for (const [id, pending] of this.pendingRequests.entries()) {
566
- if (pending.socket === socket) {
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.pendingRequests.delete(id);
569
- continue;
761
+ this.clearPendingRequest(id, pending);
570
762
  }
571
- if (pending.targets.has(socket)) {
572
- pending.targets.delete(socket);
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.pendingRequests.delete(requestId);
792
+ this.clearPendingRequest(requestId, pending);
593
793
 
594
- const response = pending.lastErrorResponse ?? createFailure(
595
- requestId,
596
- ERROR_CODES.EXTENSION_DISCONNECTED,
597
- 'Target extension disconnected before responding.',
598
- null,
599
- { method: pending.method }
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
- * @param {ClientSocket[]} sockets
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} socketPath
834
+ * @param {BridgeTransport | string} transport
650
835
  * @returns {Promise<boolean>}
651
836
  */
652
- async function pingExistingDaemon(socketPath) {
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 = net.createConnection(socketPath);
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(`${JSON.stringify({
683
- type: 'agent.request',
684
- request: {
685
- id: 'ping_probe',
686
- method: 'health.ping',
687
- tab_id: null,
688
- params: {},
689
- meta: { protocol_version: PROTOCOL_VERSION, token_budget: null }
690
- }
691
- })}\n`);
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 = normalized.action === 'uninstall'
738
- ? await resolvedDeps.removeMcpConfig(normalized.target, { global: true })
739
- : [await resolvedDeps.installMcpConfig(normalized.target, { global: true })];
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 = normalized.action === 'uninstall'
753
- ? await resolvedDeps.removeAgentFiles({
754
- targets: [normalized.target],
755
- projectPath: resolvedDeps.cwd,
756
- global: true
757
- })
758
- : await resolvedDeps.installAgentFiles({
759
- targets: [normalized.target],
760
- projectPath: resolvedDeps.cwd,
761
- global: true
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
  }