@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.
Files changed (44) hide show
  1. package/PRIVACY.md +57 -12
  2. package/README.md +19 -8
  3. package/SECURITY.md +12 -0
  4. package/package.json +15 -6
  5. package/packages/agent-client/src/cli-args.js +59 -0
  6. package/packages/agent-client/src/cli-batch.js +121 -58
  7. package/packages/agent-client/src/cli-output.js +8 -1
  8. package/packages/agent-client/src/cli.js +272 -70
  9. package/packages/agent-client/src/client.js +296 -8
  10. package/packages/agent-client/src/command-registry.js +64 -55
  11. package/packages/agent-client/src/config.js +195 -0
  12. package/packages/agent-client/src/content-extract.js +198 -0
  13. package/packages/agent-client/src/npm-self-update.js +305 -0
  14. package/packages/agent-client/src/remotes.js +4 -2
  15. package/packages/agent-client/src/runtime.js +864 -39
  16. package/packages/agent-client/src/types.ts +169 -6
  17. package/packages/mcp-server/src/guidance.js +1 -1
  18. package/packages/mcp-server/src/handlers-capture.js +69 -6
  19. package/packages/mcp-server/src/handlers-dom.js +61 -4
  20. package/packages/mcp-server/src/handlers-navigation.js +2 -1
  21. package/packages/mcp-server/src/handlers-page.js +211 -54
  22. package/packages/mcp-server/src/handlers-utils.js +101 -10
  23. package/packages/mcp-server/src/handlers.js +14 -7
  24. package/packages/mcp-server/src/server.js +176 -11
  25. package/packages/native-host/src/artifact-store.js +353 -0
  26. package/packages/native-host/src/config.js +9 -1
  27. package/packages/native-host/src/daemon-logger.js +3 -1
  28. package/packages/native-host/src/daemon.js +701 -33
  29. package/packages/native-host/src/native-host.js +28 -3
  30. package/packages/protocol/src/capability-values.js +2 -0
  31. package/packages/protocol/src/defaults.js +31 -0
  32. package/packages/protocol/src/errors.js +102 -6
  33. package/packages/protocol/src/incidental-sanitizer.js +197 -0
  34. package/packages/protocol/src/index.js +2 -0
  35. package/packages/protocol/src/protocol.js +612 -6
  36. package/packages/protocol/src/recovery-telemetry.js +302 -0
  37. package/packages/protocol/src/registry.js +206 -38
  38. package/packages/protocol/src/summary.js +203 -30
  39. package/packages/protocol/src/types.ts +721 -2
  40. package/skills/browser-bridge/SKILL.md +54 -19
  41. package/skills/browser-bridge/references/interaction.md +55 -11
  42. package/skills/browser-bridge/references/patch-workflow.md +2 -0
  43. package/skills/browser-bridge/references/protocol.md +265 -86
  44. 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
- setProtocolPackageVersion(loadPackageVersion());
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 multiple bridge requests without waiting for each response serially.
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 Promise.all(calls.map((call) => this.request(call)));
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(healthResult.supported_versions) &&
425
- !healthResult.supported_versions.includes(getProtocolVersion())
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?.supported_versions)
470
- ? healthResult.supported_versions
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
- (_r, ref) => ({ elementRef: ref }),
43
- { resolve: true, printMethod: 'dom.describe' }
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, ref) => ({
49
- elementRef: ref,
62
+ (r) => ({
63
+ target: createAtomicTarget(r[0]),
50
64
  textBudget: r[1] ? parseIntArg(r[1], 'budget') : undefined,
51
65
  }),
52
- { resolve: true, printMethod: 'dom.get_text' }
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, ref) => ({ elementRef: ref, properties: parseCommaList(r[1]) }),
58
- { resolve: true, printMethod: 'styles.get_computed' }
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
- (_r, ref) => ({ elementRef: ref }),
64
- { resolve: true, printMethod: 'layout.get_box_model' }
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, ref) => ({
88
- target: { elementRef: ref },
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
- 'input.hover',
99
- 'bbx hover <ref|selector>',
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, ref) => ({
107
- elementRef: ref,
109
+ (r) => ({
110
+ target: createAtomicTarget(r[0]),
108
111
  maxLength: r[1] ? parseIntArg(r[1], 'maxLen') : undefined,
109
112
  }),
110
- { resolve: true }
113
+ {}
111
114
  ),
112
115
  'patch-style': createShortcutCommand(
113
116
  'patch.apply_styles',
114
117
  'bbx patch-style <ref|sel> prop=val',
115
- (r, ref) => ({
116
- target: { elementRef: ref },
118
+ (r) => ({
119
+ target: createAtomicTarget(r[0]),
117
120
  declarations: parsePropertyAssignments(r.slice(1)),
118
121
  }),
119
- { resolve: true }
122
+ {}
120
123
  ),
121
124
  'patch-text': createShortcutCommand(
122
125
  'patch.apply_dom',
123
126
  'bbx patch-text <ref|sel> <text...>',
124
- (r, ref) => ({
125
- target: { elementRef: ref },
127
+ (r) => ({
128
+ target: createAtomicTarget(r[0]),
126
129
  operation: 'set_text',
127
130
  value: r.slice(1).join(' '),
128
131
  }),
129
- { resolve: true, description: 'Apply a reversible DOM text patch' }
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, ref) => ({ elementRef: ref, attributes: parseCommaList(r[1]) }),
226
- { resolve: true, printMethod: 'dom.get_attributes' }
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
- (_r, ref) => ({ elementRef: ref }),
232
- { resolve: true, printMethod: 'styles.get_matched_rules' }
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 Diagnose install, daemon, extension, and access readiness',
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 Request Browser Bridge access for the focused window',
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?},...]' Parallel method calls",
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] Capture partial element screenshot',
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
  ]);