@browserbridge/bbx 1.0.0 → 1.1.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 (72) hide show
  1. package/README.md +6 -4
  2. package/package.json +53 -53
  3. package/packages/agent-client/src/cli-helpers.js +43 -5
  4. package/packages/agent-client/src/cli.js +176 -171
  5. package/packages/agent-client/src/client.js +66 -21
  6. package/packages/agent-client/src/command-registry.js +104 -69
  7. package/packages/agent-client/src/detect.js +162 -54
  8. package/packages/agent-client/src/install.js +34 -28
  9. package/packages/agent-client/src/mcp-config.js +40 -40
  10. package/packages/agent-client/src/runtime.js +41 -20
  11. package/packages/agent-client/src/setup-status.js +23 -30
  12. package/packages/mcp-server/src/bin.js +57 -5
  13. package/packages/mcp-server/src/handlers.js +573 -256
  14. package/packages/mcp-server/src/server.js +568 -257
  15. package/packages/native-host/bin/bridge-daemon.js +39 -6
  16. package/packages/native-host/bin/install-manifest.js +26 -4
  17. package/packages/native-host/bin/postinstall.js +4 -2
  18. package/packages/native-host/src/config.js +142 -13
  19. package/packages/native-host/src/daemon-process.js +396 -0
  20. package/packages/native-host/src/daemon.js +350 -150
  21. package/packages/native-host/src/framing.js +131 -11
  22. package/packages/native-host/src/install-manifest.js +194 -29
  23. package/packages/native-host/src/native-host.js +154 -102
  24. package/packages/protocol/src/budget.js +3 -7
  25. package/packages/protocol/src/capabilities.js +6 -3
  26. package/packages/protocol/src/defaults.js +1 -0
  27. package/packages/protocol/src/errors.js +15 -11
  28. package/packages/protocol/src/payload-cost.js +19 -6
  29. package/packages/protocol/src/protocol.js +242 -73
  30. package/packages/protocol/src/registry.js +311 -45
  31. package/packages/protocol/src/summary.js +260 -109
  32. package/packages/protocol/src/types.js +29 -4
  33. package/skills/browser-bridge/SKILL.md +3 -2
  34. package/skills/browser-bridge/agents/openai.yaml +3 -3
  35. package/skills/browser-bridge/references/interaction.md +34 -11
  36. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  37. package/skills/browser-bridge/references/protocol.md +127 -71
  38. package/skills/browser-bridge/references/tailwind.md +12 -11
  39. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  40. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  41. package/CHANGELOG.md +0 -55
  42. package/assets/banner.jpg +0 -0
  43. package/assets/logo.png +0 -0
  44. package/assets/logo.svg +0 -65
  45. package/docs/api-reference.md +0 -157
  46. package/docs/cli-guide.md +0 -128
  47. package/docs/index.md +0 -25
  48. package/docs/manual-setup.md +0 -140
  49. package/docs/mcp-vs-cli.md +0 -258
  50. package/docs/publishing.md +0 -114
  51. package/docs/quickstart.md +0 -104
  52. package/docs/troubleshooting.md +0 -59
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -52
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -459
  60. package/packages/extension/src/background-routing.js +0 -91
  61. package/packages/extension/src/background.js +0 -3227
  62. package/packages/extension/src/content-script-helpers.js +0 -281
  63. package/packages/extension/src/content-script.js +0 -1977
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -102
  66. package/packages/extension/ui/offscreen.html +0 -6
  67. package/packages/extension/ui/offscreen.js +0 -61
  68. package/packages/extension/ui/popup.html +0 -35
  69. package/packages/extension/ui/popup.js +0 -279
  70. package/packages/extension/ui/sidepanel.html +0 -102
  71. package/packages/extension/ui/sidepanel.js +0 -1854
  72. package/packages/extension/ui/ui.css +0 -1159
@@ -1,16 +1,13 @@
1
1
  // @ts-check
2
2
 
3
- import { spawn } from 'node:child_process';
4
3
  import net from 'node:net';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
4
 
8
5
  import { createFailure, ERROR_CODES } from '../../protocol/src/index.js';
9
- import { getSocketPath } from './config.js';
6
+ import { createSocketBridgeTransport, getBridgeTransport } from './config.js';
7
+ import { spawnBridgeDaemonProcess } from './daemon-process.js';
10
8
  import { createNativeMessageReader, writeJsonLine, writeNativeMessage } from './framing.js';
11
9
 
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
- const daemonEntryPath = path.resolve(__dirname, '../bin/bridge-daemon.js');
10
+ /** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
14
11
 
15
12
  /**
16
13
  * @typedef {{
@@ -54,10 +51,10 @@ const daemonEntryPath = path.resolve(__dirname, '../bin/bridge-daemon.js');
54
51
  */
55
52
  function isHostBridgeRequest(message) {
56
53
  return Boolean(
57
- message
58
- && typeof message === 'object'
59
- && /** @type {Record<string, unknown>} */ (message).type === 'host.bridge_request'
60
- && typeof /** @type {Record<string, unknown>} */ (message).request === 'object'
54
+ message &&
55
+ typeof message === 'object' &&
56
+ /** @type {Record<string, unknown>} */ (message).type === 'host.bridge_request' &&
57
+ typeof (/** @type {Record<string, unknown>} */ (message).request) === 'object'
61
58
  );
62
59
  }
63
60
 
@@ -67,10 +64,10 @@ function isHostBridgeRequest(message) {
67
64
  */
68
65
  function isHostStatusRequest(message) {
69
66
  return Boolean(
70
- message
71
- && typeof message === 'object'
72
- && /** @type {Record<string, unknown>} */ (message).type === 'host.setup_status.request'
73
- && typeof /** @type {Record<string, unknown>} */ (message).requestId === 'string'
67
+ message &&
68
+ typeof message === 'object' &&
69
+ /** @type {Record<string, unknown>} */ (message).type === 'host.setup_status.request' &&
70
+ typeof (/** @type {Record<string, unknown>} */ (message).requestId) === 'string'
74
71
  );
75
72
  }
76
73
 
@@ -80,9 +77,9 @@ function isHostStatusRequest(message) {
80
77
  */
81
78
  function isHostIdentity(message) {
82
79
  return Boolean(
83
- message
84
- && typeof message === 'object'
85
- && /** @type {Record<string, unknown>} */ (message).type === 'host.identity'
80
+ message &&
81
+ typeof message === 'object' &&
82
+ /** @type {Record<string, unknown>} */ (message).type === 'host.identity'
86
83
  );
87
84
  }
88
85
 
@@ -92,10 +89,10 @@ function isHostIdentity(message) {
92
89
  */
93
90
  function isHostAccessUpdate(message) {
94
91
  return Boolean(
95
- message
96
- && typeof message === 'object'
97
- && /** @type {Record<string, unknown>} */ (message).type === 'host.access_update'
98
- && typeof /** @type {Record<string, unknown>} */ (message).accessEnabled === 'boolean'
92
+ message &&
93
+ typeof message === 'object' &&
94
+ /** @type {Record<string, unknown>} */ (message).type === 'host.access_update' &&
95
+ typeof (/** @type {Record<string, unknown>} */ (message).accessEnabled) === 'boolean'
99
96
  );
100
97
  }
101
98
 
@@ -105,20 +102,24 @@ function isHostAccessUpdate(message) {
105
102
  */
106
103
  function isHostActivity(message) {
107
104
  return Boolean(
108
- message
109
- && typeof message === 'object'
110
- && /** @type {Record<string, unknown>} */ (message).type === 'host.activity'
105
+ message &&
106
+ typeof message === 'object' &&
107
+ /** @type {Record<string, unknown>} */ (message).type === 'host.activity'
111
108
  );
112
109
  }
113
110
 
114
111
  /**
115
- * @param {{ socketPath?: string }} [options={}]
112
+ * @param {{ transport?: BridgeTransport, socketPath?: string }} [options={}]
116
113
  * @returns {Promise<void>}
117
114
  */
118
- export async function runNativeHost({ socketPath = getSocketPath() } = {}) {
115
+ export async function runNativeHost({
116
+ transport = getBridgeTransport(),
117
+ socketPath = undefined,
118
+ } = {}) {
119
+ const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
119
120
  let socket;
120
121
  try {
121
- socket = await connectWithBootstrap(socketPath);
122
+ socket = await connectWithBootstrap(resolvedTransport);
122
123
  } catch (error) {
123
124
  await writeNativeMessage(process.stdout, {
124
125
  type: 'agent.response',
@@ -126,13 +127,23 @@ export async function runNativeHost({ socketPath = getSocketPath() } = {}) {
126
127
  'native_bootstrap',
127
128
  ERROR_CODES.NATIVE_HOST_UNAVAILABLE,
128
129
  error instanceof Error ? error.message : String(error)
129
- )
130
+ ),
130
131
  });
131
132
  return;
132
133
  }
133
134
 
134
135
  socket.setEncoding('utf8');
135
136
  bindBridgeSocketLifecycle(socket);
137
+ const handleStdinEnd = () => {
138
+ socket.destroy();
139
+ };
140
+ process.stdin.once('end', handleStdinEnd);
141
+ const cleanupStdinEndListener = () => {
142
+ process.stdin.removeListener('end', handleStdinEnd);
143
+ };
144
+ socket.once('close', cleanupStdinEndListener);
145
+ socket.once('end', cleanupStdinEndListener);
146
+ socket.once('error', cleanupStdinEndListener);
136
147
  await writeJsonLine(socket, { type: 'register', role: 'extension' });
137
148
 
138
149
  let lineBuffer = '';
@@ -159,72 +170,88 @@ export async function runNativeHost({ socketPath = getSocketPath() } = {}) {
159
170
  if (message.type === 'agent.response') {
160
171
  await writeNativeMessage(process.stdout, {
161
172
  type: 'host.bridge_response',
162
- response: message.response
173
+ response: message.response,
163
174
  });
164
175
  return;
165
176
  }
166
- if (message.type === 'extension.setup_status.response' || message.type === 'extension.setup_status.error') {
177
+ if (
178
+ message.type === 'extension.setup_status.response' ||
179
+ message.type === 'extension.setup_status.error'
180
+ ) {
167
181
  await writeNativeMessage(process.stdout, {
168
- type: message.type === 'extension.setup_status.response'
169
- ? 'host.setup_status.response'
170
- : 'host.setup_status.error',
182
+ type:
183
+ message.type === 'extension.setup_status.response'
184
+ ? 'host.setup_status.response'
185
+ : 'host.setup_status.error',
171
186
  requestId: message.requestId,
172
187
  status: message.status,
173
- error: message.error
188
+ error: message.error,
174
189
  });
175
190
  }
176
191
  })().catch((err) => {
177
- console.error('native-host: socket message handler failed:', err instanceof Error ? err.message : err);
192
+ console.error(
193
+ 'native-host: socket message handler failed:',
194
+ err instanceof Error ? err.message : err
195
+ );
178
196
  });
179
197
  }
180
198
  });
181
199
 
182
- createNativeMessageReader(process.stdin, (message) => {
183
- void (async () => {
184
- if (isHostBridgeRequest(message)) {
185
- await writeJsonLine(socket, {
186
- type: 'agent.request',
187
- request: message.request
188
- });
189
- return;
190
- }
191
- if (isHostStatusRequest(message)) {
192
- await writeJsonLine(socket, {
193
- type: 'extension.setup_status.request',
194
- requestId: message.requestId
195
- });
196
- return;
197
- }
198
- if (isHostIdentity(message)) {
199
- await writeJsonLine(socket, {
200
- type: 'extension.identity',
201
- browserName: message.browserName,
202
- profileLabel: message.profileLabel
203
- });
204
- return;
205
- }
206
- if (isHostAccessUpdate(message)) {
207
- await writeJsonLine(socket, {
208
- type: 'extension.access_update',
209
- accessEnabled: message.accessEnabled
210
- });
211
- return;
212
- }
213
- if (isHostActivity(message)) {
200
+ createNativeMessageReader(
201
+ process.stdin,
202
+ (message) => {
203
+ void (async () => {
204
+ if (isHostBridgeRequest(message)) {
205
+ await writeJsonLine(socket, {
206
+ type: 'agent.request',
207
+ request: message.request,
208
+ });
209
+ return;
210
+ }
211
+ if (isHostStatusRequest(message)) {
212
+ await writeJsonLine(socket, {
213
+ type: 'extension.setup_status.request',
214
+ requestId: message.requestId,
215
+ });
216
+ return;
217
+ }
218
+ if (isHostIdentity(message)) {
219
+ await writeJsonLine(socket, {
220
+ type: 'extension.identity',
221
+ browserName: message.browserName,
222
+ profileLabel: message.profileLabel,
223
+ });
224
+ return;
225
+ }
226
+ if (isHostAccessUpdate(message)) {
227
+ await writeJsonLine(socket, {
228
+ type: 'extension.access_update',
229
+ accessEnabled: message.accessEnabled,
230
+ });
231
+ return;
232
+ }
233
+ if (isHostActivity(message)) {
234
+ await writeJsonLine(socket, {
235
+ type: 'extension.activity',
236
+ at: message.at,
237
+ });
238
+ return;
239
+ }
214
240
  await writeJsonLine(socket, {
215
- type: 'extension.activity',
216
- at: message.at
241
+ type: 'extension.response',
242
+ response: message,
217
243
  });
218
- return;
219
- }
220
- await writeJsonLine(socket, {
221
- type: 'extension.response',
222
- response: message
244
+ })().catch((err) => {
245
+ console.error(
246
+ 'native-host: stdin message handler failed:',
247
+ err instanceof Error ? err.message : err
248
+ );
223
249
  });
224
- })().catch((err) => {
225
- console.error('native-host: stdin message handler failed:', err instanceof Error ? err.message : err);
226
- });
227
- });
250
+ },
251
+ () => {
252
+ socket.destroy();
253
+ }
254
+ );
228
255
  }
229
256
 
230
257
  /**
@@ -235,9 +262,12 @@ export async function runNativeHost({ socketPath = getSocketPath() } = {}) {
235
262
  * @param {() => void} [onTerminate]
236
263
  * @returns {void}
237
264
  */
238
- export function bindBridgeSocketLifecycle(socket, onTerminate = () => {
239
- process.exit(0);
240
- }) {
265
+ export function bindBridgeSocketLifecycle(
266
+ socket,
267
+ onTerminate = () => {
268
+ process.exit(0);
269
+ }
270
+ ) {
241
271
  let terminated = false;
242
272
 
243
273
  /**
@@ -257,28 +287,49 @@ export function bindBridgeSocketLifecycle(socket, onTerminate = () => {
257
287
  }
258
288
 
259
289
  /**
260
- * @param {string} socketPath
290
+ * @typedef {{
291
+ * connectSocketFn?: (transport: BridgeTransport) => Promise<net.Socket>,
292
+ * shouldBootstrapFn?: (error: unknown) => boolean,
293
+ * spawnBridgeDaemonFn?: () => void,
294
+ * delayFn?: (ms: number) => Promise<void>,
295
+ * maxAttempts?: number,
296
+ * }} ConnectWithBootstrapOptions
297
+ */
298
+
299
+ /**
300
+ * @param {BridgeTransport | string} transport
301
+ * @param {ConnectWithBootstrapOptions} [options]
261
302
  * @returns {Promise<net.Socket>}
262
303
  */
263
- async function connectWithBootstrap(socketPath) {
304
+ export async function connectWithBootstrap(transport, options = {}) {
305
+ const {
306
+ connectSocketFn = connectSocket,
307
+ shouldBootstrapFn = shouldBootstrap,
308
+ spawnBridgeDaemonFn = spawnBridgeDaemon,
309
+ delayFn = delay,
310
+ maxAttempts = 10,
311
+ } = options;
312
+ const resolvedTransport =
313
+ typeof transport === 'string' ? createSocketBridgeTransport(transport) : transport;
314
+
264
315
  try {
265
- return await connectSocket(socketPath);
316
+ return await connectSocketFn(resolvedTransport);
266
317
  } catch (error) {
267
- if (!shouldBootstrap(error)) {
318
+ if (!shouldBootstrapFn(error)) {
268
319
  throw error;
269
320
  }
270
321
  }
271
322
 
272
- spawnBridgeDaemon();
323
+ spawnBridgeDaemonFn();
273
324
 
274
325
  let lastError = null;
275
- for (let attempt = 0; attempt < 10; attempt += 1) {
276
- await delay(200);
326
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
327
+ await delayFn(200);
277
328
  try {
278
- return await connectSocket(socketPath);
329
+ return await connectSocketFn(resolvedTransport);
279
330
  } catch (error) {
280
331
  lastError = error;
281
- if (!shouldBootstrap(error)) {
332
+ if (!shouldBootstrapFn(error)) {
282
333
  throw error;
283
334
  }
284
335
  }
@@ -288,12 +339,15 @@ async function connectWithBootstrap(socketPath) {
288
339
  }
289
340
 
290
341
  /**
291
- * @param {string} socketPath
342
+ * @param {BridgeTransport} transport
292
343
  * @returns {Promise<net.Socket>}
293
344
  */
294
- function connectSocket(socketPath) {
345
+ function connectSocket(transport) {
295
346
  return new Promise((resolve, reject) => {
296
- const socket = net.createConnection(socketPath);
347
+ const socket =
348
+ transport.type === 'tcp'
349
+ ? net.createConnection({ host: transport.host, port: transport.port })
350
+ : net.createConnection(transport.socketPath);
297
351
  /**
298
352
  * @param {Error} error
299
353
  * @returns {void}
@@ -315,21 +369,19 @@ function connectSocket(socketPath) {
315
369
  * @returns {void}
316
370
  */
317
371
  function spawnBridgeDaemon() {
318
- const child = spawn(process.execPath, [daemonEntryPath], {
319
- detached: true,
320
- stdio: 'ignore'
321
- });
322
- child.unref();
372
+ spawnBridgeDaemonProcess();
323
373
  }
324
374
 
325
375
  /**
326
376
  * @param {unknown} error
327
377
  * @returns {boolean}
328
378
  */
329
- function shouldBootstrap(error) {
330
- return error instanceof Error
331
- && 'code' in error
332
- && (error.code === 'ENOENT' || error.code === 'ECONNREFUSED');
379
+ export function shouldBootstrap(error) {
380
+ return (
381
+ error instanceof Error &&
382
+ 'code' in error &&
383
+ (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')
384
+ );
333
385
  }
334
386
 
335
387
  /**
@@ -4,11 +4,7 @@
4
4
  /** @typedef {import('./types.js').BudgetOptions} BudgetOptions */
5
5
  /** @typedef {import('./types.js').TruncateResult} TruncateResult */
6
6
 
7
- import {
8
- DEFAULT_MAX_DEPTH,
9
- DEFAULT_MAX_NODES,
10
- DEFAULT_TEXT_BUDGET,
11
- } from './defaults.js';
7
+ import { DEFAULT_MAX_DEPTH, DEFAULT_MAX_NODES, DEFAULT_TEXT_BUDGET } from './defaults.js';
12
8
 
13
9
  /**
14
10
  * @param {BudgetOptions} [options={}]
@@ -20,7 +16,7 @@ export function applyBudget(options = {}) {
20
16
  maxDepth: clamp(options.maxDepth ?? DEFAULT_MAX_DEPTH, 1, 20),
21
17
  textBudget: clamp(options.textBudget ?? DEFAULT_TEXT_BUDGET, 32, 10000),
22
18
  includeBbox: options.includeBbox !== false,
23
- attributeAllowlist: normalizeList(options.attributeAllowlist)
19
+ attributeAllowlist: normalizeList(options.attributeAllowlist),
24
20
  };
25
21
  }
26
22
 
@@ -41,7 +37,7 @@ export function truncateText(value, budget) {
41
37
  return {
42
38
  value: `${value.slice(0, Math.max(0, budget - 1))}\u2026`,
43
39
  truncated: true,
44
- omitted: value.length - budget
40
+ omitted: value.length - budget,
45
41
  };
46
42
  }
47
43
 
@@ -17,10 +17,11 @@ export const CAPABILITIES = Object.freeze({
17
17
  CDP_DOM_SNAPSHOT: 'cdp.dom_snapshot',
18
18
  CDP_BOX_MODEL: 'cdp.box_model',
19
19
  CDP_STYLES: 'cdp.styles',
20
+ CDP_INPUT: 'cdp.input',
20
21
  AUTOMATION_INPUT: 'automation.input',
21
22
  TABS_MANAGE: 'tabs.manage',
22
23
  PERFORMANCE_READ: 'performance.read',
23
- NETWORK_READ: 'network.read'
24
+ NETWORK_READ: 'network.read',
24
25
  });
25
26
 
26
27
  export const DEFAULT_CAPABILITIES = Object.freeze([
@@ -38,9 +39,10 @@ export const DEFAULT_CAPABILITIES = Object.freeze([
38
39
  CAPABILITIES.CDP_DOM_SNAPSHOT,
39
40
  CAPABILITIES.CDP_BOX_MODEL,
40
41
  CAPABILITIES.CDP_STYLES,
42
+ CAPABILITIES.CDP_INPUT,
41
43
  CAPABILITIES.TABS_MANAGE,
42
44
  CAPABILITIES.PERFORMANCE_READ,
43
- CAPABILITIES.NETWORK_READ
45
+ CAPABILITIES.NETWORK_READ,
44
46
  ]);
45
47
 
46
48
  /** @type {Readonly<Record<CapabilityMethod, Capability | null>>} */
@@ -99,9 +101,10 @@ export const METHOD_CAPABILITIES = Object.freeze({
99
101
  'cdp.get_dom_snapshot': CAPABILITIES.CDP_DOM_SNAPSHOT,
100
102
  'cdp.get_box_model': CAPABILITIES.CDP_BOX_MODEL,
101
103
  'cdp.get_computed_styles_for_node': CAPABILITIES.CDP_STYLES,
104
+ 'cdp.dispatch_key_event': CAPABILITIES.CDP_INPUT,
102
105
  'performance.get_metrics': CAPABILITIES.PERFORMANCE_READ,
103
106
  'log.tail': null,
104
- 'health.ping': null
107
+ 'health.ping': null,
105
108
  });
106
109
 
107
110
  /**
@@ -65,6 +65,7 @@ export const DEBUGGER_BACKED_METHODS = new Set([
65
65
  'cdp.get_dom_snapshot',
66
66
  'cdp.get_box_model',
67
67
  'cdp.get_computed_styles_for_node',
68
+ 'cdp.dispatch_key_event',
68
69
  ]);
69
70
 
70
71
  /**
@@ -12,7 +12,7 @@ export const ERROR_CODES = Object.freeze({
12
12
  INVALID_REQUEST: 'INVALID_REQUEST',
13
13
  NATIVE_HOST_UNAVAILABLE: 'NATIVE_HOST_UNAVAILABLE',
14
14
  EXTENSION_DISCONNECTED: 'EXTENSION_DISCONNECTED',
15
- TIMEOUT: 'TIMEOUT'
15
+ TIMEOUT: 'TIMEOUT',
16
16
  });
17
17
 
18
18
  /**
@@ -23,47 +23,51 @@ export const ERROR_CODES = Object.freeze({
23
23
  export const ERROR_RECOVERY = Object.freeze({
24
24
  [ERROR_CODES.ACCESS_DENIED]: {
25
25
  retry: false,
26
- hint: 'Access is off for this window. Ask the user to click Enable in the Browser Bridge popup or side panel. Do not request access again until that window is enabled.'
26
+ hint: 'Access is off for this window. Ask the user to click Enable in the Browser Bridge popup or side panel. Do not request access again until that window is enabled.',
27
+ },
28
+ [ERROR_CODES.RESULT_TRUNCATED]: {
29
+ retry: false,
30
+ hint: 'Result was truncated to fit the response budget. Narrow the query or raise the relevant budget if more detail is required.',
27
31
  },
28
32
  [ERROR_CODES.ELEMENT_STALE]: {
29
33
  retry: false,
30
34
  alternativeMethod: 'dom.query',
31
- hint: 'Element was removed from the DOM. Re-query with the same selector to get a fresh elementRef.'
35
+ hint: 'Element was removed from the DOM. Re-query with the same selector to get a fresh elementRef.',
32
36
  },
33
37
  [ERROR_CODES.TAB_MISMATCH]: {
34
38
  retry: false,
35
39
  alternativeMethod: 'tabs.list',
36
- hint: 'Tab was closed or not found. Use tabs.list to find an available tab.'
40
+ hint: 'Tab was closed or not found. Use tabs.list to find an available tab.',
37
41
  },
38
42
  [ERROR_CODES.TIMEOUT]: {
39
43
  retry: true,
40
44
  retryAfterMs: 1000,
41
- hint: 'Operation exceeded the time limit. Retry once, or simplify the request (smaller maxNodes, narrower selector).'
45
+ hint: 'Operation exceeded the time limit. Retry once, or simplify the request (smaller maxNodes, narrower selector).',
42
46
  },
43
47
  [ERROR_CODES.RATE_LIMITED]: {
44
48
  retry: true,
45
49
  retryAfterMs: 2000,
46
- hint: 'Too many requests. Back off and retry after a short delay.'
50
+ hint: 'Too many requests. Back off and retry after a short delay.',
47
51
  },
48
52
  [ERROR_CODES.EXTENSION_DISCONNECTED]: {
49
53
  retry: true,
50
54
  retryAfterMs: 3000,
51
55
  alternativeMethod: 'health.ping',
52
- hint: 'Extension not connected. Check Chrome is running, then retry. Use health.ping to verify connectivity.'
56
+ hint: 'Extension not connected. Check Chrome is running, then retry. Use health.ping to verify connectivity.',
53
57
  },
54
58
  [ERROR_CODES.NATIVE_HOST_UNAVAILABLE]: {
55
59
  retry: false,
56
- hint: 'Native host not reachable. Run `bbx doctor` to diagnose the installation.'
60
+ hint: 'Native host not reachable. Run `bbx doctor` to diagnose the installation.',
57
61
  },
58
62
  [ERROR_CODES.INVALID_REQUEST]: {
59
63
  retry: false,
60
- hint: 'Malformed method or params. Check the method name and parameter types.'
64
+ hint: 'Malformed method or params. Check the method name and parameter types.',
61
65
  },
62
66
  [ERROR_CODES.INTERNAL_ERROR]: {
63
67
  retry: true,
64
68
  retryAfterMs: 1000,
65
- hint: 'Unexpected extension error. Retry once; if persistent, check page.get_console for details.'
66
- }
69
+ hint: 'Unexpected extension error. Retry once; if persistent, check page.get_console for details.',
70
+ },
67
71
  });
68
72
 
69
73
  /**
@@ -22,7 +22,9 @@ export function getUtf8ByteLength(value) {
22
22
  if (!value) {
23
23
  return 0;
24
24
  }
25
- return textEncoder.encode(value).length;
25
+ return typeof Buffer !== 'undefined'
26
+ ? Buffer.byteLength(value, 'utf8')
27
+ : textEncoder.encode(value).length;
26
28
  }
27
29
 
28
30
  /**
@@ -45,14 +47,25 @@ export function estimateSerializedPayloadCost(serialized) {
45
47
  }
46
48
 
47
49
  /**
48
- * Estimate cost for a JSON-serializable payload.
50
+ * Serialize a JSON payload for transport-oriented cost estimation.
49
51
  *
50
52
  * @param {unknown} value
51
- * @returns {PayloadCost}
53
+ * @returns {string}
52
54
  */
53
- export function estimateJsonPayloadCost(value) {
55
+ export function serializeJsonPayload(value) {
54
56
  if (typeof value === 'undefined') {
55
- return estimateSerializedPayloadCost('');
57
+ return '';
56
58
  }
57
- return estimateSerializedPayloadCost(JSON.stringify(value) ?? '');
59
+ return JSON.stringify(value) ?? '';
60
+ }
61
+
62
+ /**
63
+ * Estimate cost for a JSON-serializable payload.
64
+ *
65
+ * @param {unknown} value
66
+ * @param {string} [serialized]
67
+ * @returns {PayloadCost}
68
+ */
69
+ export function estimateJsonPayloadCost(value, serialized = serializeJsonPayload(value)) {
70
+ return estimateSerializedPayloadCost(serialized);
58
71
  }