@browserbridge/bbx 1.2.0 → 1.4.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 (35) hide show
  1. package/README.md +8 -5
  2. package/package.json +2 -2
  3. package/packages/agent-client/src/cli.js +56 -31
  4. package/packages/agent-client/src/client.js +81 -65
  5. package/packages/agent-client/src/command-registry.js +4 -15
  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 +20 -5
  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 +139 -0
  12. package/packages/mcp-server/src/guidance.js +241 -0
  13. package/packages/mcp-server/src/handlers-capture.js +91 -16
  14. package/packages/mcp-server/src/handlers-dom.js +59 -4
  15. package/packages/mcp-server/src/handlers-navigation.js +22 -2
  16. package/packages/mcp-server/src/handlers-page.js +6 -11
  17. package/packages/mcp-server/src/handlers-utils.js +69 -1
  18. package/packages/mcp-server/src/server.js +111 -28
  19. package/packages/native-host/bin/postinstall.js +42 -21
  20. package/packages/native-host/src/auth-token.js +92 -0
  21. package/packages/native-host/src/daemon-process.js +1 -2
  22. package/packages/native-host/src/daemon.js +199 -30
  23. package/packages/native-host/src/framing.js +13 -0
  24. package/packages/native-host/src/native-host.js +25 -7
  25. package/packages/protocol/src/defaults.js +3 -0
  26. package/packages/protocol/src/json-lines.js +29 -1
  27. package/packages/protocol/src/protocol.js +43 -0
  28. package/packages/protocol/src/registry.js +3 -9
  29. package/packages/protocol/src/types.ts +574 -0
  30. package/skills/browser-bridge/SKILL.md +21 -5
  31. package/skills/browser-bridge/agents/openai.yaml +1 -1
  32. package/skills/browser-bridge/references/interaction.md +6 -6
  33. package/skills/browser-bridge/references/protocol.md +57 -54
  34. package/skills/browser-bridge/references/ui-workflows.md +1 -1
  35. package/packages/protocol/src/types.js +0 -626
@@ -0,0 +1,139 @@
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: 'registration_failed';
59
+ error?: {
60
+ code?: string;
61
+ message?: string;
62
+ };
63
+ }
64
+ | {
65
+ type: 'agent.response';
66
+ response: BridgeResponse;
67
+ };
68
+
69
+ export interface PendingRequest {
70
+ resolve: (value: any) => void;
71
+ reject: (error: Error) => void;
72
+ timeoutId: NodeJS.Timeout;
73
+ }
74
+
75
+ export interface BridgeClientOptions {
76
+ transport?: BridgeTransport;
77
+ socketPath?: string;
78
+ clientId?: string;
79
+ defaultTimeoutMs?: number;
80
+ autoReconnect?: boolean;
81
+ restartDaemonOnVersionMismatch?: boolean;
82
+ restartDaemonFn?: typeof restartBridgeDaemon;
83
+ authToken?: string | null;
84
+ }
85
+
86
+ export interface ShortcutCommand {
87
+ method: BridgeMethod;
88
+ resolve?: boolean;
89
+ printMethod?: string;
90
+ usage: string;
91
+ description: string;
92
+ build: (r: string[], ref?: string) => Record<string, unknown>;
93
+ }
94
+
95
+ export interface BrowserManifestStatus {
96
+ browser: string;
97
+ manifestPath: string;
98
+ installed: boolean;
99
+ }
100
+
101
+ export interface DoctorReport {
102
+ manifestInstalled: boolean;
103
+ manifestPath: string;
104
+ allowedOrigins: string[];
105
+ defaultExtensionId: string | null;
106
+ defaultExtensionIdSource: string;
107
+ daemonReachable: boolean;
108
+ extensionConnected: boolean;
109
+ accessEnabled: boolean;
110
+ enabledWindowId: number | null;
111
+ routeTabId: number | null;
112
+ routeReady: boolean;
113
+ routeReason: string;
114
+ issues: string[];
115
+ nextSteps: string[];
116
+ browserManifests: BrowserManifestStatus[];
117
+ }
118
+
119
+ export interface DoctorReportOptions {
120
+ loadManifest?: () => Promise<{ allowed_origins?: string[] } | null>;
121
+ manifestPath?: string;
122
+ defaultExtensionIdInfo?: { extensionId: string | null; source: string };
123
+ bridgeClientRunner?: <T>(
124
+ callback: (client: { request: BridgeClientRequest }) => Promise<T>
125
+ ) => Promise<T>;
126
+ }
127
+
128
+ export type BridgeClientRequest = (options: {
129
+ method: BridgeMethod;
130
+ tabId?: number | null;
131
+ params?: Record<string, unknown>;
132
+ meta?: BridgeMeta;
133
+ timeoutMs?: number;
134
+ }) => Promise<BridgeResponse>;
135
+
136
+ export interface ScreenshotResult {
137
+ image: string;
138
+ rect: Record<string, unknown>;
139
+ }
@@ -0,0 +1,241 @@
1
+ // @ts-check
2
+
3
+ import * as z from 'zod/v4';
4
+
5
+ /** @typedef {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} McpServer */
6
+ /** @typedef {import('@modelcontextprotocol/sdk/types.js').GetPromptResult} GetPromptResult */
7
+
8
+ export const MCP_SERVER_INSTRUCTIONS = [
9
+ "Browser Bridge MCP inspects and interacts with the user's real Chrome tab through typed MCP tools.",
10
+ 'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
11
+ 'Call browser_status first. If window access is disabled, call browser_access once, ask the user to click Enable in the Browser Bridge popup or side panel, then retry once.',
12
+ 'Use structured reads first: browser_page, browser_dom, browser_styles_layout, and browser_batch. Keep budgetPreset quick or normal before widening.',
13
+ 'Reuse elementRef values returned by DOM tools. Use attribute allowlists for focused DOM reads.',
14
+ 'Escalate to browser_capture, accessibility_tree, page evaluate, viewport resize, or CDP only when structured reads cannot answer the question.',
15
+ 'Use browser_patch for temporary style or DOM experiments, and rollback patches before finishing unless the user asks to keep them.',
16
+ ].join('\n');
17
+
18
+ export const MCP_GUIDANCE_PROMPT_NAMES = Object.freeze([
19
+ 'browser_bridge_guide',
20
+ 'browser_bridge_investigate',
21
+ 'browser_bridge_debug_layout',
22
+ 'browser_bridge_verify_flow',
23
+ ]);
24
+
25
+ /**
26
+ * Register Browser Bridge MCP prompt templates. These are the MCP-mode equivalent
27
+ * of a lightweight skill: discoverable by clients without requiring filesystem
28
+ * skill installation or shell access.
29
+ *
30
+ * @param {McpServer} server
31
+ * @returns {void}
32
+ */
33
+ export function registerBridgeMcpGuidance(server) {
34
+ server.registerPrompt(
35
+ 'browser_bridge_guide',
36
+ {
37
+ title: 'Use Browser Bridge MCP',
38
+ description:
39
+ 'General Browser Bridge MCP workflow guidance. Prefer this over CLI skill setup.',
40
+ },
41
+ createGuidePrompt
42
+ );
43
+
44
+ server.registerPrompt(
45
+ 'browser_bridge_investigate',
46
+ {
47
+ title: 'Investigate Current Page',
48
+ description:
49
+ 'Inspect the current page with structured reads before screenshots or evaluation.',
50
+ argsSchema: {
51
+ objective: z.string().optional().describe('What to find, verify, or explain'),
52
+ selector: z
53
+ .string()
54
+ .optional()
55
+ .describe('Optional CSS selector to scope the first DOM read'),
56
+ scope: z.enum(['quick', 'normal', 'deep']).optional().describe('Investigation depth'),
57
+ },
58
+ },
59
+ createInvestigatePrompt
60
+ );
61
+
62
+ server.registerPrompt(
63
+ 'browser_bridge_debug_layout',
64
+ {
65
+ title: 'Debug Layout Or Styling',
66
+ description: 'Diagnose a visual, spacing, sizing, visibility, or CSS issue in the live tab.',
67
+ argsSchema: {
68
+ target: z.string().optional().describe('Element, component, text, or selector to inspect'),
69
+ symptom: z.string().optional().describe('Observed layout or styling problem'),
70
+ },
71
+ },
72
+ createDebugLayoutPrompt
73
+ );
74
+
75
+ server.registerPrompt(
76
+ 'browser_bridge_verify_flow',
77
+ {
78
+ title: 'Verify User Flow',
79
+ description:
80
+ 'Drive a user flow through MCP input tools and verify page, console, and network state.',
81
+ argsSchema: {
82
+ flow: z.string().optional().describe('User flow to exercise'),
83
+ successCriteria: z.string().optional().describe('Expected successful outcome'),
84
+ },
85
+ },
86
+ createVerifyFlowPrompt
87
+ );
88
+ }
89
+
90
+ /**
91
+ * @returns {GetPromptResult}
92
+ */
93
+ function createGuidePrompt() {
94
+ return createUserPrompt(
95
+ 'Browser Bridge MCP workflow guide.',
96
+ [
97
+ 'Use Browser Bridge MCP for this browser task.',
98
+ '',
99
+ 'Rules:',
100
+ '1. Prefer MCP tools over `bbx`; do not shell out unless setup, doctor, logs, or raw CLI debugging is explicitly needed.',
101
+ '2. Call `browser_status` first. If access is disabled, call `browser_access` once, ask the user to click Enable, then retry once.',
102
+ '3. Start with structured reads: `browser_page` action `state`, `browser_dom` action `query`/`find_text`/`find_role`, `browser_styles_layout`, and `browser_batch`.',
103
+ '4. Keep budgets tight with `budgetPreset: "quick"` or `"normal"`; widen only when results are truncated.',
104
+ '5. Reuse `elementRef` values returned by DOM tools instead of rescanning.',
105
+ '6. Escalate to `browser_capture`, accessibility tree, `browser_page` evaluate, viewport resize, or CDP only when structured reads cannot answer.',
106
+ '7. Use `browser_patch` for temporary style/DOM experiments and rollback before finishing unless the user asks to keep patches.',
107
+ '',
108
+ 'Return concise findings with evidence. Edit source code only after the live page behavior is understood.',
109
+ ].join('\n')
110
+ );
111
+ }
112
+
113
+ /**
114
+ * @param {{ objective?: string, selector?: string, scope?: 'quick' | 'normal' | 'deep' }} args
115
+ * @returns {GetPromptResult}
116
+ */
117
+ function createInvestigatePrompt(args) {
118
+ const objective = normalizeTextArg(
119
+ args.objective,
120
+ 'inspect the current page and report findings'
121
+ );
122
+ const selector = normalizeTextArg(
123
+ args.selector,
124
+ 'none; start with main, body, or semantic search'
125
+ );
126
+ const scope = normalizeTextArg(args.scope, 'normal');
127
+
128
+ return createUserPrompt(
129
+ 'Browser Bridge MCP page investigation workflow.',
130
+ [
131
+ 'Investigate the current page with Browser Bridge MCP.',
132
+ '',
133
+ `Objective: ${objective}`,
134
+ `Scope: ${scope}`,
135
+ `Initial selector: ${selector}`,
136
+ '',
137
+ 'Workflow:',
138
+ '1. Call `browser_status` to confirm daemon, extension, and access readiness.',
139
+ '2. If access is disabled, call `browser_access` once, ask the user to click Enable, then retry once.',
140
+ '3. Use `browser_batch` for independent structured reads, usually `page.get_state`, a scoped `dom.query`, and `page.get_text` when page copy matters.',
141
+ '4. Use `browser_dom` `find_text` or `find_role` when the target is known by label but not selector.',
142
+ '5. Add `browser_styles_layout`, `browser_page` console, or `browser_page` network only when they directly answer the objective.',
143
+ '6. Escalate to screenshots, accessibility tree, or evaluate only when structured reads are insufficient.',
144
+ '',
145
+ 'Return concise findings, relevant evidence, and the next source-code action if a fix is needed.',
146
+ ].join('\n')
147
+ );
148
+ }
149
+
150
+ /**
151
+ * @param {{ target?: string, symptom?: string }} args
152
+ * @returns {GetPromptResult}
153
+ */
154
+ function createDebugLayoutPrompt(args) {
155
+ const target = normalizeTextArg(args.target, 'the affected element or component');
156
+ const symptom = normalizeTextArg(args.symptom, 'the observed layout or styling problem');
157
+
158
+ return createUserPrompt(
159
+ 'Browser Bridge MCP layout debugging workflow.',
160
+ [
161
+ 'Debug a layout or styling issue in the live tab with Browser Bridge MCP.',
162
+ '',
163
+ `Target: ${target}`,
164
+ `Symptom: ${symptom}`,
165
+ '',
166
+ 'Workflow:',
167
+ '1. Call `browser_status` first and resolve access if needed.',
168
+ '2. Locate the target with `browser_dom` `query`, `find_text`, or `find_role` using a quick budget.',
169
+ '3. Read only relevant computed styles with `browser_styles_layout` action `computed` and specific `properties`.',
170
+ '4. Read dimensions with `browser_styles_layout` action `box_model`; use matched rules only when the cascade is unclear.',
171
+ '5. Prototype the smallest visual fix with `browser_patch` action `apply_styles` and `verify: true` when useful.',
172
+ '6. Check `browser_page` console for new errors after interaction or patching.',
173
+ '7. Roll back temporary patches unless the user explicitly wants them kept, then edit source with the confirmed fix.',
174
+ '',
175
+ 'Avoid screenshots until DOM, computed styles, and box model evidence are insufficient.',
176
+ ].join('\n')
177
+ );
178
+ }
179
+
180
+ /**
181
+ * @param {{ flow?: string, successCriteria?: string }} args
182
+ * @returns {GetPromptResult}
183
+ */
184
+ function createVerifyFlowPrompt(args) {
185
+ const flow = normalizeTextArg(args.flow, 'the requested user flow');
186
+ const successCriteria = normalizeTextArg(
187
+ args.successCriteria,
188
+ 'visible success state, no console errors, and expected network behavior'
189
+ );
190
+
191
+ return createUserPrompt(
192
+ 'Browser Bridge MCP user-flow verification workflow.',
193
+ [
194
+ 'Verify a user flow in the current real browser tab with Browser Bridge MCP.',
195
+ '',
196
+ `Flow: ${flow}`,
197
+ `Success criteria: ${successCriteria}`,
198
+ '',
199
+ 'Workflow:',
200
+ '1. Call `browser_status` and resolve access if needed.',
201
+ '2. Read `browser_page` state so you know the current URL and title before interacting.',
202
+ '3. Locate controls semantically with `browser_dom` `find_role` or `find_text`; reuse returned `elementRef` values.',
203
+ '4. Interact with `browser_input` actions such as `click`, `type`, `set_checked`, `select_option`, and `press_key`.',
204
+ '5. After navigation or UI changes, wait with `browser_dom` action `wait` or `browser_page` action `wait_for_load`.',
205
+ '6. Verify final page text/DOM plus `browser_page` console and network if the flow depends on API calls.',
206
+ '7. Do not create new tabs unless the user requested a fresh page or the flow requires one.',
207
+ '',
208
+ 'Report the verified result, evidence, and any blocking failures.',
209
+ ].join('\n')
210
+ );
211
+ }
212
+
213
+ /**
214
+ * @param {string} description
215
+ * @param {string} text
216
+ * @returns {GetPromptResult}
217
+ */
218
+ function createUserPrompt(description, text) {
219
+ return {
220
+ description,
221
+ messages: [
222
+ {
223
+ role: 'user',
224
+ content: {
225
+ type: 'text',
226
+ text,
227
+ },
228
+ },
229
+ ],
230
+ };
231
+ }
232
+
233
+ /**
234
+ * @param {string | undefined} value
235
+ * @param {string} fallback
236
+ * @returns {string}
237
+ */
238
+ function normalizeTextArg(value, fallback) {
239
+ const text = typeof value === 'string' ? value.trim() : '';
240
+ return text || fallback;
241
+ }
@@ -4,7 +4,7 @@ import {
4
4
  dispatchToolAction,
5
5
  getToolTokenBudget,
6
6
  REQUEST_SOURCE,
7
- requestBridge,
7
+ requestBridgeWithRetry,
8
8
  resolveToolRef,
9
9
  resolveRef,
10
10
  summarizeToolError,
@@ -40,22 +40,63 @@ export const CAPTURE_ACTIONS = {
40
40
  params: () => ({}),
41
41
  },
42
42
  cdp_box_model: {
43
- ref: true,
43
+ ref: false,
44
44
  method: 'cdp.get_box_model',
45
- params: (_, r) => ({ elementRef: r }),
45
+ params: (a) => ({ nodeId: a.nodeId }),
46
46
  },
47
47
  cdp_computed_styles: {
48
- ref: true,
48
+ ref: false,
49
49
  method: 'cdp.get_computed_styles_for_node',
50
- params: (_, r) => ({ elementRef: r }),
50
+ params: (a) => ({ nodeId: a.nodeId }),
51
51
  },
52
52
  };
53
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 {unknown} value
61
+ * @returns {value is number}
62
+ */
63
+ function isFiniteNumber(value) {
64
+ return typeof value === 'number' && Number.isFinite(value);
65
+ }
66
+
67
+ /** @param {unknown} rect */
68
+ function isValidCaptureRegion(rect) {
69
+ if (!rect || typeof rect !== 'object' || Array.isArray(rect)) {
70
+ return false;
71
+ }
72
+ const candidate = /** @type {Record<string, unknown>} */ (rect);
73
+ return (
74
+ isFiniteNumber(candidate.x) &&
75
+ isFiniteNumber(candidate.y) &&
76
+ isFiniteNumber(candidate.width) &&
77
+ candidate.width > 0 &&
78
+ isFiniteNumber(candidate.height) &&
79
+ candidate.height > 0
80
+ );
81
+ }
82
+
54
83
  /**
55
- * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
84
+ * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, nodeId?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
56
85
  * @returns {Promise<ToolResult>}
57
86
  */
58
87
  export async function handleCaptureTool(args) {
88
+ if (args.action === 'region' && !isValidCaptureRegion(args.rect)) {
89
+ return summarizeToolError(
90
+ 'rect with finite x, y, width, and height is required for region capture.'
91
+ );
92
+ }
93
+ if (
94
+ isCdpNodeCapture(args) &&
95
+ (typeof args.nodeId !== 'number' || !Number.isFinite(args.nodeId))
96
+ ) {
97
+ return summarizeToolError('nodeId must be a finite number.');
98
+ }
99
+
59
100
  return dispatchToolAction(CAPTURE_ACTIONS, args, 'capture');
60
101
  }
61
102
 
@@ -78,6 +119,21 @@ export const INPUT_ACTION_METHODS = {
78
119
  * @returns {Promise<ToolResult>}
79
120
  */
80
121
  export async function handleInputTool(args) {
122
+ if (args.action === 'type' && !hasText(args.text)) {
123
+ return summarizeToolError('text is required for input.type.');
124
+ }
125
+ if ((args.action === 'press_key' || args.action === 'cdp_press_key') && !hasText(args.key)) {
126
+ return summarizeToolError('key is required for key input actions.');
127
+ }
128
+ if (
129
+ args.action === 'select_option' &&
130
+ !hasNonEmptyArray(args.values) &&
131
+ !hasNonEmptyArray(args.labels) &&
132
+ !hasNonEmptyArray(args.indexes)
133
+ ) {
134
+ return summarizeToolError('values, labels, or indexes are required for input.select_option.');
135
+ }
136
+
81
137
  return withToolClient(async (client) => {
82
138
  const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
83
139
  const elementTarget = async () => ({
@@ -86,13 +142,14 @@ export async function handleInputTool(args) {
86
142
 
87
143
  switch (args.action) {
88
144
  case 'click': {
89
- const response = await requestBridge(
145
+ const response = await requestBridgeWithRetry(
90
146
  client,
91
147
  'input.click',
92
148
  {
93
149
  target: await elementTarget(),
94
150
  button: args.button,
95
151
  clickCount: args.clickCount,
152
+ modifiers: args.modifiers,
96
153
  },
97
154
  {
98
155
  tabId: requestedTabId,
@@ -103,7 +160,7 @@ export async function handleInputTool(args) {
103
160
  return summarizeToolResponse(response, 'input.click');
104
161
  }
105
162
  case 'focus': {
106
- const response = await requestBridge(
163
+ const response = await requestBridgeWithRetry(
107
164
  client,
108
165
  'input.focus',
109
166
  {
@@ -118,7 +175,7 @@ export async function handleInputTool(args) {
118
175
  return summarizeToolResponse(response, 'input.focus');
119
176
  }
120
177
  case 'type': {
121
- const response = await requestBridge(
178
+ const response = await requestBridgeWithRetry(
122
179
  client,
123
180
  'input.type',
124
181
  {
@@ -126,6 +183,7 @@ export async function handleInputTool(args) {
126
183
  text: args.text,
127
184
  clear: args.clear,
128
185
  submit: args.submit,
186
+ modifiers: args.modifiers,
129
187
  },
130
188
  {
131
189
  tabId: requestedTabId,
@@ -137,7 +195,7 @@ export async function handleInputTool(args) {
137
195
  }
138
196
  case 'press_key': {
139
197
  const target = args.elementRef || args.selector ? await elementTarget() : undefined;
140
- const response = await requestBridge(
198
+ const response = await requestBridgeWithRetry(
141
199
  client,
142
200
  'input.press_key',
143
201
  {
@@ -154,7 +212,7 @@ export async function handleInputTool(args) {
154
212
  return summarizeToolResponse(response, 'input.press_key');
155
213
  }
156
214
  case 'cdp_press_key': {
157
- const response = await requestBridge(
215
+ const response = await requestBridgeWithRetry(
158
216
  client,
159
217
  'cdp.dispatch_key_event',
160
218
  {
@@ -171,7 +229,7 @@ export async function handleInputTool(args) {
171
229
  return summarizeToolResponse(response, 'cdp.dispatch_key_event');
172
230
  }
173
231
  case 'set_checked': {
174
- const response = await requestBridge(
232
+ const response = await requestBridgeWithRetry(
175
233
  client,
176
234
  'input.set_checked',
177
235
  {
@@ -187,7 +245,7 @@ export async function handleInputTool(args) {
187
245
  return summarizeToolResponse(response, 'input.set_checked');
188
246
  }
189
247
  case 'select_option': {
190
- const response = await requestBridge(
248
+ const response = await requestBridgeWithRetry(
191
249
  client,
192
250
  'input.select_option',
193
251
  {
@@ -205,12 +263,13 @@ export async function handleInputTool(args) {
205
263
  return summarizeToolResponse(response, 'input.select_option');
206
264
  }
207
265
  case 'hover': {
208
- const response = await requestBridge(
266
+ const response = await requestBridgeWithRetry(
209
267
  client,
210
268
  'input.hover',
211
269
  {
212
270
  target: await elementTarget(),
213
271
  duration: args.duration,
272
+ modifiers: args.modifiers,
214
273
  },
215
274
  {
216
275
  tabId: requestedTabId,
@@ -240,7 +299,7 @@ export async function handleInputTool(args) {
240
299
  'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
241
300
  );
242
301
  }
243
- const response = await requestBridge(
302
+ const response = await requestBridgeWithRetry(
244
303
  client,
245
304
  'input.drag',
246
305
  {
@@ -258,7 +317,7 @@ export async function handleInputTool(args) {
258
317
  return summarizeToolResponse(response, 'input.drag');
259
318
  }
260
319
  case 'scroll_into_view': {
261
- const response = await requestBridge(
320
+ const response = await requestBridgeWithRetry(
262
321
  client,
263
322
  'input.scroll_into_view',
264
323
  {
@@ -277,3 +336,19 @@ export async function handleInputTool(args) {
277
336
  }
278
337
  });
279
338
  }
339
+
340
+ /**
341
+ * @param {unknown} value
342
+ * @returns {boolean}
343
+ */
344
+ function hasText(value) {
345
+ return typeof value === 'string' && value.trim().length > 0;
346
+ }
347
+
348
+ /**
349
+ * @param {unknown} value
350
+ * @returns {boolean}
351
+ */
352
+ function hasNonEmptyArray(value) {
353
+ return Array.isArray(value) && value.length > 0;
354
+ }