@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 +1 -1
- package/packages/agent-client/src/cli.js +169 -32
- package/packages/agent-client/src/command-registry.js +5 -1
- package/packages/agent-client/src/remotes.js +28 -0
- package/packages/mcp-server/src/handlers-utils.js +63 -1
- package/packages/mcp-server/src/handlers.js +24 -2
- package/packages/mcp-server/src/server.js +4 -1
- package/packages/native-host/src/daemon.js +20 -0
- package/skills/browser-bridge/SKILL.md +15 -0
package/package.json
CHANGED
|
@@ -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
|
|
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(
|
|
438
|
+
await runLocalCommand(() => handleProxyCommand(rest));
|
|
439
|
+
process.exit();
|
|
401
440
|
}
|
|
402
441
|
|
|
403
442
|
if (command === 'remote') {
|
|
404
|
-
await handleRemoteCommand(rest);
|
|
405
|
-
process.exit(
|
|
443
|
+
await runLocalCommand(() => handleRemoteCommand(rest));
|
|
444
|
+
process.exit();
|
|
406
445
|
}
|
|
407
446
|
|
|
408
447
|
const clientTimeoutMs = getClientTimeoutOverride();
|
|
409
448
|
applyWindowsTcpTransportDefaults();
|
|
410
|
-
const client =
|
|
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
|
|
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
|
|
872
|
-
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(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
''
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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}:${
|
|
897
|
-
|
|
898
|
-
|
|
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 {
|
|
1029
|
+
* @returns {import('./remotes.js').ProxyEnableOptions}
|
|
935
1030
|
*/
|
|
936
1031
|
function parseProxyEnableArgs(args) {
|
|
937
|
-
/** @type {
|
|
938
|
-
const options = {
|
|
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.
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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]
|
|
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
|
-
|
|
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.
|