@browserbridge/bbx 1.7.8 → 1.9.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/PRIVACY.md +57 -12
- package/README.md +19 -8
- package/SECURITY.md +12 -0
- package/package.json +15 -6
- package/packages/agent-client/src/cli-args.js +59 -0
- package/packages/agent-client/src/cli-batch.js +121 -58
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli.js +272 -70
- package/packages/agent-client/src/client.js +296 -8
- package/packages/agent-client/src/command-registry.js +64 -55
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/content-extract.js +198 -0
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +864 -39
- package/packages/agent-client/src/types.ts +169 -6
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +69 -6
- package/packages/mcp-server/src/handlers-dom.js +61 -4
- package/packages/mcp-server/src/handlers-navigation.js +2 -1
- package/packages/mcp-server/src/handlers-page.js +211 -54
- package/packages/mcp-server/src/handlers-utils.js +101 -10
- package/packages/mcp-server/src/handlers.js +14 -7
- package/packages/mcp-server/src/server.js +176 -11
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +701 -33
- package/packages/native-host/src/native-host.js +28 -3
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +31 -0
- package/packages/protocol/src/errors.js +102 -6
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +612 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +206 -38
- package/packages/protocol/src/summary.js +203 -30
- package/packages/protocol/src/types.ts +721 -2
- package/skills/browser-bridge/SKILL.md +54 -19
- package/skills/browser-bridge/references/interaction.md +55 -11
- package/skills/browser-bridge/references/patch-workflow.md +2 -0
- package/skills/browser-bridge/references/protocol.md +265 -86
- package/skills/browser-bridge/references/token-efficiency.md +29 -13
|
@@ -6,12 +6,20 @@ import net from 'node:net';
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
createFailure,
|
|
9
10
|
createRequest,
|
|
11
|
+
createSuccess,
|
|
10
12
|
DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
13
|
+
ERROR_CODES,
|
|
11
14
|
getProtocolVersion,
|
|
15
|
+
isBatchSafeBridgeCall,
|
|
16
|
+
isBridgeMethod,
|
|
17
|
+
MAX_BATCH_CALLS,
|
|
18
|
+
MAX_BATCH_CONCURRENCY,
|
|
12
19
|
parseJsonLines,
|
|
13
20
|
setProtocolPackageVersion,
|
|
14
21
|
} from '../../protocol/src/index.js';
|
|
22
|
+
import { extractContentFromHtml } from './content-extract.js';
|
|
15
23
|
import {
|
|
16
24
|
createSocketBridgeTransport,
|
|
17
25
|
getBridgeTransport,
|
|
@@ -19,6 +27,12 @@ import {
|
|
|
19
27
|
} from '../../native-host/src/config.js';
|
|
20
28
|
import { normalizeBridgeAuthToken, readBridgeAuthToken } from '../../native-host/src/auth-token.js';
|
|
21
29
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
30
|
+
import {
|
|
31
|
+
comparePackageVersions,
|
|
32
|
+
NpmPackageUpdatedError,
|
|
33
|
+
parseStableVersion,
|
|
34
|
+
updateCompatibleNpmPackage,
|
|
35
|
+
} from './npm-self-update.js';
|
|
22
36
|
|
|
23
37
|
/** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
|
|
24
38
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
@@ -28,7 +42,8 @@ import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
|
28
42
|
/** @typedef {import('./types.js').PendingRequest} PendingRequest */
|
|
29
43
|
/** @typedef {import('./types.js').ProtocolHealthResult} ProtocolHealthResult */
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
const CLIENT_PACKAGE_VERSION = loadPackageVersion();
|
|
46
|
+
setProtocolPackageVersion(CLIENT_PACKAGE_VERSION);
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
49
|
* @returns {string | null}
|
|
@@ -112,8 +127,12 @@ export class BridgeClient extends EventEmitter {
|
|
|
112
127
|
clientId = `agent_${randomUUID()}`,
|
|
113
128
|
defaultTimeoutMs = DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
114
129
|
autoReconnect = false,
|
|
130
|
+
checkProtocolOnConnect = true,
|
|
115
131
|
restartDaemonOnVersionMismatch = shouldAutoRestartDaemonOnVersionMismatch(),
|
|
116
132
|
restartDaemonFn = restartBridgeDaemon,
|
|
133
|
+
updateNpmOnCompatibleVersion = false,
|
|
134
|
+
exitProcessOnNpmUpdate = false,
|
|
135
|
+
updateCompatibleNpmPackageFn = updateCompatibleNpmPackage,
|
|
117
136
|
authToken = undefined,
|
|
118
137
|
} = {}) {
|
|
119
138
|
super();
|
|
@@ -123,8 +142,12 @@ export class BridgeClient extends EventEmitter {
|
|
|
123
142
|
this.clientId = clientId;
|
|
124
143
|
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
125
144
|
this.autoReconnect = autoReconnect;
|
|
145
|
+
this.checkProtocolOnConnect = checkProtocolOnConnect;
|
|
126
146
|
this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
|
|
127
147
|
this.restartDaemonFn = restartDaemonFn;
|
|
148
|
+
this.updateNpmOnCompatibleVersion = updateNpmOnCompatibleVersion;
|
|
149
|
+
this.exitProcessOnNpmUpdate = exitProcessOnNpmUpdate;
|
|
150
|
+
this.updateCompatibleNpmPackageFn = updateCompatibleNpmPackageFn;
|
|
128
151
|
this.authToken = authToken;
|
|
129
152
|
this.socket = null;
|
|
130
153
|
this.connected = false;
|
|
@@ -134,6 +157,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
134
157
|
this.waiting = new Map();
|
|
135
158
|
this._reconnecting = false;
|
|
136
159
|
this._attemptedVersionMismatchRestart = false;
|
|
160
|
+
this._attemptedNpmUpdate = false;
|
|
161
|
+
/** @type {import('./types.js').NpmUpdateResult | null} */
|
|
162
|
+
this.npmUpdateResult = null;
|
|
137
163
|
}
|
|
138
164
|
|
|
139
165
|
/**
|
|
@@ -246,6 +272,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
246
272
|
|
|
247
273
|
this.protocolCompatibility = null;
|
|
248
274
|
this.protocolWarning = null;
|
|
275
|
+
if (!this.checkProtocolOnConnect) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
249
278
|
|
|
250
279
|
/** @type {ProtocolHealthResult | null} */
|
|
251
280
|
let healthResult = null;
|
|
@@ -271,6 +300,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
271
300
|
if (this.protocolCompatibility.compatible) {
|
|
272
301
|
this._attemptedVersionMismatchRestart = false;
|
|
273
302
|
}
|
|
303
|
+
if (await this.tryCompatibleNpmUpdate(healthResult)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
274
306
|
if (this.shouldRestartDaemonForProtocolMismatch(healthResult)) {
|
|
275
307
|
this._attemptedVersionMismatchRestart = true;
|
|
276
308
|
await this.disconnectForDaemonRestart();
|
|
@@ -279,6 +311,95 @@ export class BridgeClient extends EventEmitter {
|
|
|
279
311
|
}
|
|
280
312
|
}
|
|
281
313
|
|
|
314
|
+
/**
|
|
315
|
+
* @param {ProtocolHealthResult} healthResult
|
|
316
|
+
* @returns {Promise<boolean>}
|
|
317
|
+
*/
|
|
318
|
+
async tryCompatibleNpmUpdate(healthResult) {
|
|
319
|
+
const extensionVersion = healthResult.extensionVersion;
|
|
320
|
+
const processVersion = CLIENT_PACKAGE_VERSION;
|
|
321
|
+
const extensionVersions = Array.isArray(healthResult.extension_supported_versions)
|
|
322
|
+
? healthResult.extension_supported_versions
|
|
323
|
+
: healthResult.extensionConnected === true && Array.isArray(healthResult.supported_versions)
|
|
324
|
+
? healthResult.supported_versions
|
|
325
|
+
: [];
|
|
326
|
+
if (
|
|
327
|
+
!this.updateNpmOnCompatibleVersion ||
|
|
328
|
+
this._attemptedNpmUpdate ||
|
|
329
|
+
healthResult.extensionConnected !== true ||
|
|
330
|
+
typeof extensionVersion !== 'string' ||
|
|
331
|
+
!parseStableVersion(extensionVersion) ||
|
|
332
|
+
typeof processVersion !== 'string' ||
|
|
333
|
+
!parseStableVersion(processVersion) ||
|
|
334
|
+
comparePackageVersions(extensionVersion, processVersion) <= 0 ||
|
|
335
|
+
extensionVersions.length === 0
|
|
336
|
+
) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this._attemptedNpmUpdate = true;
|
|
341
|
+
const previousAutoReconnect = this.autoReconnect;
|
|
342
|
+
this.autoReconnect = false;
|
|
343
|
+
try {
|
|
344
|
+
this.npmUpdateResult = await this.updateCompatibleNpmPackageFn({
|
|
345
|
+
extensionVersion,
|
|
346
|
+
supportedVersions: extensionVersions,
|
|
347
|
+
});
|
|
348
|
+
if (!this.npmUpdateResult.updated) {
|
|
349
|
+
if (
|
|
350
|
+
typeof this.npmUpdateResult.previousVersion === 'string' &&
|
|
351
|
+
parseStableVersion(this.npmUpdateResult.previousVersion) &&
|
|
352
|
+
comparePackageVersions(this.npmUpdateResult.previousVersion, processVersion) > 0
|
|
353
|
+
) {
|
|
354
|
+
this.requireFreshProcess(this.npmUpdateResult.previousVersion);
|
|
355
|
+
}
|
|
356
|
+
if (this.npmUpdateResult.reason === 'not_global_install') {
|
|
357
|
+
this.setProtocolWarning(
|
|
358
|
+
'Automatic npm update skipped because this Browser Bridge client is not running from the global npm installation.'
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.requireFreshProcess(this.npmUpdateResult.version || extensionVersion);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (error instanceof NpmPackageUpdatedError || this.npmUpdateResult?.updated) {
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
this.setProtocolWarning(
|
|
370
|
+
`Automatic Browser Bridge npm update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
371
|
+
);
|
|
372
|
+
return false;
|
|
373
|
+
} finally {
|
|
374
|
+
this.autoReconnect = previousAutoReconnect;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {string} version
|
|
380
|
+
* @returns {never}
|
|
381
|
+
*/
|
|
382
|
+
requireFreshProcess(version) {
|
|
383
|
+
if (this.exitProcessOnNpmUpdate) {
|
|
384
|
+
setTimeout(() => process.exit(0), 250).unref();
|
|
385
|
+
}
|
|
386
|
+
throw new NpmPackageUpdatedError(version);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {string} warning
|
|
391
|
+
* @returns {void}
|
|
392
|
+
*/
|
|
393
|
+
setProtocolWarning(warning) {
|
|
394
|
+
this.protocolWarning = this.protocolWarning ? `${this.protocolWarning} ${warning}` : warning;
|
|
395
|
+
if (this.protocolCompatibility) {
|
|
396
|
+
this.protocolCompatibility = {
|
|
397
|
+
...this.protocolCompatibility,
|
|
398
|
+
warning: this.protocolWarning,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
282
403
|
/**
|
|
283
404
|
* @param {{
|
|
284
405
|
* method: BridgeMethod,
|
|
@@ -312,6 +433,113 @@ export class BridgeClient extends EventEmitter {
|
|
|
312
433
|
meta,
|
|
313
434
|
});
|
|
314
435
|
|
|
436
|
+
if (method === 'page.extract_content') {
|
|
437
|
+
const params =
|
|
438
|
+
/** @type {import('../../protocol/src/types.js').NormalizedExtractContentParams} */ (
|
|
439
|
+
request.params
|
|
440
|
+
);
|
|
441
|
+
const titleResponse = params.includeMetadata
|
|
442
|
+
? await this.request({ method: 'page.get_state', tabId, meta, timeoutMs })
|
|
443
|
+
: null;
|
|
444
|
+
if (titleResponse && !titleResponse.ok) {
|
|
445
|
+
return {
|
|
446
|
+
...titleResponse,
|
|
447
|
+
id: request.id,
|
|
448
|
+
meta: { ...titleResponse.meta, method },
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const titleResult =
|
|
452
|
+
titleResponse?.ok && titleResponse.result && typeof titleResponse.result === 'object'
|
|
453
|
+
? /** @type {Record<string, unknown>} */ (titleResponse.result)
|
|
454
|
+
: {};
|
|
455
|
+
const readSnapshot = () =>
|
|
456
|
+
this.request({
|
|
457
|
+
method: 'dom.get_html',
|
|
458
|
+
tabId,
|
|
459
|
+
params: {
|
|
460
|
+
target: { selector: params.selector ?? 'body' },
|
|
461
|
+
outer: true,
|
|
462
|
+
maxLength: 50_000,
|
|
463
|
+
},
|
|
464
|
+
meta: { ...meta, token_budget: null },
|
|
465
|
+
timeoutMs,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
let snapshotResponse = await readSnapshot();
|
|
469
|
+
if (!snapshotResponse.ok) {
|
|
470
|
+
return {
|
|
471
|
+
...snapshotResponse,
|
|
472
|
+
id: request.id,
|
|
473
|
+
meta: { ...snapshotResponse.meta, method },
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
let snapshot = normalizeHtmlSnapshot(snapshotResponse.result);
|
|
477
|
+
if (snapshot.truncated) {
|
|
478
|
+
return createFailure(
|
|
479
|
+
request.id,
|
|
480
|
+
ERROR_CODES.RESULT_TRUNCATED,
|
|
481
|
+
'The bounded HTML snapshot was too large for semantic extraction.',
|
|
482
|
+
{
|
|
483
|
+
selector: params.selector,
|
|
484
|
+
omitted: snapshot.omitted,
|
|
485
|
+
maxLength: 50_000,
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
method,
|
|
489
|
+
continuation_hint: 'Retry with a narrower selector.',
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let settlement;
|
|
495
|
+
if (params.consistency === 'settled') {
|
|
496
|
+
const deadline = Date.now() + params.settleTimeoutMs;
|
|
497
|
+
let timedOut = false;
|
|
498
|
+
while (true) {
|
|
499
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
500
|
+
const nextResponse = await readSnapshot();
|
|
501
|
+
if (!nextResponse.ok) {
|
|
502
|
+
return {
|
|
503
|
+
...nextResponse,
|
|
504
|
+
id: request.id,
|
|
505
|
+
meta: { ...nextResponse.meta, method },
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const nextSnapshot = normalizeHtmlSnapshot(nextResponse.result);
|
|
509
|
+
if (nextSnapshot.truncated) {
|
|
510
|
+
return createFailure(
|
|
511
|
+
request.id,
|
|
512
|
+
ERROR_CODES.RESULT_TRUNCATED,
|
|
513
|
+
'The bounded HTML snapshot was too large for semantic extraction.',
|
|
514
|
+
{ selector: params.selector, omitted: nextSnapshot.omitted, maxLength: 50_000 },
|
|
515
|
+
{ method, continuation_hint: 'Retry with a narrower selector.' }
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (nextSnapshot.html === snapshot.html) {
|
|
519
|
+
snapshot = nextSnapshot;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
snapshot = nextSnapshot;
|
|
523
|
+
if (Date.now() >= deadline) {
|
|
524
|
+
timedOut = true;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
settlement = { requested: true, quietMs: 100, timedOut };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const result = extractContentFromHtml(snapshot.html, params, {
|
|
532
|
+
title: typeof titleResult.title === 'string' ? titleResult.title : undefined,
|
|
533
|
+
settlement,
|
|
534
|
+
});
|
|
535
|
+
return this.attachProtocolWarning(
|
|
536
|
+
createSuccess(request.id, result, {
|
|
537
|
+
method,
|
|
538
|
+
node_processed: true,
|
|
539
|
+
})
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
315
543
|
const responsePromise = new Promise((resolve, reject) => {
|
|
316
544
|
const timeoutId = setTimeout(() => {
|
|
317
545
|
this.waiting.delete(request.id);
|
|
@@ -340,7 +568,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
340
568
|
}
|
|
341
569
|
|
|
342
570
|
/**
|
|
343
|
-
* Send
|
|
571
|
+
* Send up to 20 read-only bridge requests with bounded concurrency.
|
|
344
572
|
*
|
|
345
573
|
* @param {Array<{
|
|
346
574
|
* method: BridgeMethod,
|
|
@@ -355,8 +583,27 @@ export class BridgeClient extends EventEmitter {
|
|
|
355
583
|
if (!Array.isArray(calls)) {
|
|
356
584
|
throw new TypeError('BridgeClient.batch expects an array of request objects.');
|
|
357
585
|
}
|
|
586
|
+
if (calls.length === 0 || calls.length > MAX_BATCH_CALLS) {
|
|
587
|
+
throw new RangeError(
|
|
588
|
+
`BridgeClient.batch expects between 1 and ${MAX_BATCH_CALLS} request objects.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
for (const [index, call] of calls.entries()) {
|
|
592
|
+
if (!call || typeof call !== 'object' || !isBridgeMethod(call.method)) {
|
|
593
|
+
throw new TypeError(`BridgeClient.batch calls[${index}] needs a known bridge method.`);
|
|
594
|
+
}
|
|
595
|
+
const params = call.params ?? {};
|
|
596
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
597
|
+
throw new TypeError(`BridgeClient.batch calls[${index}].params must be an object.`);
|
|
598
|
+
}
|
|
599
|
+
if (!isBatchSafeBridgeCall(call.method, params)) {
|
|
600
|
+
throw new TypeError(
|
|
601
|
+
`${call.method} is not safe for BridgeClient.batch. Call it sequentially.`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
358
605
|
|
|
359
|
-
return
|
|
606
|
+
return mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, (call) => this.request(call));
|
|
360
607
|
}
|
|
361
608
|
|
|
362
609
|
/**
|
|
@@ -419,10 +666,13 @@ export class BridgeClient extends EventEmitter {
|
|
|
419
666
|
? healthResult.supported_versions
|
|
420
667
|
: [];
|
|
421
668
|
const latestRemote = remoteVersions[0];
|
|
669
|
+
const extensionVersions = Array.isArray(healthResult?.extension_supported_versions)
|
|
670
|
+
? healthResult.extension_supported_versions
|
|
671
|
+
: healthResult?.supported_versions;
|
|
422
672
|
if (
|
|
423
673
|
healthResult?.extensionConnected === true &&
|
|
424
|
-
Array.isArray(
|
|
425
|
-
!
|
|
674
|
+
Array.isArray(extensionVersions) &&
|
|
675
|
+
!extensionVersions.includes(getProtocolVersion())
|
|
426
676
|
) {
|
|
427
677
|
return false;
|
|
428
678
|
}
|
|
@@ -466,9 +716,11 @@ export class BridgeClient extends EventEmitter {
|
|
|
466
716
|
* @returns {{ compatible: boolean, localVersion: string, remoteVersions: string[], warning?: string }}
|
|
467
717
|
*/
|
|
468
718
|
static checkProtocolVersion(healthResult) {
|
|
469
|
-
const remoteVersions = Array.isArray(healthResult?.
|
|
470
|
-
? healthResult.
|
|
471
|
-
:
|
|
719
|
+
const remoteVersions = Array.isArray(healthResult?.extension_supported_versions)
|
|
720
|
+
? healthResult.extension_supported_versions
|
|
721
|
+
: Array.isArray(healthResult?.supported_versions)
|
|
722
|
+
? healthResult.supported_versions
|
|
723
|
+
: [];
|
|
472
724
|
if (remoteVersions.length === 0) {
|
|
473
725
|
return {
|
|
474
726
|
compatible: true,
|
|
@@ -520,3 +772,39 @@ export class BridgeClient extends EventEmitter {
|
|
|
520
772
|
};
|
|
521
773
|
}
|
|
522
774
|
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* @template T, R
|
|
778
|
+
* @param {T[]} values
|
|
779
|
+
* @param {number} concurrency
|
|
780
|
+
* @param {(value: T) => Promise<R>} callback
|
|
781
|
+
* @returns {Promise<R[]>}
|
|
782
|
+
*/
|
|
783
|
+
async function mapWithConcurrency(values, concurrency, callback) {
|
|
784
|
+
/** @type {R[]} */
|
|
785
|
+
const results = new Array(values.length);
|
|
786
|
+
let nextIndex = 0;
|
|
787
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
788
|
+
while (nextIndex < values.length) {
|
|
789
|
+
const index = nextIndex;
|
|
790
|
+
nextIndex += 1;
|
|
791
|
+
results[index] = await callback(values[index]);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
await Promise.all(workers);
|
|
795
|
+
return results;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* @param {unknown} result
|
|
800
|
+
* @returns {{ html: string, truncated: boolean, omitted: number }}
|
|
801
|
+
*/
|
|
802
|
+
function normalizeHtmlSnapshot(result) {
|
|
803
|
+
const value =
|
|
804
|
+
result && typeof result === 'object' ? /** @type {Record<string, unknown>} */ (result) : {};
|
|
805
|
+
return {
|
|
806
|
+
html: typeof value.html === 'string' ? value.html : '',
|
|
807
|
+
truncated: value.truncated === true,
|
|
808
|
+
omitted: typeof value.omitted === 'number' ? value.omitted : 0,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
@@ -30,103 +30,106 @@ function createShortcutCommand(method, usage, build, options = {}) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Preserve explicit refs and pass selectors directly to methods that support
|
|
35
|
+
* atomic target resolution.
|
|
36
|
+
*
|
|
37
|
+
* @param {string | undefined} value
|
|
38
|
+
* @returns {{ elementRef: string } | { selector: string }}
|
|
39
|
+
*/
|
|
40
|
+
function createAtomicTarget(value) {
|
|
41
|
+
if (!value) throw new Error('A ref or selector is required.');
|
|
42
|
+
return value.startsWith('el_') ? { elementRef: value } : { selector: value };
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/** @type {Record<string, ShortcutCommand>} */
|
|
34
46
|
export const SHORTCUT_COMMANDS = {
|
|
35
|
-
'access-request': createShortcutCommand('access.request', 'bbx access-request', () =>
|
|
47
|
+
'access-request': createShortcutCommand('access.request', 'bbx access-request [intent]', (r) =>
|
|
48
|
+
r[0] ? { intent: r[0] } : {}
|
|
49
|
+
),
|
|
36
50
|
'dom-query': createShortcutCommand('dom.query', 'bbx dom-query [selector]', (r) => ({
|
|
37
51
|
selector: r[0] || 'body',
|
|
38
52
|
})),
|
|
39
53
|
describe: createShortcutCommand(
|
|
40
54
|
'dom.describe',
|
|
41
55
|
'bbx describe <ref|selector>',
|
|
42
|
-
(
|
|
43
|
-
{
|
|
56
|
+
(r) => ({ target: createAtomicTarget(r[0]) }),
|
|
57
|
+
{ printMethod: 'dom.describe' }
|
|
44
58
|
),
|
|
45
59
|
text: createShortcutCommand(
|
|
46
60
|
'dom.get_text',
|
|
47
61
|
'bbx text <ref|selector> [budget]',
|
|
48
|
-
(r
|
|
49
|
-
|
|
62
|
+
(r) => ({
|
|
63
|
+
target: createAtomicTarget(r[0]),
|
|
50
64
|
textBudget: r[1] ? parseIntArg(r[1], 'budget') : undefined,
|
|
51
65
|
}),
|
|
52
|
-
{
|
|
66
|
+
{ printMethod: 'dom.get_text' }
|
|
53
67
|
),
|
|
54
68
|
styles: createShortcutCommand(
|
|
55
69
|
'styles.get_computed',
|
|
56
70
|
'bbx styles <ref|selector> [props]',
|
|
57
|
-
(r
|
|
58
|
-
{
|
|
71
|
+
(r) => ({ target: createAtomicTarget(r[0]), properties: parseCommaList(r[1]) }),
|
|
72
|
+
{ printMethod: 'styles.get_computed' }
|
|
59
73
|
),
|
|
60
74
|
box: createShortcutCommand(
|
|
61
75
|
'layout.get_box_model',
|
|
62
76
|
'bbx box <ref|selector>',
|
|
63
|
-
(
|
|
64
|
-
{
|
|
65
|
-
),
|
|
66
|
-
click: createShortcutCommand(
|
|
67
|
-
'input.click',
|
|
68
|
-
'bbx click <ref|selector> [button]',
|
|
69
|
-
(r, ref) => ({ target: { elementRef: ref }, button: r[1] }),
|
|
70
|
-
{ resolve: true }
|
|
71
|
-
),
|
|
72
|
-
focus: createShortcutCommand(
|
|
73
|
-
'input.focus',
|
|
74
|
-
'bbx focus <ref|selector>',
|
|
75
|
-
(_r, ref) => ({ target: { elementRef: ref } }),
|
|
76
|
-
{ resolve: true }
|
|
77
|
-
),
|
|
78
|
-
type: createShortcutCommand(
|
|
79
|
-
'input.type',
|
|
80
|
-
'bbx type <ref|selector> <text...>',
|
|
81
|
-
(r, ref) => ({ target: { elementRef: ref }, text: r.slice(1).join(' ') }),
|
|
82
|
-
{ resolve: true }
|
|
77
|
+
(r) => ({ target: createAtomicTarget(r[0]) }),
|
|
78
|
+
{ printMethod: 'layout.get_box_model' }
|
|
83
79
|
),
|
|
80
|
+
click: createShortcutCommand('input.click', 'bbx click <ref|selector> [button]', (r) => ({
|
|
81
|
+
target: createAtomicTarget(r[0]),
|
|
82
|
+
button: r[1],
|
|
83
|
+
})),
|
|
84
|
+
focus: createShortcutCommand('input.focus', 'bbx focus <ref|selector>', (r) => ({
|
|
85
|
+
target: createAtomicTarget(r[0]),
|
|
86
|
+
})),
|
|
87
|
+
type: createShortcutCommand('input.type', 'bbx type <ref|selector> <text...>', (r) => ({
|
|
88
|
+
target: createAtomicTarget(r[0]),
|
|
89
|
+
text: r.slice(1).join(' '),
|
|
90
|
+
})),
|
|
84
91
|
fill: createShortcutCommand(
|
|
85
92
|
'input.fill',
|
|
86
93
|
'bbx fill <ref|selector> <value>',
|
|
87
|
-
(r
|
|
88
|
-
target:
|
|
94
|
+
(r) => ({
|
|
95
|
+
target: createAtomicTarget(r[0]),
|
|
89
96
|
value: r.slice(1).join(' '),
|
|
90
97
|
mode: 'auto',
|
|
91
98
|
}),
|
|
92
99
|
{
|
|
93
|
-
resolve: true,
|
|
94
100
|
description: 'Set input/textarea value (React/Vue/Angular-safe, auto fallback to keystrokes)',
|
|
95
101
|
}
|
|
96
102
|
),
|
|
97
|
-
hover: createShortcutCommand(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
(_r, ref) => ({ target: { elementRef: ref } }),
|
|
101
|
-
{ resolve: true }
|
|
102
|
-
),
|
|
103
|
+
hover: createShortcutCommand('input.hover', 'bbx hover <ref|selector>', (r) => ({
|
|
104
|
+
target: createAtomicTarget(r[0]),
|
|
105
|
+
})),
|
|
103
106
|
html: createShortcutCommand(
|
|
104
107
|
'dom.get_html',
|
|
105
108
|
'bbx html <ref|selector> [maxLen]',
|
|
106
|
-
(r
|
|
107
|
-
|
|
109
|
+
(r) => ({
|
|
110
|
+
target: createAtomicTarget(r[0]),
|
|
108
111
|
maxLength: r[1] ? parseIntArg(r[1], 'maxLen') : undefined,
|
|
109
112
|
}),
|
|
110
|
-
{
|
|
113
|
+
{}
|
|
111
114
|
),
|
|
112
115
|
'patch-style': createShortcutCommand(
|
|
113
116
|
'patch.apply_styles',
|
|
114
117
|
'bbx patch-style <ref|sel> prop=val',
|
|
115
|
-
(r
|
|
116
|
-
target:
|
|
118
|
+
(r) => ({
|
|
119
|
+
target: createAtomicTarget(r[0]),
|
|
117
120
|
declarations: parsePropertyAssignments(r.slice(1)),
|
|
118
121
|
}),
|
|
119
|
-
{
|
|
122
|
+
{}
|
|
120
123
|
),
|
|
121
124
|
'patch-text': createShortcutCommand(
|
|
122
125
|
'patch.apply_dom',
|
|
123
126
|
'bbx patch-text <ref|sel> <text...>',
|
|
124
|
-
(r
|
|
125
|
-
target:
|
|
127
|
+
(r) => ({
|
|
128
|
+
target: createAtomicTarget(r[0]),
|
|
126
129
|
operation: 'set_text',
|
|
127
130
|
value: r.slice(1).join(' '),
|
|
128
131
|
}),
|
|
129
|
-
{
|
|
132
|
+
{ description: 'Apply a reversible DOM text patch' }
|
|
130
133
|
),
|
|
131
134
|
patches: createShortcutCommand('patch.list', 'bbx patches', () => ({}), {
|
|
132
135
|
printMethod: 'patch.list',
|
|
@@ -201,7 +204,10 @@ export const SHORTCUT_COMMANDS = {
|
|
|
201
204
|
maxDepth: r[1] ? parseIntArg(r[1], 'maxDepth') : undefined,
|
|
202
205
|
})
|
|
203
206
|
),
|
|
204
|
-
perf: createShortcutCommand('performance.get_metrics', 'bbx perf', () => ({})
|
|
207
|
+
perf: createShortcutCommand('performance.get_metrics', 'bbx perf', () => ({}), {
|
|
208
|
+
description:
|
|
209
|
+
'Read a raw Chrome/CDP counter point sample (names and units vary; no BBX navigation window or LCP/CLS/INP measurement)',
|
|
210
|
+
}),
|
|
205
211
|
scroll: createShortcutCommand('viewport.scroll', 'bbx scroll <top> [left]', (r) => {
|
|
206
212
|
if (!r[0] && !r[1]) throw new Error('Usage: scroll <top> [left]');
|
|
207
213
|
return {
|
|
@@ -222,14 +228,14 @@ export const SHORTCUT_COMMANDS = {
|
|
|
222
228
|
attrs: createShortcutCommand(
|
|
223
229
|
'dom.get_attributes',
|
|
224
230
|
'bbx attrs <ref|selector> [attr1,...]',
|
|
225
|
-
(r
|
|
226
|
-
{
|
|
231
|
+
(r) => ({ target: createAtomicTarget(r[0]), attributes: parseCommaList(r[1]) }),
|
|
232
|
+
{ printMethod: 'dom.get_attributes' }
|
|
227
233
|
),
|
|
228
234
|
'matched-rules': createShortcutCommand(
|
|
229
235
|
'styles.get_matched_rules',
|
|
230
236
|
'bbx matched-rules <ref|selector>',
|
|
231
|
-
(
|
|
232
|
-
{
|
|
237
|
+
(r) => ({ target: createAtomicTarget(r[0]) }),
|
|
238
|
+
{ printMethod: 'styles.get_matched_rules' }
|
|
233
239
|
),
|
|
234
240
|
};
|
|
235
241
|
|
|
@@ -247,6 +253,7 @@ export const CLI_METHOD_BINDINGS = Object.freeze({
|
|
|
247
253
|
'press-key': 'input.press_key',
|
|
248
254
|
'cdp-press-key': 'cdp.dispatch_key_event',
|
|
249
255
|
screenshot: 'screenshot.capture_element',
|
|
256
|
+
har: 'network.export_har',
|
|
250
257
|
eval: 'page.evaluate',
|
|
251
258
|
});
|
|
252
259
|
|
|
@@ -260,8 +267,9 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
260
267
|
'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
|
|
261
268
|
'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
|
|
262
269
|
'bbx status Check bridge connection',
|
|
263
|
-
'bbx doctor
|
|
270
|
+
'bbx doctor Consolidated local runtime, setup, protocol, and readiness diagnostics',
|
|
264
271
|
'bbx restart Restart the local daemon and running MCP servers',
|
|
272
|
+
'bbx config set auto-update <off|compatible> Configure protocol-compatible npm updates',
|
|
265
273
|
'bbx proxy enable [--port 9223] [--bind-host 127.0.0.1] [--rotate-token] Enable loopback TCP proxy mode for SSH tunneling',
|
|
266
274
|
' Non-loopback binds require --unsafe-plaintext because raw TCP is unencrypted.',
|
|
267
275
|
'bbx proxy disable Disable proxy mode and restart the daemon',
|
|
@@ -270,7 +278,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
270
278
|
'bbx remote list List remote destinations',
|
|
271
279
|
'bbx remote test <name> Ping a remote destination',
|
|
272
280
|
'bbx remote remove <name> Remove a remote destination',
|
|
273
|
-
'bbx access-request
|
|
281
|
+
'bbx access-request [intent] Request access with inspect|interact|capture|navigate|debugger|general intent',
|
|
274
282
|
'bbx logs Recent bridge logs',
|
|
275
283
|
'bbx tabs List available tabs',
|
|
276
284
|
'bbx tab-create [url] Create a new tab',
|
|
@@ -285,7 +293,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
285
293
|
lines: [
|
|
286
294
|
'bbx call [--tab <tabId>] <method> [paramsJson|-] Call any bridge method (- reads JSON from stdin)',
|
|
287
295
|
'bbx <method> [--tab <tabId>] [paramsJson|-] Direct alias for exact bridge methods such as page.get_state',
|
|
288
|
-
"bbx batch '[{method,params,tabId?},...]'
|
|
296
|
+
"bbx batch '[{method,params,tabId?},...]' Up to 20 parallel read-only calls",
|
|
289
297
|
'Every bridge command accepts --remote <name> (or BBX_REMOTE env) to target a remote destination from `bbx remote list`.',
|
|
290
298
|
'Advanced bridge params stay available through `bbx call`, even when shortcuts expose only the common case.',
|
|
291
299
|
'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.',
|
|
@@ -372,7 +380,8 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
372
380
|
{
|
|
373
381
|
title: 'Capture',
|
|
374
382
|
lines: [
|
|
375
|
-
'bbx screenshot [--tab <tabId>] <ref|selector> [path]
|
|
383
|
+
'bbx screenshot [--tab <tabId>] [--format png|jpeg|webp] [--quality 0-100] <ref|selector> [path]',
|
|
384
|
+
'bbx har [--tab <tabId>] [--limit 1-200] [--url-pattern <pattern>] [--delivery inline|artifact|auto] [outPath]',
|
|
376
385
|
],
|
|
377
386
|
},
|
|
378
387
|
]);
|