@browserbridge/bbx 1.0.1 → 1.2.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 (70) hide show
  1. package/README.md +4 -4
  2. package/package.json +11 -13
  3. package/packages/agent-client/src/cli-helpers.js +33 -0
  4. package/packages/agent-client/src/cli.js +122 -45
  5. package/packages/agent-client/src/client.js +134 -8
  6. package/packages/agent-client/src/command-registry.js +4 -1
  7. package/packages/agent-client/src/detect.js +159 -48
  8. package/packages/agent-client/src/install.js +24 -1
  9. package/packages/agent-client/src/mcp-config.js +29 -10
  10. package/packages/agent-client/src/setup-status.js +12 -4
  11. package/packages/mcp-server/src/bin.js +57 -5
  12. package/packages/mcp-server/src/handlers-capture.js +279 -0
  13. package/packages/mcp-server/src/handlers-dom.js +196 -0
  14. package/packages/mcp-server/src/handlers-navigation.js +79 -0
  15. package/packages/mcp-server/src/handlers-page.js +365 -0
  16. package/packages/mcp-server/src/handlers-utils.js +296 -0
  17. package/packages/mcp-server/src/handlers.js +63 -1159
  18. package/packages/mcp-server/src/server.js +13 -3
  19. package/packages/native-host/bin/bridge-daemon.js +34 -4
  20. package/packages/native-host/bin/install-manifest.js +32 -2
  21. package/packages/native-host/bin/postinstall.js +16 -0
  22. package/packages/native-host/src/config.js +131 -6
  23. package/packages/native-host/src/daemon-logger.js +157 -0
  24. package/packages/native-host/src/daemon-process.js +422 -0
  25. package/packages/native-host/src/daemon.js +322 -77
  26. package/packages/native-host/src/framing.js +131 -11
  27. package/packages/native-host/src/install-manifest.js +121 -7
  28. package/packages/native-host/src/native-host.js +110 -73
  29. package/packages/protocol/src/capabilities.js +4 -0
  30. package/packages/protocol/src/defaults.js +1 -0
  31. package/packages/protocol/src/errors.js +4 -0
  32. package/packages/protocol/src/payload-cost.js +19 -6
  33. package/packages/protocol/src/protocol.js +143 -7
  34. package/packages/protocol/src/registry.js +13 -0
  35. package/packages/protocol/src/summary.js +18 -10
  36. package/packages/protocol/src/types.js +28 -3
  37. package/skills/browser-bridge/SKILL.md +2 -1
  38. package/skills/browser-bridge/references/interaction.md +1 -0
  39. package/skills/browser-bridge/references/protocol.md +2 -1
  40. package/CHANGELOG.md +0 -55
  41. package/assets/banner.jpg +0 -0
  42. package/assets/logo.png +0 -0
  43. package/assets/logo.svg +0 -65
  44. package/docs/api-reference.md +0 -157
  45. package/docs/cli-guide.md +0 -128
  46. package/docs/index.md +0 -25
  47. package/docs/manual-setup.md +0 -140
  48. package/docs/mcp-vs-cli.md +0 -258
  49. package/docs/publishing.md +0 -112
  50. package/docs/quickstart.md +0 -104
  51. package/docs/troubleshooting.md +0 -59
  52. package/docs/unpacked-extension.md +0 -72
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -38
  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 -474
  60. package/packages/extension/src/background-routing.js +0 -89
  61. package/packages/extension/src/background.js +0 -3490
  62. package/packages/extension/src/content-script-helpers.js +0 -282
  63. package/packages/extension/src/content-script.js +0 -2043
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -104
  66. package/packages/extension/ui/popup.html +0 -35
  67. package/packages/extension/ui/popup.js +0 -298
  68. package/packages/extension/ui/sidepanel.html +0 -102
  69. package/packages/extension/ui/sidepanel.js +0 -1771
  70. package/packages/extension/ui/ui.css +0 -1160
@@ -1,136 +0,0 @@
1
- # Usage Scenarios
2
-
3
- Browser Bridge is best when you need the state that already exists in a real Chrome tab: logged-in sessions, seeded storage, feature flags, SPA state, and whatever the page actually rendered.
4
-
5
- If you need repeatable clean-room automation for tests or CI, use Playwright or another browser automation stack instead.
6
-
7
- ## 1. Debug a broken layout in the real page
8
-
9
- Use this when the bug only reproduces in your current logged-in or feature-flag state.
10
-
11
- Ask an MCP-capable agent:
12
-
13
- > Use Browser Bridge MCP to inspect why the sidebar overlaps the main content.
14
- >
15
- > If your client supports subagents, delegate the investigation to a smaller low-cost worker first.
16
-
17
- Ask a CLI-skill agent:
18
-
19
- > Use the browser-bridge skill to inspect the broken sidebar layout and tell me which computed styles are causing it.
20
-
21
- Useful direct commands:
22
-
23
- ```bash
24
- bbx batch '[{"method":"page.get_state"},{"method":"dom.query","params":{"selector":".sidebar","maxNodes":20,"maxDepth":4,"textBudget":600}}]'
25
- bbx dom-query .sidebar
26
- bbx box .sidebar
27
- bbx styles .sidebar display,position,width,left,right,gap
28
- bbx matched-rules .sidebar
29
- ```
30
-
31
- ## 2. Verify that a local code change actually rendered
32
-
33
- Use this after editing source, before assuming the fix worked.
34
-
35
- Typical prompt:
36
-
37
- > Check whether the latest navbar change rendered correctly in the browser, and compare the live spacing against the intended layout.
38
-
39
- Useful direct commands:
40
-
41
- ```bash
42
- bbx call page.get_state
43
- bbx dom-query nav
44
- bbx styles nav gap,padding,align-items
45
- bbx call input.scroll_into_view '{"target":{"selector":"nav"}}'
46
- bbx screenshot nav ./tmp/navbar.png
47
- ```
48
-
49
- ## 3. Debug a form or interaction issue
50
-
51
- Use this when a click, keyboard interaction, or form control does not behave as expected.
52
-
53
- Typical prompt:
54
-
55
- > Use Browser Bridge to inspect the submit button, confirm whether it is disabled or covered, then try the interaction again.
56
-
57
- Useful direct commands:
58
-
59
- ```bash
60
- bbx describe button[type="submit"]
61
- bbx box button[type="submit"]
62
- bbx call input.scroll_into_view '{"target":{"selector":"button[type=\"submit\"]"}}'
63
- bbx click button[type="submit"]
64
- bbx console error
65
- bbx network 20
66
- ```
67
-
68
- ## 4. Prove a live CSS or DOM fix before editing source
69
-
70
- Use this when you want to validate the fix in the page first, then port it into
71
- the codebase.
72
-
73
- Typical prompt:
74
-
75
- > Inspect the hero spacing, patch the live page until it looks correct, then tell me the minimal source change to make.
76
-
77
- Useful direct commands:
78
-
79
- ```bash
80
- bbx patch-style .hero gap=24px padding=32px
81
- bbx patch-text .hero-title "New heading"
82
- bbx patches
83
- bbx rollback <patchId>
84
- ```
85
-
86
- ## 5. Collect compact evidence instead of taking full screenshots
87
-
88
- Use this when you want token-efficient evidence for the agent.
89
-
90
- Typical prompt:
91
-
92
- > Read the visible page state, console errors, and the box model for the checkout summary without dumping the whole DOM.
93
-
94
- Useful direct commands:
95
-
96
- ```bash
97
- bbx call page.get_state
98
- bbx console all
99
- bbx network 20
100
- bbx box .checkout-summary
101
- bbx text .checkout-summary medium
102
- ```
103
-
104
- If `page.get_console` or `page.get_network` returns `dropped`, the page was
105
- noisy enough to evict older buffered entries. Narrow the repro and re-run the
106
- read before assuming you saw the full history.
107
-
108
- ## 6. Capture the whole document only when the page-level layout is the issue
109
-
110
- Use this when a bug spans multiple viewports and tight crops cannot express the
111
- problem.
112
-
113
- Typical prompt:
114
-
115
- > Capture the full page so we can verify how the header, hero, and footer line up across the whole document.
116
-
117
- Useful direct commands:
118
-
119
- ```bash
120
- bbx call input.scroll_into_view '{"target":{"selector":"main"}}'
121
- bbx call screenshot.capture_full_page '{}'
122
- ```
123
-
124
- ## 7. Drop to the raw protocol when shortcuts are not enough
125
-
126
- Use this when you need a method or parameter that the higher-level commands do
127
- not expose.
128
-
129
- ```bash
130
- bbx call page.get_state
131
- bbx call dom.query '{"selector":".card","maxNodes":10}'
132
- bbx batch '[{"method":"page.get_state"},{"method":"page.get_console","params":{"level":"error"}}]'
133
- ```
134
-
135
- For a command-oriented walkthrough, see [cli-guide.md](./cli-guide.md). For
136
- integration choices, see [mcp-vs-cli.md](./mcp-vs-cli.md).
package/manifest.json DELETED
@@ -1,38 +0,0 @@
1
- {
2
- "manifest_version": 3,
3
- "minimum_chrome_version": "114",
4
- "name": "Browser Bridge",
5
- "version": "1.0.0",
6
- "description": "Local bridge between your coding agent and browser. Structured access to DOM, styles, console, network, and reversible patches.",
7
- "icons": {
8
- "16": "packages/extension/assets/icon-16.png",
9
- "32": "packages/extension/assets/icon-32.png",
10
- "48": "packages/extension/assets/icon-48.png",
11
- "128": "packages/extension/assets/icon-128.png"
12
- },
13
- "permissions": [
14
- "alarms",
15
- "debugger",
16
- "nativeMessaging",
17
- "scripting",
18
- "sidePanel",
19
- "storage",
20
- "tabs"
21
- ],
22
- "host_permissions": ["<all_urls>"],
23
- "background": {
24
- "service_worker": "packages/extension/src/background.js",
25
- "type": "module"
26
- },
27
- "action": {
28
- "default_title": "Browser Bridge",
29
- "default_popup": "packages/extension/ui/popup.html",
30
- "default_icon": {
31
- "16": "packages/extension/assets/icon-16.png",
32
- "32": "packages/extension/assets/icon-32.png"
33
- }
34
- },
35
- "side_panel": {
36
- "default_path": "packages/extension/ui/sidepanel.html"
37
- }
38
- }
@@ -1,474 +0,0 @@
1
- // @ts-check
2
-
3
- import {
4
- ERROR_CODES,
5
- estimateJsonPayloadCost,
6
- getMethodCapability,
7
- getCostClass,
8
- getUtf8ByteLength,
9
- isDebuggerBackedMethod,
10
- } from '../../protocol/src/index.js';
11
-
12
- /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
13
- /** @typedef {import('../../protocol/src/types.js').Capability} Capability */
14
- /** @typedef {import('../../protocol/src/types.js').ErrorCode} ErrorCode */
15
-
16
- const INTERACTIVE_AX_ROLES = new Set([
17
- 'button',
18
- 'link',
19
- 'textbox',
20
- 'checkbox',
21
- 'radio',
22
- 'combobox',
23
- 'listbox',
24
- 'menuitem',
25
- 'tab',
26
- 'switch',
27
- 'slider',
28
- 'spinbutton',
29
- 'searchbox',
30
- 'menuitemcheckbox',
31
- 'menuitemradio',
32
- 'option',
33
- ]);
34
-
35
- /**
36
- * @param {chrome.tabs.Tab} tab
37
- * @param {string} method
38
- * @returns {{ method: string, tabId: number | null, windowId: number | null, url: string, title: string, status: string }}
39
- */
40
- export function summarizeTabResult(tab, method) {
41
- return {
42
- method,
43
- tabId: typeof tab.id === 'number' ? tab.id : null,
44
- windowId: typeof tab.windowId === 'number' ? tab.windowId : null,
45
- url: tab.url ?? '',
46
- title: tab.title ?? '',
47
- status: tab.status ?? 'unknown',
48
- };
49
- }
50
-
51
- /**
52
- * @param {unknown} prop
53
- * @returns {string}
54
- */
55
- export function axValue(prop) {
56
- if (!prop || typeof prop !== 'object') return '';
57
- const val = /** @type {{ value?: unknown }} */ (prop).value;
58
- return typeof val === 'string' ? val : '';
59
- }
60
-
61
- /**
62
- * @param {unknown} prop
63
- * @returns {boolean}
64
- */
65
- export function axBool(prop) {
66
- if (!prop || typeof prop !== 'object') return false;
67
- return /** @type {{ value?: unknown }} */ (prop).value === true;
68
- }
69
-
70
- /**
71
- * @param {unknown} prop
72
- * @returns {string | null}
73
- */
74
- export function axTristateValue(prop) {
75
- if (!prop || typeof prop !== 'object') return null;
76
- const val = /** @type {{ value?: unknown }} */ (prop).value;
77
- if (val === 'true' || val === true) return 'true';
78
- if (val === 'false' || val === false) return 'false';
79
- if (val === 'mixed') return 'mixed';
80
- return null;
81
- }
82
-
83
- /**
84
- * @param {Record<string, unknown>} node
85
- * @returns {{ nodeId: string, role: string, name: string, description: string, value: string, focused: boolean, required: boolean, checked: string | null, disabled: boolean, interactive: boolean, childIds: string[] }}
86
- */
87
- export function simplifyAXNode(node) {
88
- const role = axValue(node.role);
89
- return {
90
- nodeId: String(node.nodeId ?? ''),
91
- role,
92
- name: axValue(node.name),
93
- description: axValue(node.description),
94
- value: axValue(node.value),
95
- focused: axBool(node.focused),
96
- required: axBool(node.required),
97
- checked: axTristateValue(node.checked),
98
- disabled: axBool(node.disabled),
99
- interactive: INTERACTIVE_AX_ROLES.has(role) || axBool(node.focusable),
100
- childIds: Array.isArray(node.childIds) ? node.childIds.map(String) : [],
101
- };
102
- }
103
-
104
- /**
105
- * @param {string} method
106
- * @returns {boolean}
107
- */
108
- export function shouldLogAction(method) {
109
- return ![
110
- 'health.ping',
111
- 'log.tail',
112
- 'skill.get_runtime_context',
113
- 'setup.get_status',
114
- 'setup.install',
115
- 'tabs.list',
116
- ].includes(method);
117
- }
118
-
119
- /**
120
- * Treat page exceptions as part of the error stream so filtered reads return
121
- * runtime failures alongside explicit `console.error` calls.
122
- *
123
- * @param {string} requestedLevel
124
- * @param {string} entryLevel
125
- * @returns {boolean}
126
- */
127
- export function matchesConsoleLevel(requestedLevel, entryLevel) {
128
- if (requestedLevel === entryLevel) {
129
- return true;
130
- }
131
- if (requestedLevel === 'error') {
132
- return entryLevel === 'exception' || entryLevel === 'rejection';
133
- }
134
- return false;
135
- }
136
-
137
- /**
138
- * @param {BridgeResponse} response
139
- * @returns {string}
140
- */
141
- export function summarizeActionResult(response) {
142
- if (!response.ok) {
143
- return response.error.message;
144
- }
145
-
146
- const result =
147
- response.result && typeof response.result === 'object'
148
- ? /** @type {Record<string, unknown>} */ (response.result)
149
- : {};
150
-
151
- if (typeof result.patchId === 'string') {
152
- return `Patch ${result.patchId} applied.`;
153
- }
154
-
155
- if (Array.isArray(result.nodes)) {
156
- return `${result.nodes.length} node(s) returned.`;
157
- }
158
-
159
- if (typeof result.image === 'string') {
160
- return 'Partial screenshot captured.';
161
- }
162
-
163
- return 'Completed successfully.';
164
- }
165
-
166
- /**
167
- * Estimate approximate token cost from a bridge response.
168
- *
169
- * @param {BridgeResponse} response
170
- * @returns {{
171
- * responseBytes: number,
172
- * approxTokens: number,
173
- * textBytes: number,
174
- * textApproxTokens: number,
175
- * imageApproxTokens: number,
176
- * imageBytes: number,
177
- * hasScreenshot: boolean,
178
- * nodeCount: number | null
179
- * }}
180
- */
181
- export function estimateResponseTokens(response) {
182
- const payload = response.ok ? response.result : { error: response.error };
183
- const estimate = estimateJsonPayloadCost(payload);
184
- const responseBytes = estimate.bytes;
185
- const result =
186
- response.ok && response.result && typeof response.result === 'object'
187
- ? /** @type {Record<string, unknown>} */ (response.result)
188
- : null;
189
- const hasScreenshot = result != null && typeof result.image === 'string';
190
- const nodeCount = result != null && Array.isArray(result.nodes) ? result.nodes.length : null;
191
- const textPayload = hasScreenshot && result != null ? omitScreenshotImage(result) : payload;
192
- const textEstimate = estimateJsonPayloadCost(textPayload);
193
- const imageTransportBytes = Math.max(0, responseBytes - textEstimate.bytes);
194
- const imageBytes = hasScreenshot && result != null ? estimateInlineImageBytes(result.image) : 0;
195
-
196
- return {
197
- responseBytes,
198
- approxTokens: estimate.approxTokens,
199
- textBytes: textEstimate.bytes,
200
- textApproxTokens: textEstimate.approxTokens,
201
- imageApproxTokens: imageTransportBytes === 0 ? 0 : Math.ceil(imageTransportBytes / 4),
202
- imageBytes,
203
- hasScreenshot,
204
- nodeCount,
205
- };
206
- }
207
-
208
- /**
209
- * @param {string} method
210
- * @param {BridgeResponse} response
211
- * @returns {{
212
- * responseBytes: number,
213
- * approxTokens: number,
214
- * textBytes: number,
215
- * textApproxTokens: number,
216
- * imageApproxTokens: number,
217
- * imageBytes: number,
218
- * hasScreenshot: boolean,
219
- * nodeCount: number | null,
220
- * costClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
221
- * textCostClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
222
- * debuggerBacked: boolean
223
- * }}
224
- */
225
- export function getResponseDiagnostics(method, response) {
226
- const estimate = estimateResponseTokens(response);
227
- return {
228
- ...estimate,
229
- costClass: getCostClass(estimate.approxTokens),
230
- textCostClass: getCostClass(estimate.textApproxTokens),
231
- debuggerBacked: isDebuggerBackedMethod(method),
232
- };
233
- }
234
-
235
- /**
236
- * Keep screenshot metadata while excluding the large inline image payload from
237
- * token-oriented UI estimates.
238
- *
239
- * @param {Record<string, unknown>} result
240
- * @returns {Record<string, unknown>}
241
- */
242
- function omitScreenshotImage(result) {
243
- const textPayload = { ...result };
244
- delete textPayload.image;
245
- return textPayload;
246
- }
247
-
248
- /**
249
- * Estimate decoded image bytes for data URLs so the UI can show image size
250
- * without pretending the base64 blob is text-token traffic.
251
- *
252
- * @param {unknown} image
253
- * @returns {number}
254
- */
255
- function estimateInlineImageBytes(image) {
256
- if (typeof image !== 'string' || image.length === 0) {
257
- return 0;
258
- }
259
-
260
- const match = /^data:[^;]+;base64,([A-Za-z0-9+/=\s]+)$/u.exec(image);
261
- if (!match) {
262
- return getUtf8ByteLength(image);
263
- }
264
-
265
- const base64 = match[1].replace(/\s+/gu, '');
266
- if (base64.length === 0) {
267
- return 0;
268
- }
269
-
270
- const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
271
- return Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
272
- }
273
-
274
- /**
275
- * Deterministically trim oversized success payloads to fit within an
276
- * approximate token budget. This prefers shrinking large strings and slicing
277
- * top-level result arrays before falling back to a compact continuation payload.
278
- *
279
- * @param {string} method
280
- * @param {BridgeResponse} response
281
- * @param {number | null | undefined} tokenBudget
282
- * @returns {BridgeResponse}
283
- */
284
- export function enforceTokenBudget(method, response, tokenBudget) {
285
- if (
286
- !response.ok ||
287
- typeof tokenBudget !== 'number' ||
288
- !Number.isFinite(tokenBudget) ||
289
- tokenBudget <= 0
290
- ) {
291
- return response;
292
- }
293
-
294
- const maxBytes = Math.max(128, Math.floor(tokenBudget * 4));
295
- const responseBytes = estimateJsonPayloadCost(response.result).bytes;
296
- if (responseBytes <= maxBytes) {
297
- return {
298
- ...response,
299
- meta: {
300
- ...response.meta,
301
- budget_applied: false,
302
- budget_truncated: false,
303
- continuation_hint: null,
304
- },
305
- };
306
- }
307
-
308
- const cloned = cloneJsonValue(response.result);
309
- let truncated = false;
310
- let iterations = 0;
311
- const MAX_BUDGET_ITERATIONS = 100;
312
- while (
313
- estimateJsonPayloadCost(cloned).bytes > maxBytes &&
314
- shrinkForBudget(cloned) &&
315
- iterations < MAX_BUDGET_ITERATIONS
316
- ) {
317
- truncated = true;
318
- iterations += 1;
319
- }
320
-
321
- let result = cloned;
322
- if (estimateJsonPayloadCost(result).bytes > maxBytes) {
323
- result = {
324
- truncated: true,
325
- continuationHint: `Retry ${method} with a larger token budget or tighter params.`,
326
- };
327
- truncated = true;
328
- }
329
-
330
- return {
331
- ...response,
332
- result,
333
- meta: {
334
- ...response.meta,
335
- budget_applied: true,
336
- budget_truncated: truncated,
337
- continuation_hint: truncated
338
- ? `Retry ${method} with a larger token budget or tighter params.`
339
- : null,
340
- },
341
- };
342
- }
343
-
344
- /**
345
- * @param {unknown} value
346
- * @returns {any}
347
- */
348
- function cloneJsonValue(value) {
349
- return value == null ? value : JSON.parse(JSON.stringify(value));
350
- }
351
-
352
- /**
353
- * @param {any} value
354
- * @returns {boolean}
355
- */
356
- function shrinkForBudget(value) {
357
- if (!value || typeof value !== 'object') {
358
- return false;
359
- }
360
-
361
- if (Array.isArray(value)) {
362
- if (value.length > 1) {
363
- const nextLength = Math.max(1, Math.floor(value.length * 0.75));
364
- value.splice(nextLength);
365
- return true;
366
- }
367
- return value.length === 1 ? shrinkForBudget(value[0]) : false;
368
- }
369
-
370
- for (const key of ['image', 'html', 'text', 'value']) {
371
- if (typeof value[key] === 'string' && value[key].length > 64) {
372
- value[key] =
373
- key === 'image'
374
- ? '[omitted image over token budget]'
375
- : `${value[key].slice(0, Math.max(32, Math.floor(value[key].length * 0.75) - 1))}\u2026`;
376
- if (typeof value.truncated !== 'boolean') {
377
- value.truncated = true;
378
- }
379
- return true;
380
- }
381
- }
382
-
383
- for (const key of ['nodes', 'entries', 'tabs', 'patches']) {
384
- if (Array.isArray(value[key]) && value[key].length > 1) {
385
- const originalLength = value[key].length;
386
- const nextLength = Math.max(1, Math.floor(originalLength * 0.75));
387
- value[key].splice(nextLength);
388
- if (typeof value.count !== 'number') {
389
- value.count = originalLength;
390
- }
391
- if (typeof value.total !== 'number') {
392
- value.total = originalLength;
393
- }
394
- value.truncated = true;
395
- return true;
396
- }
397
- }
398
-
399
- for (const entry of Object.values(value)) {
400
- if (shrinkForBudget(entry)) {
401
- if (typeof value.truncated !== 'boolean') {
402
- value.truncated = true;
403
- }
404
- return true;
405
- }
406
- }
407
-
408
- const keys = Object.keys(value);
409
- if (keys.length > 2) {
410
- delete value[keys[keys.length - 1]];
411
- value.truncated = true;
412
- return true;
413
- }
414
-
415
- return false;
416
- }
417
-
418
- /**
419
- * @param {unknown} error
420
- * @returns {string}
421
- */
422
- export function getErrorMessage(error) {
423
- if (typeof error === 'string') {
424
- return error;
425
- }
426
- if (error instanceof Error) {
427
- return error.message;
428
- }
429
- return 'Unexpected extension error.';
430
- }
431
-
432
- /**
433
- * @param {string} message
434
- * @returns {string}
435
- */
436
- export function normalizeRuntimeErrorMessage(message) {
437
- return /^No tab with id[: ]/i.test(message) ? ERROR_CODES.TAB_MISMATCH : message;
438
- }
439
-
440
- /**
441
- * @param {{ x?: number, y?: number, width?: number, height?: number, scale?: number }} [rect={}]
442
- * @returns {{ x: number, y: number, width: number, height: number }}
443
- */
444
- export function normalizeCropRect(rect = {}) {
445
- const scale = Number(rect.scale) || 1;
446
- return {
447
- x: Math.max(0, Math.round((rect.x || 0) * scale)),
448
- y: Math.max(0, Math.round((rect.y || 0) * scale)),
449
- width: Math.max(1, Math.round((rect.width || 1) * scale)),
450
- height: Math.max(1, Math.round((rect.height || 1) * scale)),
451
- };
452
- }
453
-
454
- /**
455
- * @param {string} url
456
- * @returns {string}
457
- */
458
- export function safeOrigin(url) {
459
- try {
460
- return new URL(url).origin;
461
- } catch {
462
- return '';
463
- }
464
- }
465
-
466
- /**
467
- * @param {string} method
468
- * @returns {Capability | null}
469
- */
470
- export function inferCapability(method) {
471
- return getMethodCapability(
472
- /** @type {import('../../protocol/src/types.js').BridgeMethod} */ (method)
473
- );
474
- }