@browserbridge/bbx 1.6.3 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.6.3",
3
+ "version": "1.7.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -9,11 +9,13 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  import {
11
11
  applyWindowsTcpTransportDefaults,
12
+ createTcpBridgeTransport,
12
13
  getProxyConfigPath,
13
14
  readProxyConfig,
14
15
  SUPPORTED_BROWSERS,
15
16
  } from '../../native-host/src/config.js';
16
17
  import { writeBridgeAuthToken } from '../../native-host/src/auth-token.js';
18
+ import { pingExistingDaemon } from '../../native-host/src/daemon.js';
17
19
  import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
18
20
  import { uninstallNativeManifest } from '../../native-host/src/install-manifest.js';
19
21
  import {
@@ -23,7 +25,6 @@ import {
23
25
  summarizeBatchResponseItem,
24
26
  } from '../../protocol/src/index.js';
25
27
  import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
26
- import { BridgeClient } from './client.js';
27
28
  import { CLI_HELP_SECTIONS, SHORTCUT_COMMANDS } from './command-registry.js';
28
29
  import {
29
30
  interactiveCheckbox,
@@ -59,6 +60,7 @@ import {
59
60
  parseRemoteEndpoint,
60
61
  readRemoteConfig,
61
62
  removeRemoteDestination,
63
+ resolveProxyEnableSettings,
62
64
  } from './remotes.js';
63
65
  import { collectSetupStatus } from './setup-status.js';
64
66
  import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
@@ -69,6 +71,32 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
69
71
  const REQUEST_SOURCE = 'cli';
70
72
  const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
71
73
  const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
74
+ const REMOTE_ENV = 'BBX_REMOTE';
75
+
76
+ /**
77
+ * Commands that only make sense against the local machine (setup, daemon
78
+ * lifecycle, and config management). The explicit --remote flag is rejected
79
+ * for these; the ambient BBX_REMOTE env var is ignored.
80
+ *
81
+ * @type {ReadonlySet<string>}
82
+ */
83
+ const LOCAL_ONLY_COMMANDS = new Set([
84
+ 'help',
85
+ '--help',
86
+ '-h',
87
+ '--version',
88
+ '-v',
89
+ 'skill',
90
+ 'install',
91
+ 'uninstall',
92
+ 'install-skill',
93
+ 'install-mcp',
94
+ 'mcp',
95
+ 'proxy',
96
+ 'remote',
97
+ 'doctor',
98
+ 'restart',
99
+ ]);
72
100
 
73
101
  /**
74
102
  * Read all of stdin as UTF-8 text. Resolves once stdin closes.
@@ -89,7 +117,18 @@ function readStdin() {
89
117
  });
90
118
  }
91
119
 
92
- const [, , command, ...rest] = process.argv;
120
+ const remoteFlag = extractRemoteFlag(process.argv.slice(2));
121
+ const [command, ...rest] = remoteFlag.rest;
122
+
123
+ if (remoteFlag.explicit && command && LOCAL_ONLY_COMMANDS.has(command)) {
124
+ process.stderr.write(`The --remote flag is not supported with "${command}".\n`);
125
+ process.exit(1);
126
+ }
127
+
128
+ const remoteDestinationId =
129
+ command && LOCAL_ONLY_COMMANDS.has(command)
130
+ ? null
131
+ : (remoteFlag.remoteId ?? process.env[REMOTE_ENV] ?? null);
93
132
 
94
133
  if (!command || ['help', '--help', '-h'].includes(command)) {
95
134
  printUsage();
@@ -396,23 +435,60 @@ if (command === 'mcp') {
396
435
  }
397
436
 
398
437
  if (command === 'proxy') {
399
- await handleProxyCommand(rest);
400
- process.exit(0);
438
+ await runLocalCommand(() => handleProxyCommand(rest));
439
+ process.exit();
401
440
  }
402
441
 
403
442
  if (command === 'remote') {
404
- await handleRemoteCommand(rest);
405
- process.exit(0);
443
+ await runLocalCommand(() => handleRemoteCommand(rest));
444
+ process.exit();
406
445
  }
407
446
 
408
447
  const clientTimeoutMs = getClientTimeoutOverride();
409
448
  applyWindowsTcpTransportDefaults();
410
- const client = new BridgeClient(
411
- clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : undefined
412
- );
449
+ const client = await createCliClient();
413
450
 
414
451
  await main();
415
452
 
453
+ /**
454
+ * Run a local (pre-main) command, converting thrown errors into a friendly
455
+ * one-line message instead of an unhandled stack trace.
456
+ *
457
+ * @param {() => Promise<void>} run
458
+ * @returns {Promise<void>}
459
+ */
460
+ async function runLocalCommand(run) {
461
+ try {
462
+ await run();
463
+ } catch (error) {
464
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
465
+ process.exit(1);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Create the bridge client for this invocation, honoring the --remote flag
471
+ * and BBX_REMOTE env var. Exits with a friendly error when the destination
472
+ * is not configured.
473
+ *
474
+ * @returns {Promise<import('./client.js').BridgeClient>}
475
+ */
476
+ async function createCliClient() {
477
+ try {
478
+ return await createBridgeClientForDestination(
479
+ remoteDestinationId,
480
+ clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : {}
481
+ );
482
+ } catch (error) {
483
+ printJson({
484
+ ok: false,
485
+ summary: `ERROR: ${error instanceof Error ? error.message : String(error)}`,
486
+ evidence: null,
487
+ });
488
+ process.exit(1);
489
+ }
490
+ }
491
+
416
492
  async function main() {
417
493
  try {
418
494
  if (command === 'status') {
@@ -859,7 +935,12 @@ async function handleProxyCommand(args) {
859
935
  const [subcommand, ...restArgs] = args;
860
936
  if (subcommand === 'enable') {
861
937
  const options = parseProxyEnableArgs(restArgs);
862
- const token = options.token ?? randomUUID();
938
+ const existing = readProxyConfig();
939
+ const { port, bindHost, token, tokenSource } = resolveProxyEnableSettings(
940
+ existing,
941
+ options,
942
+ randomUUID
943
+ );
863
944
  await writeBridgeAuthToken(token);
864
945
  const configPath = getProxyConfigPath();
865
946
  await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
@@ -868,8 +949,8 @@ async function handleProxyCommand(args) {
868
949
  `${JSON.stringify(
869
950
  {
870
951
  enabled: true,
871
- port: options.port,
872
- bindHost: options.bindHost,
952
+ port,
953
+ bindHost,
873
954
  token,
874
955
  },
875
956
  null,
@@ -878,14 +959,22 @@ async function handleProxyCommand(args) {
878
959
  { encoding: 'utf8', mode: 0o600 }
879
960
  );
880
961
  const result = await restartBridgeDaemon();
881
- const exampleHost = getProxyExampleHost(options.bindHost);
882
- process.stdout.write(
883
- [
884
- `Browser Bridge proxy enabled on ${options.bindHost}:${options.port}.`,
885
- '',
886
- `Token: ${token}`,
887
- `Config: ${configPath}`,
888
- `Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
962
+ const exampleHost = getProxyExampleHost(bindHost);
963
+ const tokenNote =
964
+ tokenSource === 'existing'
965
+ ? ' (unchanged — already-configured clients keep working; pass --rotate-token to generate a new secret)'
966
+ : tokenSource === 'generated' && existing
967
+ ? ' (rotated — update every configured client with `bbx remote add`)'
968
+ : '';
969
+ const lines = [
970
+ `Browser Bridge proxy enabled on ${bindHost}:${port}.`,
971
+ '',
972
+ `Token: ${token}${tokenNote}`,
973
+ `Config: ${configPath}`,
974
+ `Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
975
+ ];
976
+ if (tokenSource !== 'existing') {
977
+ lines.push(
889
978
  '',
890
979
  'On your dev machine, add this remote:',
891
980
  '',
@@ -893,10 +982,11 @@ async function handleProxyCommand(args) {
893
982
  '',
894
983
  'Example:',
895
984
  '',
896
- `bbx remote add remote-bbx ${exampleHost}:${options.port} --token ${token}`,
897
- '',
898
- ].join('\n')
899
- );
985
+ `bbx remote add remote-bbx ${exampleHost}:${port} --token ${token}`
986
+ );
987
+ }
988
+ lines.push('');
989
+ process.stdout.write(lines.join('\n'));
900
990
  return;
901
991
  }
902
992
 
@@ -916,10 +1006,12 @@ async function handleProxyCommand(args) {
916
1006
  process.stdout.write('Browser Bridge proxy is disabled.\n');
917
1007
  return;
918
1008
  }
1009
+ const reachable = await pingExistingDaemon(createTcpBridgeTransport(config.port, '127.0.0.1'));
919
1010
  process.stdout.write(
920
1011
  [
921
1012
  `Browser Bridge proxy is enabled on ${config.bindHost}:${config.port}.`,
922
1013
  `Config: ${getProxyConfigPath()}`,
1014
+ `Daemon: ${reachable ? 'reachable' : 'not reachable'} on 127.0.0.1:${config.port}`,
923
1015
  '',
924
1016
  ].join('\n')
925
1017
  );
@@ -930,12 +1022,15 @@ async function handleProxyCommand(args) {
930
1022
  }
931
1023
 
932
1024
  /**
1025
+ * Parse `bbx proxy enable` flags. Values stay undefined when not passed so an
1026
+ * existing proxy config can be reused instead of silently reset to defaults.
1027
+ *
933
1028
  * @param {string[]} args
934
- * @returns {{ port: number, bindHost: string, token?: string }}
1029
+ * @returns {import('./remotes.js').ProxyEnableOptions}
935
1030
  */
936
1031
  function parseProxyEnableArgs(args) {
937
- /** @type {{ port: number, bindHost: string, token?: string }} */
938
- const options = { port: 9223, bindHost: '0.0.0.0' };
1032
+ /** @type {import('./remotes.js').ProxyEnableOptions} */
1033
+ const options = {};
939
1034
  for (let index = 0; index < args.length; index += 1) {
940
1035
  const arg = args[index];
941
1036
  if (arg === '--port') {
@@ -956,9 +1051,16 @@ function parseProxyEnableArgs(args) {
956
1051
  }
957
1052
  continue;
958
1053
  }
1054
+ if (arg === '--rotate-token') {
1055
+ options.rotateToken = true;
1056
+ continue;
1057
+ }
959
1058
  throw new Error(`Unknown proxy enable option "${arg}".`);
960
1059
  }
961
- if (options.port < 1 || options.port > 65535) {
1060
+ if (options.token && options.rotateToken) {
1061
+ throw new Error('Use either --token or --rotate-token, not both.');
1062
+ }
1063
+ if (options.port !== undefined && (options.port < 1 || options.port > 65535)) {
962
1064
  throw new Error('port must be an integer between 1 and 65535.');
963
1065
  }
964
1066
  return options;
@@ -1049,10 +1151,23 @@ async function handleRemoteCommand(args) {
1049
1151
  try {
1050
1152
  await remoteClient.connect();
1051
1153
  const response = await remoteClient.request({ method: 'health.ping' });
1154
+ if (response.ok) {
1155
+ const health = /** @type {{ extensionConnected?: boolean }} */ (response.result);
1156
+ process.stdout.write(
1157
+ `Remote destination "${name}" is reachable (extension ${
1158
+ health.extensionConnected === true ? 'connected' : 'not connected'
1159
+ }).\n`
1160
+ );
1161
+ } else {
1162
+ process.exitCode = 1;
1163
+ process.stdout.write(`Remote destination "${name}" failed: ${response.error.message}\n`);
1164
+ }
1165
+ } catch (error) {
1166
+ process.exitCode = 1;
1052
1167
  process.stdout.write(
1053
- response.ok
1054
- ? `Remote destination "${name}" is reachable.\n`
1055
- : `Remote destination "${name}" failed: ${response.error.message}\n`
1168
+ `Remote destination "${name}" is not reachable: ${
1169
+ error instanceof Error ? error.message : String(error)
1170
+ }\n`
1056
1171
  );
1057
1172
  } finally {
1058
1173
  await remoteClient.close();
@@ -1214,7 +1329,7 @@ function printCallResponse(response) {
1214
1329
  }
1215
1330
 
1216
1331
  function printUsage() {
1217
- const blocks = ['Usage: bbx <command> [args]'];
1332
+ const blocks = ['Usage: bbx [--remote <name>] <command> [args]'];
1218
1333
  for (const section of CLI_HELP_SECTIONS) {
1219
1334
  blocks.push('', `${section.title}:`);
1220
1335
  blocks.push(...section.lines.map((line) => ` ${line}`));
@@ -1297,6 +1412,28 @@ async function parseCallCommand(args) {
1297
1412
  throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
1298
1413
  }
1299
1414
 
1415
+ /**
1416
+ * Extract the global --remote flag from anywhere in the argument list.
1417
+ * Exits with a usage error when the flag is present without a value.
1418
+ *
1419
+ * @param {string[]} args
1420
+ * @returns {{ remoteId: string | null, explicit: boolean, rest: string[] }}
1421
+ */
1422
+ function extractRemoteFlag(args) {
1423
+ const rest = [...args];
1424
+ const index = rest.indexOf('--remote');
1425
+ if (index === -1) {
1426
+ return { remoteId: null, explicit: false, rest };
1427
+ }
1428
+ const remoteId = rest[index + 1];
1429
+ if (!remoteId || remoteId.startsWith('--')) {
1430
+ process.stderr.write('--remote requires a destination name (see `bbx remote list`).\n');
1431
+ process.exit(1);
1432
+ }
1433
+ rest.splice(index, 2);
1434
+ return { remoteId, explicit: true, rest };
1435
+ }
1436
+
1300
1437
  /**
1301
1438
  * @param {string[]} args
1302
1439
  * @returns {{ tabId: number | null, rest: string[] }}
@@ -257,10 +257,13 @@ export const CLI_HELP_SECTIONS = Object.freeze([
257
257
  'bbx status Check bridge connection',
258
258
  'bbx doctor Diagnose install, daemon, extension, and access readiness',
259
259
  'bbx restart Restart the local Browser Bridge daemon',
260
- 'bbx proxy enable [--port 9223] [--bind-host 0.0.0.0] Enable opt-in TCP proxy mode and print a token',
260
+ 'bbx proxy enable [--port 9223] [--bind-host 0.0.0.0] [--rotate-token] Enable opt-in TCP proxy mode (idempotent: re-running keeps settings and token)',
261
261
  'bbx proxy disable Disable proxy mode and restart the daemon',
262
+ 'bbx proxy status Show proxy config and daemon reachability',
262
263
  'bbx remote add <name> <host:port> --token <token> Add a remote Browser Bridge destination (example: remote-bbx 192.168.56.20:9223)',
263
264
  'bbx remote list List remote destinations',
265
+ 'bbx remote test <name> Ping a remote destination',
266
+ 'bbx remote remove <name> Remove a remote destination',
264
267
  'bbx access-request Request Browser Bridge access for the focused window',
265
268
  'bbx logs Recent bridge logs',
266
269
  'bbx tabs List available tabs',
@@ -277,6 +280,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
277
280
  'bbx call [--tab <tabId>] <method> [paramsJson|-] Call any bridge method (- reads JSON from stdin)',
278
281
  'bbx <method> [--tab <tabId>] [paramsJson|-] Direct alias for exact bridge methods such as page.get_state',
279
282
  "bbx batch '[{method,params,tabId?},...]' Parallel method calls",
283
+ 'Every bridge command accepts --remote <name> (or BBX_REMOTE env) to target a remote destination from `bbx remote list`.',
280
284
  'Advanced bridge params stay available through `bbx call`, even when shortcuts expose only the common case.',
281
285
  'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.',
282
286
  '',
@@ -179,6 +179,34 @@ export async function listBridgeDestinations() {
179
179
  ];
180
180
  }
181
181
 
182
+ /**
183
+ * @typedef {{ port?: number, bindHost?: string, token?: string, rotateToken?: boolean }} ProxyEnableOptions
184
+ */
185
+
186
+ /**
187
+ * Merge `bbx proxy enable` options with an existing proxy config so re-running
188
+ * the command is idempotent: the port, bind host, and shared secret are
189
+ * preserved unless explicitly overridden. The token only changes when passed
190
+ * via --token or regenerated via --rotate-token, so already-configured remote
191
+ * clients keep working across repeated enables.
192
+ *
193
+ * @param {{ port: number, bindHost: string, token?: string } | null} existing
194
+ * @param {ProxyEnableOptions} options
195
+ * @param {() => string} generateToken
196
+ * @returns {{ port: number, bindHost: string, token: string, tokenSource: 'explicit' | 'existing' | 'generated' }}
197
+ */
198
+ export function resolveProxyEnableSettings(existing, options, generateToken) {
199
+ const port = options.port ?? existing?.port ?? DEFAULT_REMOTE_PORT;
200
+ const bindHost = options.bindHost ?? existing?.bindHost ?? '0.0.0.0';
201
+ if (options.token) {
202
+ return { port, bindHost, token: options.token, tokenSource: 'explicit' };
203
+ }
204
+ if (!options.rotateToken && existing?.token) {
205
+ return { port, bindHost, token: existing.token, tokenSource: 'existing' };
206
+ }
207
+ return { port, bindHost, token: generateToken(), tokenSource: 'generated' };
208
+ }
209
+
182
210
  /**
183
211
  * @param {string | null | undefined} destinationId
184
212
  * @param {{ defaultTimeoutMs?: number }} [options={}]
@@ -132,6 +132,67 @@ export function summarizeToolError(error) {
132
132
  );
133
133
  }
134
134
 
135
+ /**
136
+ * @param {ToolResult} result
137
+ * @returns {boolean}
138
+ */
139
+ function shouldTryRemoteFallback(result) {
140
+ if (!result.isError && result.structuredContent.ok !== false) {
141
+ return false;
142
+ }
143
+ const text = result.content[0]?.text ?? '';
144
+ return /\b(ACCESS_DENIED|DAEMON_OFFLINE|EXTENSION_DISCONNECTED|NATIVE_HOST_UNAVAILABLE|CONNECTION_LOST)\b|\b(ENOENT|ECONNREFUSED|ETIMEDOUT)\b|ERROR: connect\b/u.test(
145
+ text
146
+ );
147
+ }
148
+
149
+ /**
150
+ * @param {ToolResult} result
151
+ * @param {string} destinationId
152
+ * @returns {ToolResult}
153
+ */
154
+ function annotateAutoDestination(result, destinationId) {
155
+ return {
156
+ ...result,
157
+ structuredContent: {
158
+ ...result.structuredContent,
159
+ destinationId,
160
+ autoSelectedDestinationId: destinationId,
161
+ },
162
+ };
163
+ }
164
+
165
+ /**
166
+ * @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
167
+ * @param {ToolResult} localResult
168
+ * @returns {Promise<ToolResult>}
169
+ */
170
+ async function tryRemoteFallback(callback, localResult) {
171
+ const remotes = (await listBridgeDestinations()).filter((destination) => !destination.local);
172
+ if (remotes.length === 0) {
173
+ return localResult;
174
+ }
175
+
176
+ let lastFallbackResult = localResult;
177
+ for (const remote of remotes) {
178
+ const client = await createBridgeClientForDestination(remote.id);
179
+ try {
180
+ await client.connect();
181
+ const result = annotateAutoDestination(await callback(client), remote.id);
182
+ lastFallbackResult = result;
183
+ if (!shouldTryRemoteFallback(result)) {
184
+ return result;
185
+ }
186
+ } catch (error) {
187
+ lastFallbackResult = annotateAutoDestination(summarizeToolError(error), remote.id);
188
+ } finally {
189
+ await client.close();
190
+ }
191
+ }
192
+
193
+ return lastFallbackResult;
194
+ }
195
+
135
196
  /**
136
197
  * @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
137
198
  * @param {{ destinationId?: string | null }} [options]
@@ -140,7 +201,8 @@ export function summarizeToolError(error) {
140
201
  export async function withToolClient(callback, options = {}) {
141
202
  try {
142
203
  if (!options.destinationId) {
143
- return await withBridgeClient(callback);
204
+ const result = await withBridgeClient(callback);
205
+ return shouldTryRemoteFallback(result) ? await tryRemoteFallback(callback, result) : result;
144
206
  }
145
207
  const client = await createBridgeClientForDestination(options.destinationId);
146
208
  await client.connect();
@@ -67,10 +67,31 @@ export {
67
67
  const HOME_DIR = os.homedir();
68
68
 
69
69
  /**
70
+ * @param {{ destinationId?: string }} [args]
70
71
  * @returns {Promise<ToolResult>}
71
72
  */
72
- export async function handleStatusTool() {
73
+ export async function handleStatusTool(args = {}) {
73
74
  try {
75
+ const requestedDestinationId =
76
+ typeof args.destinationId === 'string' ? args.destinationId : null;
77
+ if (requestedDestinationId && requestedDestinationId !== 'local') {
78
+ const destination = (await getBridgeDestinations()).find(
79
+ (entry) => entry.id === requestedDestinationId
80
+ );
81
+ const result = await callRemoteHealth(requestedDestinationId);
82
+ return createToolResult(
83
+ result.reachable
84
+ ? `Browser Bridge destination "${requestedDestinationId}" is reachable.`
85
+ : `Browser Bridge destination "${requestedDestinationId}" is not reachable.`,
86
+ {
87
+ ok: result.reachable === true,
88
+ destination: destination ?? { id: requestedDestinationId, local: false },
89
+ ...result,
90
+ },
91
+ result.reachable !== true
92
+ );
93
+ }
94
+
74
95
  const report = await getDoctorReport();
75
96
  const destinations = await Promise.all(
76
97
  (await getBridgeDestinations()).map(async (destination) => {
@@ -147,7 +168,7 @@ export async function handleSetupTool(args) {
147
168
  }
148
169
 
149
170
  /**
150
- * @param {{ limit?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
171
+ * @param {{ limit?: number, budgetPreset?: 'quick' | 'normal' | 'deep', destinationId?: string }} args
151
172
  * @returns {Promise<ToolResult>}
152
173
  */
153
174
  export async function handleLogTool(args) {
@@ -163,6 +184,7 @@ export async function handleLogTool(args) {
163
184
  },
164
185
  {
165
186
  tokenBudget: getToolTokenBudget(normalizedArgs),
187
+ destinationId: typeof args.destinationId === 'string' ? args.destinationId : null,
166
188
  }
167
189
  );
168
190
  }
@@ -149,6 +149,7 @@ export function createBridgeMcpServer() {
149
149
  .enum(['quick', 'normal', 'deep'])
150
150
  .optional()
151
151
  .describe(BUDGET_PRESET_DESCRIPTION),
152
+ destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
152
153
  },
153
154
  },
154
155
  handleLogTool
@@ -704,7 +705,9 @@ export function createBridgeMcpServer() {
704
705
  title: 'Request Browser Bridge Access',
705
706
  description:
706
707
  'Request window access for Browser Bridge. Surfaces an Enable prompt in the extension popup or side panel. Use once per window; if access is already pending, ask the user to enable that window instead of requesting again.',
707
- inputSchema: {},
708
+ inputSchema: {
709
+ destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
710
+ },
708
711
  },
709
712
  handleAccessTool
710
713
  );
@@ -638,6 +638,7 @@ export class BridgeDaemon {
638
638
  extensionConnected: false,
639
639
  socketPath: this.socketPath,
640
640
  transport: formatBridgeTransport(this.transport),
641
+ proxy: this.getProxyStatusPayload(),
641
642
  connectedExtensions: [],
642
643
  daemon_supported_versions: SUPPORTED_VERSIONS,
643
644
  ...getVersionNegotiationPayload(request.meta?.protocol_version),
@@ -878,6 +879,24 @@ export class BridgeDaemon {
878
879
  socket.__accessEnabled = accessEnabled;
879
880
  }
880
881
 
882
+ /**
883
+ * Structured remote-exposure status for health payloads. The proxy counts
884
+ * as enabled only when the daemon listens on a TCP address other than
885
+ * loopback, i.e. agents on other machines can reach it.
886
+ *
887
+ * @returns {{ enabled: boolean, endpoint: string | null }}
888
+ */
889
+ getProxyStatusPayload() {
890
+ if (this.transport.type !== 'tcp') {
891
+ return { enabled: false, endpoint: null };
892
+ }
893
+ const bindHost = this.transport.bindHost ?? this.transport.host;
894
+ const isLoopback = bindHost === '127.0.0.1' || bindHost === '::1' || bindHost === 'localhost';
895
+ return isLoopback
896
+ ? { enabled: false, endpoint: null }
897
+ : { enabled: true, endpoint: `${bindHost}:${this.transport.port}` };
898
+ }
899
+
881
900
  /**
882
901
  * @param {ClientSocket} socket
883
902
  * @param {DaemonMessage} message
@@ -930,6 +949,7 @@ export class BridgeDaemon {
930
949
  .../** @type {Record<string, unknown>} */ (responseMessage.result),
931
950
  daemonVersion: DAEMON_VERSION,
932
951
  daemon_supported_versions: SUPPORTED_VERSIONS,
952
+ proxy: this.getProxyStatusPayload(),
933
953
  },
934
954
  {
935
955
  ...responseMessage.meta,
@@ -34,6 +34,8 @@ bbx tab-activate <tabId> # bring a tab to the foreground
34
34
  bbx skill # live runtime presets + limits
35
35
  ```
36
36
 
37
+ Use the globally installed `bbx` command or `node packages/agent-client/src/cli.js ...` for live readiness checks from this repository. Avoid `rtk npm exec -- bbx status` as a health signal: that path can restart/replace the daemon and report `Extension: disconnected` before Chrome reconnects. If it happens, wait a few seconds and verify with `bbx status`.
38
+
37
39
  ### Inspect & Find
38
40
 
39
41
  ```bash
@@ -89,6 +91,19 @@ bbx screenshot <ref> [outPath] # capture partial element screenshot
89
91
  bbx call screenshot.capture_full_page '{}' # raw base64; avoid unless document context matters
90
92
  ```
91
93
 
94
+ ### Remote Destinations
95
+
96
+ Every bridge command accepts `--remote <name>` (or `BBX_REMOTE=<name>` env) to target a browser on another machine registered with `bbx remote add`:
97
+
98
+ ```bash
99
+ bbx remote list # configured remote destinations
100
+ bbx remote test <name> # ping a remote through the proxy
101
+ bbx tabs --remote <name> # any bridge command works with --remote
102
+ bbx dom-query "#app" --remote <name>
103
+ ```
104
+
105
+ In MCP mode, pass `destinationId` on any browser tool call instead; `browser_status` lists all destinations with reachability. Only use remotes the user has configured; local remains the default.
106
+
92
107
  ## Access Flow
93
108
 
94
109
  Browser Bridge access is window-scoped. The user turns it on once for the current browser window in the popup or side panel.