@browserbridge/bbx 1.0.1 → 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 +4 -4
- package/package.json +11 -13
- package/packages/agent-client/src/cli-helpers.js +33 -0
- package/packages/agent-client/src/cli.js +116 -41
- package/packages/agent-client/src/client.js +29 -4
- package/packages/agent-client/src/command-registry.js +3 -0
- package/packages/agent-client/src/detect.js +159 -48
- package/packages/agent-client/src/install.js +24 -1
- package/packages/agent-client/src/mcp-config.js +29 -10
- package/packages/agent-client/src/setup-status.js +12 -4
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +28 -7
- package/packages/mcp-server/src/server.js +12 -2
- package/packages/native-host/bin/bridge-daemon.js +33 -4
- package/packages/native-host/bin/install-manifest.js +24 -2
- package/packages/native-host/src/config.js +131 -6
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +217 -68
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +121 -7
- package/packages/native-host/src/native-host.js +110 -73
- package/packages/protocol/src/capabilities.js +3 -0
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +4 -0
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +143 -7
- package/packages/protocol/src/registry.js +11 -0
- package/packages/protocol/src/summary.js +18 -10
- package/packages/protocol/src/types.js +28 -3
- package/skills/browser-bridge/SKILL.md +2 -1
- package/skills/browser-bridge/references/interaction.md +1 -0
- package/skills/browser-bridge/references/protocol.md +2 -1
- 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 -112
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/unpacked-extension.md +0 -72
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -38
- 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 -474
- package/packages/extension/src/background-routing.js +0 -89
- package/packages/extension/src/background.js +0 -3490
- package/packages/extension/src/content-script-helpers.js +0 -282
- package/packages/extension/src/content-script.js +0 -2043
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -104
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -298
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1771
- package/packages/extension/ui/ui.css +0 -1160
package/docs/usage-scenarios.md
DELETED
|
@@ -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
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
}
|