@browserbridge/bbx 1.6.4 → 1.7.1
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 +172 -34
- package/packages/agent-client/src/client.js +44 -7
- package/packages/agent-client/src/command-registry.js +5 -1
- package/packages/agent-client/src/remotes.js +28 -0
- package/packages/agent-client/src/runtime.js +230 -0
- package/packages/agent-client/src/types.ts +21 -0
- package/packages/mcp-server/src/handlers.js +2 -1
- package/packages/mcp-server/src/server.js +4 -1
- package/packages/native-host/bin/bridge-daemon.js +17 -2
- package/packages/native-host/bin/postinstall.js +18 -0
- package/packages/native-host/src/config.js +23 -0
- package/packages/native-host/src/daemon-process.js +138 -4
- package/packages/native-host/src/daemon.js +32 -9
- package/packages/native-host/src/native-host.js +1 -1
- package/packages/protocol/src/protocol.js +56 -7
- package/packages/protocol/src/summary.js +7 -2
- package/skills/browser-bridge/SKILL.md +15 -0
- package/skills/browser-bridge/references/protocol.md +1 -1
package/package.json
CHANGED
|
@@ -9,21 +9,23 @@ 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 {
|
|
20
22
|
createRuntimeContext,
|
|
23
|
+
getProtocolVersion,
|
|
21
24
|
METHODS,
|
|
22
25
|
summarizeBatchErrorItem,
|
|
23
26
|
summarizeBatchResponseItem,
|
|
24
27
|
} from '../../protocol/src/index.js';
|
|
25
28
|
import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
26
|
-
import { BridgeClient } from './client.js';
|
|
27
29
|
import { CLI_HELP_SECTIONS, SHORTCUT_COMMANDS } from './command-registry.js';
|
|
28
30
|
import {
|
|
29
31
|
interactiveCheckbox,
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
parseRemoteEndpoint,
|
|
60
62
|
readRemoteConfig,
|
|
61
63
|
removeRemoteDestination,
|
|
64
|
+
resolveProxyEnableSettings,
|
|
62
65
|
} from './remotes.js';
|
|
63
66
|
import { collectSetupStatus } from './setup-status.js';
|
|
64
67
|
import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
@@ -69,6 +72,32 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
|
69
72
|
const REQUEST_SOURCE = 'cli';
|
|
70
73
|
const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
|
|
71
74
|
const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
|
|
75
|
+
const REMOTE_ENV = 'BBX_REMOTE';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Commands that only make sense against the local machine (setup, daemon
|
|
79
|
+
* lifecycle, and config management). The explicit --remote flag is rejected
|
|
80
|
+
* for these; the ambient BBX_REMOTE env var is ignored.
|
|
81
|
+
*
|
|
82
|
+
* @type {ReadonlySet<string>}
|
|
83
|
+
*/
|
|
84
|
+
const LOCAL_ONLY_COMMANDS = new Set([
|
|
85
|
+
'help',
|
|
86
|
+
'--help',
|
|
87
|
+
'-h',
|
|
88
|
+
'--version',
|
|
89
|
+
'-v',
|
|
90
|
+
'skill',
|
|
91
|
+
'install',
|
|
92
|
+
'uninstall',
|
|
93
|
+
'install-skill',
|
|
94
|
+
'install-mcp',
|
|
95
|
+
'mcp',
|
|
96
|
+
'proxy',
|
|
97
|
+
'remote',
|
|
98
|
+
'doctor',
|
|
99
|
+
'restart',
|
|
100
|
+
]);
|
|
72
101
|
|
|
73
102
|
/**
|
|
74
103
|
* Read all of stdin as UTF-8 text. Resolves once stdin closes.
|
|
@@ -89,7 +118,18 @@ function readStdin() {
|
|
|
89
118
|
});
|
|
90
119
|
}
|
|
91
120
|
|
|
92
|
-
const
|
|
121
|
+
const remoteFlag = extractRemoteFlag(process.argv.slice(2));
|
|
122
|
+
const [command, ...rest] = remoteFlag.rest;
|
|
123
|
+
|
|
124
|
+
if (remoteFlag.explicit && command && LOCAL_ONLY_COMMANDS.has(command)) {
|
|
125
|
+
process.stderr.write(`The --remote flag is not supported with "${command}".\n`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const remoteDestinationId =
|
|
130
|
+
command && LOCAL_ONLY_COMMANDS.has(command)
|
|
131
|
+
? null
|
|
132
|
+
: (remoteFlag.remoteId ?? process.env[REMOTE_ENV] ?? null);
|
|
93
133
|
|
|
94
134
|
if (!command || ['help', '--help', '-h'].includes(command)) {
|
|
95
135
|
printUsage();
|
|
@@ -396,23 +436,60 @@ if (command === 'mcp') {
|
|
|
396
436
|
}
|
|
397
437
|
|
|
398
438
|
if (command === 'proxy') {
|
|
399
|
-
await handleProxyCommand(rest);
|
|
400
|
-
process.exit(
|
|
439
|
+
await runLocalCommand(() => handleProxyCommand(rest));
|
|
440
|
+
process.exit();
|
|
401
441
|
}
|
|
402
442
|
|
|
403
443
|
if (command === 'remote') {
|
|
404
|
-
await handleRemoteCommand(rest);
|
|
405
|
-
process.exit(
|
|
444
|
+
await runLocalCommand(() => handleRemoteCommand(rest));
|
|
445
|
+
process.exit();
|
|
406
446
|
}
|
|
407
447
|
|
|
408
448
|
const clientTimeoutMs = getClientTimeoutOverride();
|
|
409
449
|
applyWindowsTcpTransportDefaults();
|
|
410
|
-
const client =
|
|
411
|
-
clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : undefined
|
|
412
|
-
);
|
|
450
|
+
const client = await createCliClient();
|
|
413
451
|
|
|
414
452
|
await main();
|
|
415
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Run a local (pre-main) command, converting thrown errors into a friendly
|
|
456
|
+
* one-line message instead of an unhandled stack trace.
|
|
457
|
+
*
|
|
458
|
+
* @param {() => Promise<void>} run
|
|
459
|
+
* @returns {Promise<void>}
|
|
460
|
+
*/
|
|
461
|
+
async function runLocalCommand(run) {
|
|
462
|
+
try {
|
|
463
|
+
await run();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Create the bridge client for this invocation, honoring the --remote flag
|
|
472
|
+
* and BBX_REMOTE env var. Exits with a friendly error when the destination
|
|
473
|
+
* is not configured.
|
|
474
|
+
*
|
|
475
|
+
* @returns {Promise<import('./client.js').BridgeClient>}
|
|
476
|
+
*/
|
|
477
|
+
async function createCliClient() {
|
|
478
|
+
try {
|
|
479
|
+
return await createBridgeClientForDestination(
|
|
480
|
+
remoteDestinationId,
|
|
481
|
+
clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : {}
|
|
482
|
+
);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
printJson({
|
|
485
|
+
ok: false,
|
|
486
|
+
summary: `ERROR: ${error instanceof Error ? error.message : String(error)}`,
|
|
487
|
+
evidence: null,
|
|
488
|
+
});
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
416
493
|
async function main() {
|
|
417
494
|
try {
|
|
418
495
|
if (command === 'status') {
|
|
@@ -552,7 +629,7 @@ async function main() {
|
|
|
552
629
|
evidence: null,
|
|
553
630
|
durationMs: 0,
|
|
554
631
|
approxTokens: 0,
|
|
555
|
-
meta: { protocol_version:
|
|
632
|
+
meta: { protocol_version: getProtocolVersion() },
|
|
556
633
|
error: {
|
|
557
634
|
code: 'INVALID_REQUEST',
|
|
558
635
|
message: 'Each batch call needs a method.',
|
|
@@ -569,7 +646,7 @@ async function main() {
|
|
|
569
646
|
evidence: null,
|
|
570
647
|
durationMs: 0,
|
|
571
648
|
approxTokens: 0,
|
|
572
|
-
meta: { protocol_version:
|
|
649
|
+
meta: { protocol_version: getProtocolVersion() },
|
|
573
650
|
error: {
|
|
574
651
|
code: 'INVALID_REQUEST',
|
|
575
652
|
message: `Unknown bridge method "${call.method}".`,
|
|
@@ -859,7 +936,12 @@ async function handleProxyCommand(args) {
|
|
|
859
936
|
const [subcommand, ...restArgs] = args;
|
|
860
937
|
if (subcommand === 'enable') {
|
|
861
938
|
const options = parseProxyEnableArgs(restArgs);
|
|
862
|
-
const
|
|
939
|
+
const existing = readProxyConfig();
|
|
940
|
+
const { port, bindHost, token, tokenSource } = resolveProxyEnableSettings(
|
|
941
|
+
existing,
|
|
942
|
+
options,
|
|
943
|
+
randomUUID
|
|
944
|
+
);
|
|
863
945
|
await writeBridgeAuthToken(token);
|
|
864
946
|
const configPath = getProxyConfigPath();
|
|
865
947
|
await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
@@ -868,8 +950,8 @@ async function handleProxyCommand(args) {
|
|
|
868
950
|
`${JSON.stringify(
|
|
869
951
|
{
|
|
870
952
|
enabled: true,
|
|
871
|
-
port
|
|
872
|
-
bindHost
|
|
953
|
+
port,
|
|
954
|
+
bindHost,
|
|
873
955
|
token,
|
|
874
956
|
},
|
|
875
957
|
null,
|
|
@@ -878,14 +960,22 @@ async function handleProxyCommand(args) {
|
|
|
878
960
|
{ encoding: 'utf8', mode: 0o600 }
|
|
879
961
|
);
|
|
880
962
|
const result = await restartBridgeDaemon();
|
|
881
|
-
const exampleHost = getProxyExampleHost(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
''
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
963
|
+
const exampleHost = getProxyExampleHost(bindHost);
|
|
964
|
+
const tokenNote =
|
|
965
|
+
tokenSource === 'existing'
|
|
966
|
+
? ' (unchanged — already-configured clients keep working; pass --rotate-token to generate a new secret)'
|
|
967
|
+
: tokenSource === 'generated' && existing
|
|
968
|
+
? ' (rotated — update every configured client with `bbx remote add`)'
|
|
969
|
+
: '';
|
|
970
|
+
const lines = [
|
|
971
|
+
`Browser Bridge proxy enabled on ${bindHost}:${port}.`,
|
|
972
|
+
'',
|
|
973
|
+
`Token: ${token}${tokenNote}`,
|
|
974
|
+
`Config: ${configPath}`,
|
|
975
|
+
`Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
|
|
976
|
+
];
|
|
977
|
+
if (tokenSource !== 'existing') {
|
|
978
|
+
lines.push(
|
|
889
979
|
'',
|
|
890
980
|
'On your dev machine, add this remote:',
|
|
891
981
|
'',
|
|
@@ -893,10 +983,11 @@ async function handleProxyCommand(args) {
|
|
|
893
983
|
'',
|
|
894
984
|
'Example:',
|
|
895
985
|
'',
|
|
896
|
-
`bbx remote add remote-bbx ${exampleHost}:${
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
);
|
|
986
|
+
`bbx remote add remote-bbx ${exampleHost}:${port} --token ${token}`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
lines.push('');
|
|
990
|
+
process.stdout.write(lines.join('\n'));
|
|
900
991
|
return;
|
|
901
992
|
}
|
|
902
993
|
|
|
@@ -916,10 +1007,12 @@ async function handleProxyCommand(args) {
|
|
|
916
1007
|
process.stdout.write('Browser Bridge proxy is disabled.\n');
|
|
917
1008
|
return;
|
|
918
1009
|
}
|
|
1010
|
+
const reachable = await pingExistingDaemon(createTcpBridgeTransport(config.port, '127.0.0.1'));
|
|
919
1011
|
process.stdout.write(
|
|
920
1012
|
[
|
|
921
1013
|
`Browser Bridge proxy is enabled on ${config.bindHost}:${config.port}.`,
|
|
922
1014
|
`Config: ${getProxyConfigPath()}`,
|
|
1015
|
+
`Daemon: ${reachable ? 'reachable' : 'not reachable'} on 127.0.0.1:${config.port}`,
|
|
923
1016
|
'',
|
|
924
1017
|
].join('\n')
|
|
925
1018
|
);
|
|
@@ -930,12 +1023,15 @@ async function handleProxyCommand(args) {
|
|
|
930
1023
|
}
|
|
931
1024
|
|
|
932
1025
|
/**
|
|
1026
|
+
* Parse `bbx proxy enable` flags. Values stay undefined when not passed so an
|
|
1027
|
+
* existing proxy config can be reused instead of silently reset to defaults.
|
|
1028
|
+
*
|
|
933
1029
|
* @param {string[]} args
|
|
934
|
-
* @returns {
|
|
1030
|
+
* @returns {import('./remotes.js').ProxyEnableOptions}
|
|
935
1031
|
*/
|
|
936
1032
|
function parseProxyEnableArgs(args) {
|
|
937
|
-
/** @type {
|
|
938
|
-
const options = {
|
|
1033
|
+
/** @type {import('./remotes.js').ProxyEnableOptions} */
|
|
1034
|
+
const options = {};
|
|
939
1035
|
for (let index = 0; index < args.length; index += 1) {
|
|
940
1036
|
const arg = args[index];
|
|
941
1037
|
if (arg === '--port') {
|
|
@@ -956,9 +1052,16 @@ function parseProxyEnableArgs(args) {
|
|
|
956
1052
|
}
|
|
957
1053
|
continue;
|
|
958
1054
|
}
|
|
1055
|
+
if (arg === '--rotate-token') {
|
|
1056
|
+
options.rotateToken = true;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
959
1059
|
throw new Error(`Unknown proxy enable option "${arg}".`);
|
|
960
1060
|
}
|
|
961
|
-
if (options.
|
|
1061
|
+
if (options.token && options.rotateToken) {
|
|
1062
|
+
throw new Error('Use either --token or --rotate-token, not both.');
|
|
1063
|
+
}
|
|
1064
|
+
if (options.port !== undefined && (options.port < 1 || options.port > 65535)) {
|
|
962
1065
|
throw new Error('port must be an integer between 1 and 65535.');
|
|
963
1066
|
}
|
|
964
1067
|
return options;
|
|
@@ -1049,10 +1152,23 @@ async function handleRemoteCommand(args) {
|
|
|
1049
1152
|
try {
|
|
1050
1153
|
await remoteClient.connect();
|
|
1051
1154
|
const response = await remoteClient.request({ method: 'health.ping' });
|
|
1155
|
+
if (response.ok) {
|
|
1156
|
+
const health = /** @type {{ extensionConnected?: boolean }} */ (response.result);
|
|
1157
|
+
process.stdout.write(
|
|
1158
|
+
`Remote destination "${name}" is reachable (extension ${
|
|
1159
|
+
health.extensionConnected === true ? 'connected' : 'not connected'
|
|
1160
|
+
}).\n`
|
|
1161
|
+
);
|
|
1162
|
+
} else {
|
|
1163
|
+
process.exitCode = 1;
|
|
1164
|
+
process.stdout.write(`Remote destination "${name}" failed: ${response.error.message}\n`);
|
|
1165
|
+
}
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
process.exitCode = 1;
|
|
1052
1168
|
process.stdout.write(
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1169
|
+
`Remote destination "${name}" is not reachable: ${
|
|
1170
|
+
error instanceof Error ? error.message : String(error)
|
|
1171
|
+
}\n`
|
|
1056
1172
|
);
|
|
1057
1173
|
} finally {
|
|
1058
1174
|
await remoteClient.close();
|
|
@@ -1214,7 +1330,7 @@ function printCallResponse(response) {
|
|
|
1214
1330
|
}
|
|
1215
1331
|
|
|
1216
1332
|
function printUsage() {
|
|
1217
|
-
const blocks = ['Usage: bbx <command> [args]'];
|
|
1333
|
+
const blocks = ['Usage: bbx [--remote <name>] <command> [args]'];
|
|
1218
1334
|
for (const section of CLI_HELP_SECTIONS) {
|
|
1219
1335
|
blocks.push('', `${section.title}:`);
|
|
1220
1336
|
blocks.push(...section.lines.map((line) => ` ${line}`));
|
|
@@ -1297,6 +1413,28 @@ async function parseCallCommand(args) {
|
|
|
1297
1413
|
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
1298
1414
|
}
|
|
1299
1415
|
|
|
1416
|
+
/**
|
|
1417
|
+
* Extract the global --remote flag from anywhere in the argument list.
|
|
1418
|
+
* Exits with a usage error when the flag is present without a value.
|
|
1419
|
+
*
|
|
1420
|
+
* @param {string[]} args
|
|
1421
|
+
* @returns {{ remoteId: string | null, explicit: boolean, rest: string[] }}
|
|
1422
|
+
*/
|
|
1423
|
+
function extractRemoteFlag(args) {
|
|
1424
|
+
const rest = [...args];
|
|
1425
|
+
const index = rest.indexOf('--remote');
|
|
1426
|
+
if (index === -1) {
|
|
1427
|
+
return { remoteId: null, explicit: false, rest };
|
|
1428
|
+
}
|
|
1429
|
+
const remoteId = rest[index + 1];
|
|
1430
|
+
if (!remoteId || remoteId.startsWith('--')) {
|
|
1431
|
+
process.stderr.write('--remote requires a destination name (see `bbx remote list`).\n');
|
|
1432
|
+
process.exit(1);
|
|
1433
|
+
}
|
|
1434
|
+
rest.splice(index, 2);
|
|
1435
|
+
return { remoteId, explicit: true, rest };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1300
1438
|
/**
|
|
1301
1439
|
* @param {string[]} args
|
|
1302
1440
|
* @returns {{ tabId: number | null, rest: string[] }}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { EventEmitter, once } from 'node:events';
|
|
4
|
+
import fs from 'node:fs';
|
|
4
5
|
import net from 'node:net';
|
|
5
6
|
import { randomUUID } from 'node:crypto';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
createRequest,
|
|
9
10
|
DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
10
|
-
|
|
11
|
+
getProtocolVersion,
|
|
11
12
|
parseJsonLines,
|
|
13
|
+
setProtocolPackageVersion,
|
|
12
14
|
} from '../../protocol/src/index.js';
|
|
13
15
|
import {
|
|
14
16
|
createSocketBridgeTransport,
|
|
@@ -26,6 +28,21 @@ import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
|
26
28
|
/** @typedef {import('./types.js').PendingRequest} PendingRequest */
|
|
27
29
|
/** @typedef {import('./types.js').ProtocolHealthResult} ProtocolHealthResult */
|
|
28
30
|
|
|
31
|
+
setProtocolPackageVersion(loadPackageVersion());
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @returns {string | null}
|
|
35
|
+
*/
|
|
36
|
+
function loadPackageVersion() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8');
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return parsed && typeof parsed.version === 'string' ? parsed.version : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
/**
|
|
30
47
|
* @param {string} left
|
|
31
48
|
* @param {string} right
|
|
@@ -57,6 +74,18 @@ function createTimeoutError(method, timeoutMs) {
|
|
|
57
74
|
return error;
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
/**
|
|
78
|
+
* npm exec often runs a checkout-local CLI against an already-connected global
|
|
79
|
+
* daemon. In that mode, protocol drift should warn instead of replacing the
|
|
80
|
+
* user's working daemon and temporarily disconnecting the extension.
|
|
81
|
+
*
|
|
82
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
function shouldAutoRestartDaemonOnVersionMismatch(env = process.env) {
|
|
86
|
+
return env.npm_command !== 'exec';
|
|
87
|
+
}
|
|
88
|
+
|
|
60
89
|
/**
|
|
61
90
|
* @param {net.Socket} socket
|
|
62
91
|
* @param {string} line
|
|
@@ -83,7 +112,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
83
112
|
clientId = `agent_${randomUUID()}`,
|
|
84
113
|
defaultTimeoutMs = DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
85
114
|
autoReconnect = false,
|
|
86
|
-
restartDaemonOnVersionMismatch =
|
|
115
|
+
restartDaemonOnVersionMismatch = shouldAutoRestartDaemonOnVersionMismatch(),
|
|
87
116
|
restartDaemonFn = restartBridgeDaemon,
|
|
88
117
|
authToken = undefined,
|
|
89
118
|
} = {}) {
|
|
@@ -390,9 +419,16 @@ export class BridgeClient extends EventEmitter {
|
|
|
390
419
|
? healthResult.supported_versions
|
|
391
420
|
: [];
|
|
392
421
|
const latestRemote = remoteVersions[0];
|
|
422
|
+
if (
|
|
423
|
+
healthResult?.extensionConnected === true &&
|
|
424
|
+
Array.isArray(healthResult.supported_versions) &&
|
|
425
|
+
!healthResult.supported_versions.includes(getProtocolVersion())
|
|
426
|
+
) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
393
429
|
return (
|
|
394
430
|
typeof latestRemote === 'string' &&
|
|
395
|
-
compareProtocolVersions(latestRemote,
|
|
431
|
+
compareProtocolVersions(latestRemote, getProtocolVersion()) < 0
|
|
396
432
|
);
|
|
397
433
|
}
|
|
398
434
|
|
|
@@ -436,20 +472,21 @@ export class BridgeClient extends EventEmitter {
|
|
|
436
472
|
if (remoteVersions.length === 0) {
|
|
437
473
|
return {
|
|
438
474
|
compatible: true,
|
|
439
|
-
localVersion:
|
|
475
|
+
localVersion: getProtocolVersion(),
|
|
440
476
|
remoteVersions,
|
|
441
477
|
};
|
|
442
478
|
}
|
|
443
|
-
const
|
|
479
|
+
const localVersion = getProtocolVersion();
|
|
480
|
+
const compatible = remoteVersions.includes(localVersion);
|
|
444
481
|
return {
|
|
445
482
|
compatible,
|
|
446
|
-
localVersion
|
|
483
|
+
localVersion,
|
|
447
484
|
remoteVersions,
|
|
448
485
|
...(!compatible && {
|
|
449
486
|
warning:
|
|
450
487
|
typeof healthResult?.migration_hint === 'string' && healthResult.migration_hint
|
|
451
488
|
? healthResult.migration_hint
|
|
452
|
-
: `Protocol mismatch: client speaks ${
|
|
489
|
+
: `Protocol mismatch: client speaks ${localVersion} but remote supports [${remoteVersions.join(', ')}]. Update the ${remoteVersions[0] > localVersion ? 'client (npm)' : 'extension'} to match.`,
|
|
453
490
|
}),
|
|
454
491
|
};
|
|
455
492
|
}
|
|
@@ -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={}]
|