@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.
- package/README.md +6 -4
- package/package.json +53 -53
- package/packages/agent-client/src/cli-helpers.js +43 -5
- package/packages/agent-client/src/cli.js +176 -171
- package/packages/agent-client/src/client.js +66 -21
- package/packages/agent-client/src/command-registry.js +104 -69
- package/packages/agent-client/src/detect.js +162 -54
- package/packages/agent-client/src/install.js +34 -28
- package/packages/agent-client/src/mcp-config.js +40 -40
- package/packages/agent-client/src/runtime.js +41 -20
- package/packages/agent-client/src/setup-status.js +23 -30
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +573 -256
- package/packages/mcp-server/src/server.js +568 -257
- package/packages/native-host/bin/bridge-daemon.js +39 -6
- package/packages/native-host/bin/install-manifest.js +26 -4
- package/packages/native-host/bin/postinstall.js +4 -2
- package/packages/native-host/src/config.js +142 -13
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +350 -150
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +194 -29
- package/packages/native-host/src/native-host.js +154 -102
- package/packages/protocol/src/budget.js +3 -7
- package/packages/protocol/src/capabilities.js +6 -3
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +15 -11
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +242 -73
- package/packages/protocol/src/registry.js +311 -45
- package/packages/protocol/src/summary.js +260 -109
- package/packages/protocol/src/types.js +29 -4
- package/skills/browser-bridge/SKILL.md +3 -2
- package/skills/browser-bridge/agents/openai.yaml +3 -3
- package/skills/browser-bridge/references/interaction.md +34 -11
- package/skills/browser-bridge/references/patch-workflow.md +3 -0
- package/skills/browser-bridge/references/protocol.md +127 -71
- package/skills/browser-bridge/references/tailwind.md +12 -11
- package/skills/browser-bridge/references/token-efficiency.md +23 -22
- package/skills/browser-bridge/references/ui-workflows.md +8 -0
- package/CHANGELOG.md +0 -55
- package/assets/banner.jpg +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -65
- package/docs/api-reference.md +0 -157
- package/docs/cli-guide.md +0 -128
- package/docs/index.md +0 -25
- package/docs/manual-setup.md +0 -140
- package/docs/mcp-vs-cli.md +0 -258
- package/docs/publishing.md +0 -114
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -52
- package/packages/extension/assets/icon-128.png +0 -0
- package/packages/extension/assets/icon-16.png +0 -0
- package/packages/extension/assets/icon-32.png +0 -0
- package/packages/extension/assets/icon-48.png +0 -0
- package/packages/extension/src/background-helpers.js +0 -459
- package/packages/extension/src/background-routing.js +0 -91
- package/packages/extension/src/background.js +0 -3227
- package/packages/extension/src/content-script-helpers.js +0 -281
- package/packages/extension/src/content-script.js +0 -1977
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -102
- package/packages/extension/ui/offscreen.html +0 -6
- package/packages/extension/ui/offscreen.js +0 -61
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -279
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1854
- package/packages/extension/ui/ui.css +0 -1159
package/manifest.json
DELETED
|
@@ -1,52 +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
|
-
"activeTab",
|
|
15
|
-
"alarms",
|
|
16
|
-
"debugger",
|
|
17
|
-
"nativeMessaging",
|
|
18
|
-
"scripting",
|
|
19
|
-
"sidePanel",
|
|
20
|
-
"storage",
|
|
21
|
-
"tabs",
|
|
22
|
-
"offscreen"
|
|
23
|
-
],
|
|
24
|
-
"host_permissions": [
|
|
25
|
-
"<all_urls>"
|
|
26
|
-
],
|
|
27
|
-
"background": {
|
|
28
|
-
"service_worker": "packages/extension/src/background.js",
|
|
29
|
-
"type": "module"
|
|
30
|
-
},
|
|
31
|
-
"action": {
|
|
32
|
-
"default_title": "Browser Bridge",
|
|
33
|
-
"default_popup": "packages/extension/ui/popup.html",
|
|
34
|
-
"default_icon": {
|
|
35
|
-
"16": "packages/extension/assets/icon-16.png",
|
|
36
|
-
"32": "packages/extension/assets/icon-32.png"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
"side_panel": {
|
|
40
|
-
"default_path": "packages/extension/ui/sidepanel.html"
|
|
41
|
-
},
|
|
42
|
-
"web_accessible_resources": [
|
|
43
|
-
{
|
|
44
|
-
"resources": [
|
|
45
|
-
"packages/extension/ui/ui.css"
|
|
46
|
-
],
|
|
47
|
-
"matches": [
|
|
48
|
-
"<all_urls>"
|
|
49
|
-
]
|
|
50
|
-
}
|
|
51
|
-
]
|
|
52
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,459 +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', 'link', 'textbox', 'checkbox', 'radio', 'combobox',
|
|
18
|
-
'listbox', 'menuitem', 'tab', 'switch', 'slider', 'spinbutton',
|
|
19
|
-
'searchbox', 'menuitemcheckbox', 'menuitemradio', 'option'
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {chrome.tabs.Tab} tab
|
|
24
|
-
* @param {string} method
|
|
25
|
-
* @returns {{ method: string, tabId: number | null, windowId: number | null, url: string, title: string, status: string }}
|
|
26
|
-
*/
|
|
27
|
-
export function summarizeTabResult(tab, method) {
|
|
28
|
-
return {
|
|
29
|
-
method,
|
|
30
|
-
tabId: typeof tab.id === 'number' ? tab.id : null,
|
|
31
|
-
windowId: typeof tab.windowId === 'number' ? tab.windowId : null,
|
|
32
|
-
url: tab.url ?? '',
|
|
33
|
-
title: tab.title ?? '',
|
|
34
|
-
status: tab.status ?? 'unknown'
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {unknown} prop
|
|
40
|
-
* @returns {string}
|
|
41
|
-
*/
|
|
42
|
-
export function axValue(prop) {
|
|
43
|
-
if (!prop || typeof prop !== 'object') return '';
|
|
44
|
-
const val = /** @type {{ value?: unknown }} */ (prop).value;
|
|
45
|
-
return typeof val === 'string' ? val : '';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {unknown} prop
|
|
50
|
-
* @returns {boolean}
|
|
51
|
-
*/
|
|
52
|
-
export function axBool(prop) {
|
|
53
|
-
if (!prop || typeof prop !== 'object') return false;
|
|
54
|
-
return /** @type {{ value?: unknown }} */ (prop).value === true;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @param {unknown} prop
|
|
59
|
-
* @returns {string | null}
|
|
60
|
-
*/
|
|
61
|
-
export function axTristateValue(prop) {
|
|
62
|
-
if (!prop || typeof prop !== 'object') return null;
|
|
63
|
-
const val = /** @type {{ value?: unknown }} */ (prop).value;
|
|
64
|
-
if (val === 'true' || val === true) return 'true';
|
|
65
|
-
if (val === 'false' || val === false) return 'false';
|
|
66
|
-
if (val === 'mixed') return 'mixed';
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @param {Record<string, unknown>} node
|
|
72
|
-
* @returns {{ nodeId: string, role: string, name: string, description: string, value: string, focused: boolean, required: boolean, checked: string | null, disabled: boolean, interactive: boolean, childIds: string[] }}
|
|
73
|
-
*/
|
|
74
|
-
export function simplifyAXNode(node) {
|
|
75
|
-
const role = axValue(node.role);
|
|
76
|
-
return {
|
|
77
|
-
nodeId: String(node.nodeId ?? ''),
|
|
78
|
-
role,
|
|
79
|
-
name: axValue(node.name),
|
|
80
|
-
description: axValue(node.description),
|
|
81
|
-
value: axValue(node.value),
|
|
82
|
-
focused: axBool(node.focused),
|
|
83
|
-
required: axBool(node.required),
|
|
84
|
-
checked: axTristateValue(node.checked),
|
|
85
|
-
disabled: axBool(node.disabled),
|
|
86
|
-
interactive: INTERACTIVE_AX_ROLES.has(role) || axBool(node.focusable),
|
|
87
|
-
childIds: Array.isArray(node.childIds) ? node.childIds.map(String) : []
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @param {string} method
|
|
93
|
-
* @returns {boolean}
|
|
94
|
-
*/
|
|
95
|
-
export function shouldLogAction(method) {
|
|
96
|
-
return ![
|
|
97
|
-
'health.ping',
|
|
98
|
-
'log.tail',
|
|
99
|
-
'skill.get_runtime_context',
|
|
100
|
-
'setup.get_status',
|
|
101
|
-
'setup.install',
|
|
102
|
-
'tabs.list'
|
|
103
|
-
].includes(method);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Treat page exceptions as part of the error stream so filtered reads return
|
|
108
|
-
* runtime failures alongside explicit `console.error` calls.
|
|
109
|
-
*
|
|
110
|
-
* @param {string} requestedLevel
|
|
111
|
-
* @param {string} entryLevel
|
|
112
|
-
* @returns {boolean}
|
|
113
|
-
*/
|
|
114
|
-
export function matchesConsoleLevel(requestedLevel, entryLevel) {
|
|
115
|
-
if (requestedLevel === entryLevel) {
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
if (requestedLevel === 'error') {
|
|
119
|
-
return entryLevel === 'exception' || entryLevel === 'rejection';
|
|
120
|
-
}
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* @param {BridgeResponse} response
|
|
126
|
-
* @returns {string}
|
|
127
|
-
*/
|
|
128
|
-
export function summarizeActionResult(response) {
|
|
129
|
-
if (!response.ok) {
|
|
130
|
-
return response.error.message;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const result = response.result && typeof response.result === 'object'
|
|
134
|
-
? /** @type {Record<string, unknown>} */ (response.result)
|
|
135
|
-
: {};
|
|
136
|
-
|
|
137
|
-
if (typeof result.patchId === 'string') {
|
|
138
|
-
return `Patch ${result.patchId} applied.`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (Array.isArray(result.nodes)) {
|
|
142
|
-
return `${result.nodes.length} node(s) returned.`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (typeof result.image === 'string') {
|
|
146
|
-
return 'Partial screenshot captured.';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return 'Completed successfully.';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Estimate approximate token cost from a bridge response.
|
|
154
|
-
*
|
|
155
|
-
* @param {BridgeResponse} response
|
|
156
|
-
* @returns {{
|
|
157
|
-
* responseBytes: number,
|
|
158
|
-
* approxTokens: number,
|
|
159
|
-
* textBytes: number,
|
|
160
|
-
* textApproxTokens: number,
|
|
161
|
-
* imageApproxTokens: number,
|
|
162
|
-
* imageBytes: number,
|
|
163
|
-
* hasScreenshot: boolean,
|
|
164
|
-
* nodeCount: number | null
|
|
165
|
-
* }}
|
|
166
|
-
*/
|
|
167
|
-
export function estimateResponseTokens(response) {
|
|
168
|
-
const payload = response.ok
|
|
169
|
-
? response.result
|
|
170
|
-
: { error: response.error };
|
|
171
|
-
const estimate = estimateJsonPayloadCost(payload);
|
|
172
|
-
const responseBytes = estimate.bytes;
|
|
173
|
-
const result = response.ok && response.result && typeof response.result === 'object'
|
|
174
|
-
? /** @type {Record<string, unknown>} */ (response.result)
|
|
175
|
-
: null;
|
|
176
|
-
const hasScreenshot = result != null && typeof result.image === 'string';
|
|
177
|
-
const nodeCount = result != null && Array.isArray(result.nodes) ? result.nodes.length : null;
|
|
178
|
-
const textPayload = hasScreenshot && result != null
|
|
179
|
-
? omitScreenshotImage(result)
|
|
180
|
-
: payload;
|
|
181
|
-
const textEstimate = estimateJsonPayloadCost(textPayload);
|
|
182
|
-
const imageTransportBytes = Math.max(0, responseBytes - textEstimate.bytes);
|
|
183
|
-
const imageBytes = hasScreenshot && result != null
|
|
184
|
-
? estimateInlineImageBytes(result.image)
|
|
185
|
-
: 0;
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
responseBytes,
|
|
189
|
-
approxTokens: estimate.approxTokens,
|
|
190
|
-
textBytes: textEstimate.bytes,
|
|
191
|
-
textApproxTokens: textEstimate.approxTokens,
|
|
192
|
-
imageApproxTokens: imageTransportBytes === 0 ? 0 : Math.ceil(imageTransportBytes / 4),
|
|
193
|
-
imageBytes,
|
|
194
|
-
hasScreenshot,
|
|
195
|
-
nodeCount,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* @param {string} method
|
|
201
|
-
* @param {BridgeResponse} response
|
|
202
|
-
* @returns {{
|
|
203
|
-
* responseBytes: number,
|
|
204
|
-
* approxTokens: number,
|
|
205
|
-
* textBytes: number,
|
|
206
|
-
* textApproxTokens: number,
|
|
207
|
-
* imageApproxTokens: number,
|
|
208
|
-
* imageBytes: number,
|
|
209
|
-
* hasScreenshot: boolean,
|
|
210
|
-
* nodeCount: number | null,
|
|
211
|
-
* costClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
|
|
212
|
-
* textCostClass: 'cheap' | 'moderate' | 'heavy' | 'extreme',
|
|
213
|
-
* debuggerBacked: boolean
|
|
214
|
-
* }}
|
|
215
|
-
*/
|
|
216
|
-
export function getResponseDiagnostics(method, response) {
|
|
217
|
-
const estimate = estimateResponseTokens(response);
|
|
218
|
-
return {
|
|
219
|
-
...estimate,
|
|
220
|
-
costClass: getCostClass(estimate.approxTokens),
|
|
221
|
-
textCostClass: getCostClass(estimate.textApproxTokens),
|
|
222
|
-
debuggerBacked: isDebuggerBackedMethod(method),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Keep screenshot metadata while excluding the large inline image payload from
|
|
228
|
-
* token-oriented UI estimates.
|
|
229
|
-
*
|
|
230
|
-
* @param {Record<string, unknown>} result
|
|
231
|
-
* @returns {Record<string, unknown>}
|
|
232
|
-
*/
|
|
233
|
-
function omitScreenshotImage(result) {
|
|
234
|
-
const textPayload = { ...result };
|
|
235
|
-
delete textPayload.image;
|
|
236
|
-
return textPayload;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Estimate decoded image bytes for data URLs so the UI can show image size
|
|
241
|
-
* without pretending the base64 blob is text-token traffic.
|
|
242
|
-
*
|
|
243
|
-
* @param {unknown} image
|
|
244
|
-
* @returns {number}
|
|
245
|
-
*/
|
|
246
|
-
function estimateInlineImageBytes(image) {
|
|
247
|
-
if (typeof image !== 'string' || image.length === 0) {
|
|
248
|
-
return 0;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const match = /^data:[^;]+;base64,([A-Za-z0-9+/=\s]+)$/u.exec(image);
|
|
252
|
-
if (!match) {
|
|
253
|
-
return getUtf8ByteLength(image);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const base64 = match[1].replace(/\s+/gu, '');
|
|
257
|
-
if (base64.length === 0) {
|
|
258
|
-
return 0;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const padding = base64.endsWith('==')
|
|
262
|
-
? 2
|
|
263
|
-
: base64.endsWith('=')
|
|
264
|
-
? 1
|
|
265
|
-
: 0;
|
|
266
|
-
return Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Deterministically trim oversized success payloads to fit within an
|
|
271
|
-
* approximate token budget. This prefers shrinking large strings and slicing
|
|
272
|
-
* top-level result arrays before falling back to a compact continuation payload.
|
|
273
|
-
*
|
|
274
|
-
* @param {string} method
|
|
275
|
-
* @param {BridgeResponse} response
|
|
276
|
-
* @param {number | null | undefined} tokenBudget
|
|
277
|
-
* @returns {BridgeResponse}
|
|
278
|
-
*/
|
|
279
|
-
export function enforceTokenBudget(method, response, tokenBudget) {
|
|
280
|
-
if (!response.ok || typeof tokenBudget !== 'number' || !Number.isFinite(tokenBudget) || tokenBudget <= 0) {
|
|
281
|
-
return response;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const maxBytes = Math.max(128, Math.floor(tokenBudget * 4));
|
|
285
|
-
const responseBytes = estimateJsonPayloadCost(response.result).bytes;
|
|
286
|
-
if (responseBytes <= maxBytes) {
|
|
287
|
-
return {
|
|
288
|
-
...response,
|
|
289
|
-
meta: {
|
|
290
|
-
...response.meta,
|
|
291
|
-
budget_applied: false,
|
|
292
|
-
budget_truncated: false,
|
|
293
|
-
continuation_hint: null,
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const cloned = cloneJsonValue(response.result);
|
|
299
|
-
let truncated = false;
|
|
300
|
-
let iterations = 0;
|
|
301
|
-
const MAX_BUDGET_ITERATIONS = 100;
|
|
302
|
-
while (estimateJsonPayloadCost(cloned).bytes > maxBytes && shrinkForBudget(cloned) && iterations < MAX_BUDGET_ITERATIONS) {
|
|
303
|
-
truncated = true;
|
|
304
|
-
iterations += 1;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
let result = cloned;
|
|
308
|
-
if (estimateJsonPayloadCost(result).bytes > maxBytes) {
|
|
309
|
-
result = {
|
|
310
|
-
truncated: true,
|
|
311
|
-
continuationHint: `Retry ${method} with a larger token budget or tighter params.`,
|
|
312
|
-
};
|
|
313
|
-
truncated = true;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
...response,
|
|
318
|
-
result,
|
|
319
|
-
meta: {
|
|
320
|
-
...response.meta,
|
|
321
|
-
budget_applied: true,
|
|
322
|
-
budget_truncated: truncated,
|
|
323
|
-
continuation_hint: truncated
|
|
324
|
-
? `Retry ${method} with a larger token budget or tighter params.`
|
|
325
|
-
: null,
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* @param {unknown} value
|
|
332
|
-
* @returns {any}
|
|
333
|
-
*/
|
|
334
|
-
function cloneJsonValue(value) {
|
|
335
|
-
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* @param {any} value
|
|
340
|
-
* @returns {boolean}
|
|
341
|
-
*/
|
|
342
|
-
function shrinkForBudget(value) {
|
|
343
|
-
if (!value || typeof value !== 'object') {
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (Array.isArray(value)) {
|
|
348
|
-
if (value.length > 1) {
|
|
349
|
-
const nextLength = Math.max(1, Math.floor(value.length * 0.75));
|
|
350
|
-
value.splice(nextLength);
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
|
-
return value.length === 1 ? shrinkForBudget(value[0]) : false;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
for (const key of ['image', 'html', 'text', 'value']) {
|
|
357
|
-
if (typeof value[key] === 'string' && value[key].length > 64) {
|
|
358
|
-
value[key] = key === 'image'
|
|
359
|
-
? '[omitted image over token budget]'
|
|
360
|
-
: `${value[key].slice(0, Math.max(32, Math.floor(value[key].length * 0.75) - 1))}\u2026`;
|
|
361
|
-
if (typeof value.truncated !== 'boolean') {
|
|
362
|
-
value.truncated = true;
|
|
363
|
-
}
|
|
364
|
-
return true;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
for (const key of ['nodes', 'entries', 'tabs', 'patches']) {
|
|
369
|
-
if (Array.isArray(value[key]) && value[key].length > 1) {
|
|
370
|
-
const originalLength = value[key].length;
|
|
371
|
-
const nextLength = Math.max(1, Math.floor(originalLength * 0.75));
|
|
372
|
-
value[key].splice(nextLength);
|
|
373
|
-
if (typeof value.count !== 'number') {
|
|
374
|
-
value.count = originalLength;
|
|
375
|
-
}
|
|
376
|
-
if (typeof value.total !== 'number') {
|
|
377
|
-
value.total = originalLength;
|
|
378
|
-
}
|
|
379
|
-
value.truncated = true;
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
for (const entry of Object.values(value)) {
|
|
385
|
-
if (shrinkForBudget(entry)) {
|
|
386
|
-
if (typeof value.truncated !== 'boolean') {
|
|
387
|
-
value.truncated = true;
|
|
388
|
-
}
|
|
389
|
-
return true;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const keys = Object.keys(value);
|
|
394
|
-
if (keys.length > 2) {
|
|
395
|
-
delete value[keys[keys.length - 1]];
|
|
396
|
-
value.truncated = true;
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* @param {unknown} error
|
|
405
|
-
* @returns {string}
|
|
406
|
-
*/
|
|
407
|
-
export function getErrorMessage(error) {
|
|
408
|
-
if (typeof error === 'string') {
|
|
409
|
-
return error;
|
|
410
|
-
}
|
|
411
|
-
if (error instanceof Error) {
|
|
412
|
-
return error.message;
|
|
413
|
-
}
|
|
414
|
-
return 'Unexpected extension error.';
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* @param {string} message
|
|
419
|
-
* @returns {string}
|
|
420
|
-
*/
|
|
421
|
-
export function normalizeRuntimeErrorMessage(message) {
|
|
422
|
-
return /^No tab with id[: ]/i.test(message)
|
|
423
|
-
? ERROR_CODES.TAB_MISMATCH
|
|
424
|
-
: message;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* @param {{ x?: number, y?: number, width?: number, height?: number, scale?: number }} [rect={}]
|
|
429
|
-
* @returns {{ x: number, y: number, width: number, height: number }}
|
|
430
|
-
*/
|
|
431
|
-
export function normalizeCropRect(rect = {}) {
|
|
432
|
-
const scale = Number(rect.scale) || 1;
|
|
433
|
-
return {
|
|
434
|
-
x: Math.max(0, Math.round((rect.x || 0) * scale)),
|
|
435
|
-
y: Math.max(0, Math.round((rect.y || 0) * scale)),
|
|
436
|
-
width: Math.max(1, Math.round((rect.width || 1) * scale)),
|
|
437
|
-
height: Math.max(1, Math.round((rect.height || 1) * scale))
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* @param {string} url
|
|
443
|
-
* @returns {string}
|
|
444
|
-
*/
|
|
445
|
-
export function safeOrigin(url) {
|
|
446
|
-
try {
|
|
447
|
-
return new URL(url).origin;
|
|
448
|
-
} catch {
|
|
449
|
-
return '';
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* @param {string} method
|
|
455
|
-
* @returns {Capability | null}
|
|
456
|
-
*/
|
|
457
|
-
export function inferCapability(method) {
|
|
458
|
-
return getMethodCapability(/** @type {import('../../protocol/src/types.js').BridgeMethod} */ (method));
|
|
459
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
import { ERROR_CODES } from '../../protocol/src/index.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {{
|
|
7
|
-
* tabId: number,
|
|
8
|
-
* windowId: number,
|
|
9
|
-
* title: string,
|
|
10
|
-
* url: string
|
|
11
|
-
* }} ResolvedTabTarget
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {string} url
|
|
16
|
-
* @returns {boolean}
|
|
17
|
-
*/
|
|
18
|
-
export function isRestrictedAutomationUrl(url) {
|
|
19
|
-
return /^(about:|chrome:|chrome-extension:|chrome-search:|devtools:|edge:|brave:|moz-extension:|view-source:)/i.test(url);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {number | null | undefined} requestTabId
|
|
24
|
-
* @param {chrome.tabs.Tab | null | undefined} explicitTab
|
|
25
|
-
* @param {chrome.tabs.Tab | null | undefined} activeTab
|
|
26
|
-
* @returns {chrome.tabs.Tab | null}
|
|
27
|
-
*/
|
|
28
|
-
export function selectRequestTabCandidate(requestTabId, explicitTab, activeTab) {
|
|
29
|
-
if (typeof requestTabId === 'number' && Number.isFinite(requestTabId)) {
|
|
30
|
-
return explicitTab ?? null;
|
|
31
|
-
}
|
|
32
|
-
return activeTab ?? null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @param {chrome.tabs.Tab | null | undefined} tab
|
|
37
|
-
* @param {number} enabledWindowId
|
|
38
|
-
* @param {{ requireScriptable?: boolean }} [options]
|
|
39
|
-
* @returns {ResolvedTabTarget}
|
|
40
|
-
*/
|
|
41
|
-
export function resolveWindowScopedTab(tab, enabledWindowId, options = {}) {
|
|
42
|
-
const requireScriptable = options.requireScriptable !== false;
|
|
43
|
-
if (
|
|
44
|
-
typeof tab?.id !== 'number'
|
|
45
|
-
|| !Number.isFinite(tab.id)
|
|
46
|
-
|| typeof tab.windowId !== 'number'
|
|
47
|
-
) {
|
|
48
|
-
throw new Error(ERROR_CODES.TAB_MISMATCH);
|
|
49
|
-
}
|
|
50
|
-
if (tab.windowId !== enabledWindowId) {
|
|
51
|
-
throw new Error(ERROR_CODES.ACCESS_DENIED);
|
|
52
|
-
}
|
|
53
|
-
if (typeof tab.url !== 'string' || !tab.url) {
|
|
54
|
-
throw new Error(ERROR_CODES.TAB_MISMATCH);
|
|
55
|
-
}
|
|
56
|
-
if (requireScriptable && isRestrictedAutomationUrl(tab.url)) {
|
|
57
|
-
throw new Error(ERROR_CODES.ACCESS_DENIED);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
tabId: tab.id,
|
|
62
|
-
windowId: tab.windowId,
|
|
63
|
-
title: tab.title ?? '',
|
|
64
|
-
url: tab.url
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @param {chrome.tabs.Tab | null | undefined} tab
|
|
70
|
-
* @returns {ResolvedTabTarget | null}
|
|
71
|
-
*/
|
|
72
|
-
export function normalizeRequestedAccessTab(tab) {
|
|
73
|
-
if (
|
|
74
|
-
typeof tab?.id !== 'number'
|
|
75
|
-
|| !Number.isFinite(tab.id)
|
|
76
|
-
|| typeof tab.windowId !== 'number'
|
|
77
|
-
|| typeof tab.url !== 'string'
|
|
78
|
-
|| !tab.url
|
|
79
|
-
) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
if (isRestrictedAutomationUrl(tab.url)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
tabId: tab.id,
|
|
87
|
-
windowId: tab.windowId,
|
|
88
|
-
title: tab.title ?? '',
|
|
89
|
-
url: tab.url
|
|
90
|
-
};
|
|
91
|
-
}
|