@browserbridge/bbx 1.1.0 → 1.3.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 (31) hide show
  1. package/README.md +6 -5
  2. package/package.json +1 -1
  3. package/packages/agent-client/src/cli.js +30 -20
  4. package/packages/agent-client/src/client.js +105 -42
  5. package/packages/agent-client/src/command-registry.js +4 -14
  6. package/packages/agent-client/src/detect.js +3 -3
  7. package/packages/agent-client/src/install.js +3 -7
  8. package/packages/agent-client/src/mcp-config.js +1 -3
  9. package/packages/agent-client/src/runtime.js +7 -41
  10. package/packages/agent-client/src/setup-status.js +3 -13
  11. package/packages/agent-client/src/types.ts +131 -0
  12. package/packages/mcp-server/src/handlers-capture.js +291 -0
  13. package/packages/mcp-server/src/handlers-dom.js +203 -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 +318 -0
  17. package/packages/mcp-server/src/handlers.js +59 -1176
  18. package/packages/mcp-server/src/server.js +2 -1
  19. package/packages/native-host/bin/bridge-daemon.js +2 -1
  20. package/packages/native-host/bin/install-manifest.js +8 -0
  21. package/packages/native-host/bin/postinstall.js +46 -9
  22. package/packages/native-host/src/daemon-logger.js +157 -0
  23. package/packages/native-host/src/daemon-process.js +43 -18
  24. package/packages/native-host/src/daemon.js +133 -12
  25. package/packages/native-host/src/framing.js +13 -0
  26. package/packages/native-host/src/native-host.js +7 -5
  27. package/packages/protocol/src/capabilities.js +1 -0
  28. package/packages/protocol/src/protocol.js +40 -0
  29. package/packages/protocol/src/registry.js +5 -9
  30. package/packages/protocol/src/types.ts +572 -0
  31. package/packages/protocol/src/types.js +0 -626
@@ -0,0 +1,131 @@
1
+ import type { BridgeTransport } from '../../native-host/src/config.js';
2
+ import type {
3
+ BridgeMeta,
4
+ BridgeMethod,
5
+ BridgeRequestSource,
6
+ BridgeResponse,
7
+ } from '../../protocol/src/types.js';
8
+ import type { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
9
+
10
+ export type { BridgeMeta, BridgeMethod, BridgeRequestSource, BridgeResponse, BridgeTransport };
11
+
12
+ export type McpClientName =
13
+ | 'codex'
14
+ | 'claude'
15
+ | 'cursor'
16
+ | 'copilot'
17
+ | 'opencode'
18
+ | 'antigravity'
19
+ | 'windsurf'
20
+ | 'agents';
21
+
22
+ export type SupportedTarget = McpClientName;
23
+
24
+ export type Detector = () => boolean | Promise<boolean>;
25
+
26
+ export interface InstallAgentOptions {
27
+ targets: SupportedTarget[];
28
+ projectPath: string;
29
+ global: boolean;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ export interface SetupStatusOptions {
34
+ global?: boolean;
35
+ cwd?: string;
36
+ projectPath?: string;
37
+ mcpDetectors?: Record<string, Detector>;
38
+ skillDetectors?: Record<string, Detector>;
39
+ access?: (targetPath: string) => Promise<void>;
40
+ readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>;
41
+ }
42
+
43
+ export interface ProtocolHealthResult {
44
+ extensionConnected?: boolean;
45
+ supported_versions?: string[];
46
+ daemon_supported_versions?: string[];
47
+ deprecated_since?: string;
48
+ migration_hint?: string;
49
+ }
50
+
51
+ export type ClientMessage =
52
+ | {
53
+ type: 'registered';
54
+ role: 'agent' | 'extension';
55
+ clientId?: string;
56
+ }
57
+ | {
58
+ type: 'agent.response';
59
+ response: BridgeResponse;
60
+ };
61
+
62
+ export interface PendingRequest {
63
+ resolve: (value: any) => void;
64
+ reject: (error: Error) => void;
65
+ timeoutId: NodeJS.Timeout;
66
+ }
67
+
68
+ export interface BridgeClientOptions {
69
+ transport?: BridgeTransport;
70
+ socketPath?: string;
71
+ clientId?: string;
72
+ defaultTimeoutMs?: number;
73
+ autoReconnect?: boolean;
74
+ restartDaemonOnVersionMismatch?: boolean;
75
+ restartDaemonFn?: typeof restartBridgeDaemon;
76
+ }
77
+
78
+ export interface ShortcutCommand {
79
+ method: BridgeMethod;
80
+ resolve?: boolean;
81
+ printMethod?: string;
82
+ usage: string;
83
+ description: string;
84
+ build: (r: string[], ref?: string) => Record<string, unknown>;
85
+ }
86
+
87
+ export interface BrowserManifestStatus {
88
+ browser: string;
89
+ manifestPath: string;
90
+ installed: boolean;
91
+ }
92
+
93
+ export interface DoctorReport {
94
+ manifestInstalled: boolean;
95
+ manifestPath: string;
96
+ allowedOrigins: string[];
97
+ defaultExtensionId: string | null;
98
+ defaultExtensionIdSource: string;
99
+ daemonReachable: boolean;
100
+ extensionConnected: boolean;
101
+ accessEnabled: boolean;
102
+ enabledWindowId: number | null;
103
+ routeTabId: number | null;
104
+ routeReady: boolean;
105
+ routeReason: string;
106
+ issues: string[];
107
+ nextSteps: string[];
108
+ browserManifests: BrowserManifestStatus[];
109
+ }
110
+
111
+ export interface DoctorReportOptions {
112
+ loadManifest?: () => Promise<{ allowed_origins?: string[] } | null>;
113
+ manifestPath?: string;
114
+ defaultExtensionIdInfo?: { extensionId: string | null; source: string };
115
+ bridgeClientRunner?: <T>(
116
+ callback: (client: { request: BridgeClientRequest }) => Promise<T>
117
+ ) => Promise<T>;
118
+ }
119
+
120
+ export type BridgeClientRequest = (options: {
121
+ method: BridgeMethod;
122
+ tabId?: number | null;
123
+ params?: Record<string, unknown>;
124
+ meta?: BridgeMeta;
125
+ timeoutMs?: number;
126
+ }) => Promise<BridgeResponse>;
127
+
128
+ export interface ScreenshotResult {
129
+ image: string;
130
+ rect: Record<string, unknown>;
131
+ }
@@ -0,0 +1,291 @@
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: false,
44
+ method: 'cdp.get_box_model',
45
+ params: (a) => ({ nodeId: a.nodeId }),
46
+ },
47
+ cdp_computed_styles: {
48
+ ref: false,
49
+ method: 'cdp.get_computed_styles_for_node',
50
+ params: (a) => ({ nodeId: a.nodeId }),
51
+ },
52
+ };
53
+
54
+ /** @param {Record<string, unknown>} args */
55
+ function isCdpNodeCapture(args) {
56
+ return args.action === 'cdp_box_model' || args.action === 'cdp_computed_styles';
57
+ }
58
+
59
+ /**
60
+ * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, nodeId?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
61
+ * @returns {Promise<ToolResult>}
62
+ */
63
+ export async function handleCaptureTool(args) {
64
+ if (
65
+ isCdpNodeCapture(args) &&
66
+ (typeof args.nodeId !== 'number' || !Number.isFinite(args.nodeId))
67
+ ) {
68
+ return summarizeToolError('nodeId must be a finite number.');
69
+ }
70
+
71
+ return dispatchToolAction(CAPTURE_ACTIONS, args, 'capture');
72
+ }
73
+
74
+ /** @type {Record<string, BridgeMethod>} */
75
+ export const INPUT_ACTION_METHODS = {
76
+ click: 'input.click',
77
+ focus: 'input.focus',
78
+ type: 'input.type',
79
+ press_key: 'input.press_key',
80
+ cdp_press_key: 'cdp.dispatch_key_event',
81
+ set_checked: 'input.set_checked',
82
+ select_option: 'input.select_option',
83
+ hover: 'input.hover',
84
+ drag: 'input.drag',
85
+ scroll_into_view: 'input.scroll_into_view',
86
+ };
87
+
88
+ /**
89
+ * @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
90
+ * @returns {Promise<ToolResult>}
91
+ */
92
+ export async function handleInputTool(args) {
93
+ return withToolClient(async (client) => {
94
+ const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
95
+ const elementTarget = async () => ({
96
+ elementRef: await resolveToolRef(client, args, requestedTabId),
97
+ });
98
+
99
+ switch (args.action) {
100
+ case 'click': {
101
+ const response = await requestBridge(
102
+ client,
103
+ 'input.click',
104
+ {
105
+ target: await elementTarget(),
106
+ button: args.button,
107
+ clickCount: args.clickCount,
108
+ },
109
+ {
110
+ tabId: requestedTabId,
111
+ source: REQUEST_SOURCE,
112
+ tokenBudget: getToolTokenBudget(args),
113
+ }
114
+ );
115
+ return summarizeToolResponse(response, 'input.click');
116
+ }
117
+ case 'focus': {
118
+ const response = await requestBridge(
119
+ client,
120
+ 'input.focus',
121
+ {
122
+ target: await elementTarget(),
123
+ },
124
+ {
125
+ tabId: requestedTabId,
126
+ source: REQUEST_SOURCE,
127
+ tokenBudget: getToolTokenBudget(args),
128
+ }
129
+ );
130
+ return summarizeToolResponse(response, 'input.focus');
131
+ }
132
+ case 'type': {
133
+ const response = await requestBridge(
134
+ client,
135
+ 'input.type',
136
+ {
137
+ target: await elementTarget(),
138
+ text: args.text,
139
+ clear: args.clear,
140
+ submit: args.submit,
141
+ },
142
+ {
143
+ tabId: requestedTabId,
144
+ source: REQUEST_SOURCE,
145
+ tokenBudget: getToolTokenBudget(args),
146
+ }
147
+ );
148
+ return summarizeToolResponse(response, 'input.type');
149
+ }
150
+ case 'press_key': {
151
+ const target = args.elementRef || args.selector ? await elementTarget() : undefined;
152
+ const response = await requestBridge(
153
+ client,
154
+ 'input.press_key',
155
+ {
156
+ target,
157
+ key: args.key,
158
+ modifiers: args.modifiers,
159
+ },
160
+ {
161
+ tabId: requestedTabId,
162
+ source: REQUEST_SOURCE,
163
+ tokenBudget: getToolTokenBudget(args),
164
+ }
165
+ );
166
+ return summarizeToolResponse(response, 'input.press_key');
167
+ }
168
+ case 'cdp_press_key': {
169
+ const response = await requestBridge(
170
+ client,
171
+ 'cdp.dispatch_key_event',
172
+ {
173
+ key: args.key,
174
+ code: args.code,
175
+ modifiers: args.modifiers,
176
+ },
177
+ {
178
+ tabId: requestedTabId,
179
+ source: REQUEST_SOURCE,
180
+ tokenBudget: getToolTokenBudget(args),
181
+ }
182
+ );
183
+ return summarizeToolResponse(response, 'cdp.dispatch_key_event');
184
+ }
185
+ case 'set_checked': {
186
+ const response = await requestBridge(
187
+ client,
188
+ 'input.set_checked',
189
+ {
190
+ target: await elementTarget(),
191
+ checked: args.checked,
192
+ },
193
+ {
194
+ tabId: requestedTabId,
195
+ source: REQUEST_SOURCE,
196
+ tokenBudget: getToolTokenBudget(args),
197
+ }
198
+ );
199
+ return summarizeToolResponse(response, 'input.set_checked');
200
+ }
201
+ case 'select_option': {
202
+ const response = await requestBridge(
203
+ client,
204
+ 'input.select_option',
205
+ {
206
+ target: await elementTarget(),
207
+ values: args.values,
208
+ labels: args.labels,
209
+ indexes: args.indexes,
210
+ },
211
+ {
212
+ tabId: requestedTabId,
213
+ source: REQUEST_SOURCE,
214
+ tokenBudget: getToolTokenBudget(args),
215
+ }
216
+ );
217
+ return summarizeToolResponse(response, 'input.select_option');
218
+ }
219
+ case 'hover': {
220
+ const response = await requestBridge(
221
+ client,
222
+ 'input.hover',
223
+ {
224
+ target: await elementTarget(),
225
+ duration: args.duration,
226
+ },
227
+ {
228
+ tabId: requestedTabId,
229
+ source: REQUEST_SOURCE,
230
+ tokenBudget: getToolTokenBudget(args),
231
+ }
232
+ );
233
+ return summarizeToolResponse(response, 'input.hover');
234
+ }
235
+ case 'drag': {
236
+ const source = {
237
+ elementRef:
238
+ args.sourceElementRef ||
239
+ (args.sourceSelector
240
+ ? await resolveRef(client, args.sourceSelector, requestedTabId, REQUEST_SOURCE)
241
+ : ''),
242
+ };
243
+ const destination = {
244
+ elementRef:
245
+ args.destinationElementRef ||
246
+ (args.destinationSelector
247
+ ? await resolveRef(client, args.destinationSelector, requestedTabId, REQUEST_SOURCE)
248
+ : ''),
249
+ };
250
+ if (!source.elementRef || !destination.elementRef) {
251
+ return summarizeToolError(
252
+ 'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
253
+ );
254
+ }
255
+ const response = await requestBridge(
256
+ client,
257
+ 'input.drag',
258
+ {
259
+ source,
260
+ destination,
261
+ offsetX: args.offsetX,
262
+ offsetY: args.offsetY,
263
+ },
264
+ {
265
+ tabId: requestedTabId,
266
+ source: REQUEST_SOURCE,
267
+ tokenBudget: getToolTokenBudget(args),
268
+ }
269
+ );
270
+ return summarizeToolResponse(response, 'input.drag');
271
+ }
272
+ case 'scroll_into_view': {
273
+ const response = await requestBridge(
274
+ client,
275
+ 'input.scroll_into_view',
276
+ {
277
+ target: await elementTarget(),
278
+ },
279
+ {
280
+ tabId: requestedTabId,
281
+ source: REQUEST_SOURCE,
282
+ tokenBudget: getToolTokenBudget(args),
283
+ }
284
+ );
285
+ return summarizeToolResponse(response, 'input.scroll_into_view');
286
+ }
287
+ default:
288
+ return summarizeToolError(`Unsupported input action "${args.action}".`);
289
+ }
290
+ });
291
+ }
@@ -0,0 +1,203 @@
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
+ patchId: a.patchId,
152
+ verify: a.verify,
153
+ }),
154
+ },
155
+ apply_dom: {
156
+ ref: true,
157
+ method: 'patch.apply_dom',
158
+ params: (a, r) => {
159
+ const operation = typeof a.operation === 'string' ? a.operation : '';
160
+ /** @type {Record<string, string>} */
161
+ const opMap = {
162
+ setAttribute: 'set_attribute',
163
+ removeAttribute: 'remove_attribute',
164
+ addClass: 'add_class',
165
+ removeClass: 'remove_class',
166
+ setTextContent: 'set_text',
167
+ setProperty: 'set_attribute',
168
+ };
169
+ const normalizedOperation = opMap[operation] || operation;
170
+ const value =
171
+ normalizedOperation === 'add_class' || normalizedOperation === 'remove_class'
172
+ ? (a.value ?? a.name)
173
+ : a.value;
174
+ return {
175
+ target: { elementRef: r },
176
+ operation: normalizedOperation,
177
+ value,
178
+ name: a.name,
179
+ patchId: a.patchId,
180
+ verify: a.verify,
181
+ };
182
+ },
183
+ },
184
+ list: { ref: false, method: 'patch.list', params: () => ({}) },
185
+ rollback: {
186
+ ref: false,
187
+ method: 'patch.rollback',
188
+ params: (a) => ({ patchId: a.patchId }),
189
+ },
190
+ commit_baseline: {
191
+ ref: false,
192
+ method: 'patch.commit_session_baseline',
193
+ params: () => ({}),
194
+ },
195
+ };
196
+
197
+ /**
198
+ * @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
199
+ * @returns {Promise<ToolResult>}
200
+ */
201
+ export async function handlePatchTool(args) {
202
+ return dispatchToolAction(PATCH_ACTIONS, args, 'patch');
203
+ }
@@ -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
+ }