@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
@@ -0,0 +1,279 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ dispatchToolAction,
5
+ getToolTokenBudget,
6
+ REQUEST_SOURCE,
7
+ requestBridge,
8
+ resolveToolRef,
9
+ resolveRef,
10
+ summarizeToolError,
11
+ summarizeToolResponse,
12
+ withToolClient,
13
+ } from './handlers-utils.js';
14
+
15
+ /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
16
+ /** @typedef {import('./handlers-utils.js').ToolAction} ToolAction */
17
+ /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
18
+
19
+ /** @type {Record<string, ToolAction>} */
20
+ export const CAPTURE_ACTIONS = {
21
+ element: {
22
+ ref: true,
23
+ method: 'screenshot.capture_element',
24
+ params: (_, r) => ({ elementRef: r }),
25
+ },
26
+ region: {
27
+ ref: false,
28
+ method: 'screenshot.capture_region',
29
+ params: (a) => /** @type {Record<string, unknown>} */ (a.rect || {}),
30
+ },
31
+ full_page: {
32
+ ref: false,
33
+ method: 'screenshot.capture_full_page',
34
+ params: () => ({}),
35
+ },
36
+ cdp_document: { ref: false, method: 'cdp.get_document', params: () => ({}) },
37
+ cdp_dom_snapshot: {
38
+ ref: false,
39
+ method: 'cdp.get_dom_snapshot',
40
+ params: () => ({}),
41
+ },
42
+ cdp_box_model: {
43
+ ref: true,
44
+ method: 'cdp.get_box_model',
45
+ params: (_, r) => ({ elementRef: r }),
46
+ },
47
+ cdp_computed_styles: {
48
+ ref: true,
49
+ method: 'cdp.get_computed_styles_for_node',
50
+ params: (_, r) => ({ elementRef: r }),
51
+ },
52
+ };
53
+
54
+ /**
55
+ * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
56
+ * @returns {Promise<ToolResult>}
57
+ */
58
+ export async function handleCaptureTool(args) {
59
+ return dispatchToolAction(CAPTURE_ACTIONS, args, 'capture');
60
+ }
61
+
62
+ /** @type {Record<string, BridgeMethod>} */
63
+ export const INPUT_ACTION_METHODS = {
64
+ click: 'input.click',
65
+ focus: 'input.focus',
66
+ type: 'input.type',
67
+ press_key: 'input.press_key',
68
+ cdp_press_key: 'cdp.dispatch_key_event',
69
+ set_checked: 'input.set_checked',
70
+ select_option: 'input.select_option',
71
+ hover: 'input.hover',
72
+ drag: 'input.drag',
73
+ scroll_into_view: 'input.scroll_into_view',
74
+ };
75
+
76
+ /**
77
+ * @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
78
+ * @returns {Promise<ToolResult>}
79
+ */
80
+ export async function handleInputTool(args) {
81
+ return withToolClient(async (client) => {
82
+ const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
83
+ const elementTarget = async () => ({
84
+ elementRef: await resolveToolRef(client, args, requestedTabId),
85
+ });
86
+
87
+ switch (args.action) {
88
+ case 'click': {
89
+ const response = await requestBridge(
90
+ client,
91
+ 'input.click',
92
+ {
93
+ target: await elementTarget(),
94
+ button: args.button,
95
+ clickCount: args.clickCount,
96
+ },
97
+ {
98
+ tabId: requestedTabId,
99
+ source: REQUEST_SOURCE,
100
+ tokenBudget: getToolTokenBudget(args),
101
+ }
102
+ );
103
+ return summarizeToolResponse(response, 'input.click');
104
+ }
105
+ case 'focus': {
106
+ const response = await requestBridge(
107
+ client,
108
+ 'input.focus',
109
+ {
110
+ target: await elementTarget(),
111
+ },
112
+ {
113
+ tabId: requestedTabId,
114
+ source: REQUEST_SOURCE,
115
+ tokenBudget: getToolTokenBudget(args),
116
+ }
117
+ );
118
+ return summarizeToolResponse(response, 'input.focus');
119
+ }
120
+ case 'type': {
121
+ const response = await requestBridge(
122
+ client,
123
+ 'input.type',
124
+ {
125
+ target: await elementTarget(),
126
+ text: args.text,
127
+ clear: args.clear,
128
+ submit: args.submit,
129
+ },
130
+ {
131
+ tabId: requestedTabId,
132
+ source: REQUEST_SOURCE,
133
+ tokenBudget: getToolTokenBudget(args),
134
+ }
135
+ );
136
+ return summarizeToolResponse(response, 'input.type');
137
+ }
138
+ case 'press_key': {
139
+ const target = args.elementRef || args.selector ? await elementTarget() : undefined;
140
+ const response = await requestBridge(
141
+ client,
142
+ 'input.press_key',
143
+ {
144
+ target,
145
+ key: args.key,
146
+ modifiers: args.modifiers,
147
+ },
148
+ {
149
+ tabId: requestedTabId,
150
+ source: REQUEST_SOURCE,
151
+ tokenBudget: getToolTokenBudget(args),
152
+ }
153
+ );
154
+ return summarizeToolResponse(response, 'input.press_key');
155
+ }
156
+ case 'cdp_press_key': {
157
+ const response = await requestBridge(
158
+ client,
159
+ 'cdp.dispatch_key_event',
160
+ {
161
+ key: args.key,
162
+ code: args.code,
163
+ modifiers: args.modifiers,
164
+ },
165
+ {
166
+ tabId: requestedTabId,
167
+ source: REQUEST_SOURCE,
168
+ tokenBudget: getToolTokenBudget(args),
169
+ }
170
+ );
171
+ return summarizeToolResponse(response, 'cdp.dispatch_key_event');
172
+ }
173
+ case 'set_checked': {
174
+ const response = await requestBridge(
175
+ client,
176
+ 'input.set_checked',
177
+ {
178
+ target: await elementTarget(),
179
+ checked: args.checked,
180
+ },
181
+ {
182
+ tabId: requestedTabId,
183
+ source: REQUEST_SOURCE,
184
+ tokenBudget: getToolTokenBudget(args),
185
+ }
186
+ );
187
+ return summarizeToolResponse(response, 'input.set_checked');
188
+ }
189
+ case 'select_option': {
190
+ const response = await requestBridge(
191
+ client,
192
+ 'input.select_option',
193
+ {
194
+ target: await elementTarget(),
195
+ values: args.values,
196
+ labels: args.labels,
197
+ indexes: args.indexes,
198
+ },
199
+ {
200
+ tabId: requestedTabId,
201
+ source: REQUEST_SOURCE,
202
+ tokenBudget: getToolTokenBudget(args),
203
+ }
204
+ );
205
+ return summarizeToolResponse(response, 'input.select_option');
206
+ }
207
+ case 'hover': {
208
+ const response = await requestBridge(
209
+ client,
210
+ 'input.hover',
211
+ {
212
+ target: await elementTarget(),
213
+ duration: args.duration,
214
+ },
215
+ {
216
+ tabId: requestedTabId,
217
+ source: REQUEST_SOURCE,
218
+ tokenBudget: getToolTokenBudget(args),
219
+ }
220
+ );
221
+ return summarizeToolResponse(response, 'input.hover');
222
+ }
223
+ case 'drag': {
224
+ const source = {
225
+ elementRef:
226
+ args.sourceElementRef ||
227
+ (args.sourceSelector
228
+ ? await resolveRef(client, args.sourceSelector, requestedTabId, REQUEST_SOURCE)
229
+ : ''),
230
+ };
231
+ const destination = {
232
+ elementRef:
233
+ args.destinationElementRef ||
234
+ (args.destinationSelector
235
+ ? await resolveRef(client, args.destinationSelector, requestedTabId, REQUEST_SOURCE)
236
+ : ''),
237
+ };
238
+ if (!source.elementRef || !destination.elementRef) {
239
+ return summarizeToolError(
240
+ 'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
241
+ );
242
+ }
243
+ const response = await requestBridge(
244
+ client,
245
+ 'input.drag',
246
+ {
247
+ source,
248
+ destination,
249
+ offsetX: args.offsetX,
250
+ offsetY: args.offsetY,
251
+ },
252
+ {
253
+ tabId: requestedTabId,
254
+ source: REQUEST_SOURCE,
255
+ tokenBudget: getToolTokenBudget(args),
256
+ }
257
+ );
258
+ return summarizeToolResponse(response, 'input.drag');
259
+ }
260
+ case 'scroll_into_view': {
261
+ const response = await requestBridge(
262
+ client,
263
+ 'input.scroll_into_view',
264
+ {
265
+ target: await elementTarget(),
266
+ },
267
+ {
268
+ tabId: requestedTabId,
269
+ source: REQUEST_SOURCE,
270
+ tokenBudget: getToolTokenBudget(args),
271
+ }
272
+ );
273
+ return summarizeToolResponse(response, 'input.scroll_into_view');
274
+ }
275
+ default:
276
+ return summarizeToolError(`Unsupported input action "${args.action}".`);
277
+ }
278
+ });
279
+ }
@@ -0,0 +1,196 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ applyHtmlBudgetPreset,
5
+ applyTextBudgetPreset,
6
+ applyTreeBudgetPreset,
7
+ dispatchToolAction,
8
+ inferBudgetFromSelector,
9
+ } from './handlers-utils.js';
10
+
11
+ /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
12
+ /** @typedef {import('./handlers-utils.js').ToolAction} ToolAction */
13
+ /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
14
+
15
+ /** @type {Record<string, ToolAction>} */
16
+ export const DOM_ACTIONS = {
17
+ query: {
18
+ ref: false,
19
+ method: 'dom.query',
20
+ params: (a) => ({
21
+ selector: a.selector || 'body',
22
+ withinRef: a.withinRef,
23
+ maxNodes: a.maxNodes,
24
+ maxDepth: a.maxDepth,
25
+ textBudget: a.textBudget,
26
+ includeBbox: a.includeBbox,
27
+ attributeAllowlist: a.attributeAllowlist,
28
+ }),
29
+ },
30
+ describe: {
31
+ ref: true,
32
+ method: 'dom.describe',
33
+ params: (_, r) => ({ elementRef: r }),
34
+ },
35
+ text: {
36
+ ref: true,
37
+ method: 'dom.get_text',
38
+ params: (a, r) => ({ elementRef: r, textBudget: a.textBudget }),
39
+ },
40
+ attributes: {
41
+ ref: true,
42
+ method: 'dom.get_attributes',
43
+ params: (a, r) => ({ elementRef: r, attributes: a.attributes || [] }),
44
+ },
45
+ wait: {
46
+ ref: false,
47
+ method: 'dom.wait_for',
48
+ params: (a) => ({
49
+ selector: a.selector,
50
+ text: a.text,
51
+ state: a.state,
52
+ timeoutMs: a.timeoutMs,
53
+ }),
54
+ },
55
+ find_text: {
56
+ ref: false,
57
+ method: 'dom.find_by_text',
58
+ params: (a) => ({
59
+ text: a.text,
60
+ exact: a.exact,
61
+ selector: a.selector,
62
+ maxResults: a.maxResults,
63
+ }),
64
+ },
65
+ find_role: {
66
+ ref: false,
67
+ method: 'dom.find_by_role',
68
+ params: (a) => ({
69
+ role: a.role,
70
+ name: a.name,
71
+ selector: a.selector,
72
+ maxResults: a.maxResults,
73
+ }),
74
+ },
75
+ html: {
76
+ ref: true,
77
+ method: 'dom.get_html',
78
+ params: (a, r) => ({
79
+ elementRef: r,
80
+ outer: a.outer,
81
+ maxLength: a.maxLength,
82
+ }),
83
+ },
84
+ accessibility_tree: {
85
+ ref: false,
86
+ method: 'dom.get_accessibility_tree',
87
+ params: (a) => ({ maxNodes: a.maxNodes, maxDepth: a.maxDepth }),
88
+ },
89
+ };
90
+
91
+ /**
92
+ * @param {{ action: string, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
93
+ * @returns {Promise<ToolResult>}
94
+ */
95
+ export async function handleDomTool(args) {
96
+ if (args.action === 'query' || args.action === 'accessibility_tree') {
97
+ const inferred = inferBudgetFromSelector(args);
98
+ const withBudget = inferred ? { ...args, budgetPreset: args.budgetPreset ?? inferred } : args;
99
+ return dispatchToolAction(DOM_ACTIONS, applyTreeBudgetPreset(withBudget), 'DOM');
100
+ }
101
+ if (args.action === 'text') {
102
+ return dispatchToolAction(DOM_ACTIONS, applyTextBudgetPreset(args), 'DOM');
103
+ }
104
+ if (args.action === 'html') {
105
+ return dispatchToolAction(DOM_ACTIONS, applyHtmlBudgetPreset(args), 'DOM');
106
+ }
107
+ return dispatchToolAction(DOM_ACTIONS, args, 'DOM');
108
+ }
109
+
110
+ /** @type {Record<string, ToolAction>} */
111
+ export const STYLES_LAYOUT_ACTIONS = {
112
+ computed: {
113
+ ref: true,
114
+ method: 'styles.get_computed',
115
+ params: (a, r) => ({ elementRef: r, properties: a.properties }),
116
+ },
117
+ matched_rules: {
118
+ ref: true,
119
+ method: 'styles.get_matched_rules',
120
+ params: (_, r) => ({ elementRef: r }),
121
+ },
122
+ box_model: {
123
+ ref: true,
124
+ method: 'layout.get_box_model',
125
+ params: (_, r) => ({ elementRef: r }),
126
+ },
127
+ hit_test: {
128
+ ref: false,
129
+ method: 'layout.hit_test',
130
+ params: (a) => ({ x: a.x, y: a.y }),
131
+ },
132
+ };
133
+
134
+ /**
135
+ * @param {{ action: string, elementRef?: string, selector?: string, properties?: string[], x?: number, y?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
136
+ * @returns {Promise<ToolResult>}
137
+ */
138
+ export async function handleStylesLayoutTool(args) {
139
+ return dispatchToolAction(STYLES_LAYOUT_ACTIONS, args, 'styles/layout');
140
+ }
141
+
142
+ /** @type {Record<string, ToolAction>} */
143
+ export const PATCH_ACTIONS = {
144
+ apply_styles: {
145
+ ref: true,
146
+ method: 'patch.apply_styles',
147
+ params: (a, r) => ({
148
+ target: { elementRef: r },
149
+ declarations: a.declarations,
150
+ important: a.important,
151
+ verify: a.verify,
152
+ }),
153
+ },
154
+ apply_dom: {
155
+ ref: true,
156
+ method: 'patch.apply_dom',
157
+ params: (a, r) => {
158
+ const operation = typeof a.operation === 'string' ? a.operation : '';
159
+ /** @type {Record<string, string>} */
160
+ const opMap = {
161
+ setAttribute: 'set_attribute',
162
+ removeAttribute: 'remove_attribute',
163
+ addClass: 'toggle_class',
164
+ removeClass: 'toggle_class',
165
+ setTextContent: 'set_text',
166
+ setProperty: 'set_attribute',
167
+ };
168
+ return {
169
+ target: { elementRef: r },
170
+ operation: opMap[operation] || operation,
171
+ value: a.value,
172
+ name: a.name,
173
+ verify: a.verify,
174
+ };
175
+ },
176
+ },
177
+ list: { ref: false, method: 'patch.list', params: () => ({}) },
178
+ rollback: {
179
+ ref: false,
180
+ method: 'patch.rollback',
181
+ params: (a) => ({ patchId: a.patchId }),
182
+ },
183
+ commit_baseline: {
184
+ ref: false,
185
+ method: 'patch.commit_session_baseline',
186
+ params: () => ({}),
187
+ },
188
+ };
189
+
190
+ /**
191
+ * @param {{ action: string, elementRef?: string, selector?: string, declarations?: Record<string, string>, important?: boolean, operation?: string, value?: unknown, name?: string, patchId?: string, verify?: boolean, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
192
+ * @returns {Promise<ToolResult>}
193
+ */
194
+ export async function handlePatchTool(args) {
195
+ return dispatchToolAction(PATCH_ACTIONS, args, 'patch');
196
+ }
@@ -0,0 +1,79 @@
1
+ // @ts-check
2
+
3
+ import { callBridgeTool, getToolTokenBudget, summarizeToolError } from './handlers-utils.js';
4
+
5
+ /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
6
+ /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
7
+
8
+ /**
9
+ * @param {{ action: string, url?: string, active?: boolean, tabId?: number }} args
10
+ * @returns {Promise<ToolResult>}
11
+ */
12
+ export async function handleTabsTool(args) {
13
+ if (args.action === 'list') {
14
+ return callBridgeTool('tabs.list');
15
+ }
16
+ if (args.action === 'create') {
17
+ return callBridgeTool('tabs.create', {
18
+ url: args.url,
19
+ active: args.active,
20
+ });
21
+ }
22
+ if (args.action === 'close') {
23
+ if (typeof args.tabId !== 'number') {
24
+ return summarizeToolError('tabId is required for tabs.close.');
25
+ }
26
+ return callBridgeTool('tabs.close', { tabId: args.tabId });
27
+ }
28
+ return summarizeToolError(`Unsupported tabs action "${args.action}".`);
29
+ }
30
+
31
+ /** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
32
+ export const NAVIGATION_ACTIONS = {
33
+ navigate: {
34
+ method: 'navigation.navigate',
35
+ params: (a) => ({
36
+ url: a.url,
37
+ waitForLoad: a.waitForLoad,
38
+ timeoutMs: a.timeoutMs,
39
+ }),
40
+ },
41
+ reload: {
42
+ method: 'navigation.reload',
43
+ params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
44
+ },
45
+ go_back: {
46
+ method: 'navigation.go_back',
47
+ params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
48
+ },
49
+ go_forward: {
50
+ method: 'navigation.go_forward',
51
+ params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
52
+ },
53
+ scroll: {
54
+ method: 'viewport.scroll',
55
+ params: (a) => ({
56
+ top: a.top,
57
+ left: a.left,
58
+ behavior: a.behavior,
59
+ relative: a.relative,
60
+ }),
61
+ },
62
+ resize: {
63
+ method: 'viewport.resize',
64
+ params: (a) => ({ width: a.width, height: a.height, reset: a.reset }),
65
+ },
66
+ };
67
+
68
+ /**
69
+ * @param {{ action: string, url?: string, waitForLoad?: boolean, timeoutMs?: number, top?: number, left?: number, behavior?: string, relative?: boolean, width?: number, height?: number, reset?: boolean, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
70
+ * @returns {Promise<ToolResult>}
71
+ */
72
+ export async function handleNavigationTool(args) {
73
+ const entry = NAVIGATION_ACTIONS[args.action];
74
+ if (!entry) return summarizeToolError(`Unsupported navigation action "${args.action}".`);
75
+ return callBridgeTool(entry.method, entry.params(args), {
76
+ tabId: typeof args.tabId === 'number' ? args.tabId : null,
77
+ tokenBudget: getToolTokenBudget(args),
78
+ });
79
+ }