@browserbridge/bbx 1.3.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.
package/README.md CHANGED
@@ -110,7 +110,9 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
110
110
  3. Run `bbx install`, or target a specific browser with `bbx install --browser edge`, `bbx install --browser brave`, `bbx install --browser chromium`, or `bbx install --browser arc`
111
111
  4. In the extension side panel, install MCP or CLI (skill) for your agent of choice
112
112
  5. Enable Browser Bridge for the browser window you want to inspect/control with the AI agent
113
- 6. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the `browser-bridge` / `$bbx` skill in CLI mode
113
+ 6. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the installed Browser Bridge skill in CLI mode (`/browser-bridge`, `browser-bridge`, or the client-specific skill trigger)
114
+
115
+ MCP mode is self-contained: the server exposes tools, startup instructions, and prompt templates, so a separate CLI skill is not required for MCP guidance.
114
116
 
115
117
  ## How it works
116
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -61,7 +61,7 @@
61
61
  "postinstall": "node packages/native-host/bin/postinstall.js",
62
62
  "package:extension": "node scripts/package-extension.mjs",
63
63
  "check:extension-zip": "node scripts/check-extension-zip.mjs",
64
- "release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm pack --dry-run",
64
+ "release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm run check:extension-zip && npm pack --dry-run",
65
65
  "prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run coverage:check",
66
66
  "status": "node packages/agent-client/src/cli.js status",
67
67
  "daemon": "node packages/native-host/bin/bridge-daemon.js",
@@ -128,15 +128,21 @@ if (command === 'install-skill') {
128
128
  const positional = rest.filter((a) => !a.startsWith('--'));
129
129
 
130
130
  if (positional.length === 0) {
131
- // Parse scope flags without going through parseInstallAgentArgs.
132
- let isGlobal = true;
133
- if (rest.includes('--local')) isGlobal = false;
134
- if (rest.includes('--global')) isGlobal = true;
131
+ let scopeOptions;
132
+ try {
133
+ scopeOptions = parseInstallAgentArgs(rest);
134
+ } catch (error) {
135
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
136
+ process.exit(1);
137
+ }
138
+
139
+ const isGlobal = scopeOptions.global !== false;
140
+ const projectPath = isGlobal ? os.homedir() : scopeOptions.projectPath;
135
141
 
136
142
  const setupStatus = await collectSetupStatus({
137
143
  global: isGlobal,
138
144
  cwd: process.cwd(),
139
- projectPath: isGlobal ? os.homedir() : process.cwd(),
145
+ projectPath,
140
146
  ...getSetupStatusTestOverrides(),
141
147
  });
142
148
  /** @type {import('./types.js').SupportedTarget[]} */
@@ -176,7 +182,6 @@ if (command === 'install-skill') {
176
182
  targets = /** @type {import('./types.js').SupportedTarget[]} */ (selected);
177
183
  }
178
184
 
179
- const projectPath = isGlobal ? os.homedir() : process.cwd();
180
185
  if (selected !== null) {
181
186
  const deselectedTargets =
182
187
  /** @type {import('./types.js').SupportedTarget[]} */ (
@@ -226,20 +231,32 @@ if (command === 'install-skill') {
226
231
  }
227
232
 
228
233
  if (command === 'install-mcp') {
229
- const argsLeft = [...rest];
230
234
  let isGlobal = true;
235
+ /** @type {string[]} */
236
+ const positionals = [];
231
237
 
232
- const localIdx = argsLeft.indexOf('--local');
233
- if (localIdx !== -1) {
234
- isGlobal = false;
235
- argsLeft.splice(localIdx, 1);
238
+ for (const arg of rest) {
239
+ if (arg === '--local') {
240
+ isGlobal = false;
241
+ continue;
242
+ }
243
+ if (arg === '--global') {
244
+ isGlobal = true;
245
+ continue;
246
+ }
247
+ if (arg.startsWith('--')) {
248
+ process.stderr.write(`Unknown install-mcp option "${arg}".\n`);
249
+ process.exit(1);
250
+ }
251
+ positionals.push(arg);
236
252
  }
237
- const globalIdx = argsLeft.indexOf('--global');
238
- if (globalIdx !== -1) {
239
- argsLeft.splice(globalIdx, 1);
253
+
254
+ if (positionals.length > 1) {
255
+ process.stderr.write(`Unexpected extra argument "${positionals[1]}".\n`);
256
+ process.exit(1);
240
257
  }
241
258
 
242
- const clientArg = argsLeft[0];
259
+ const clientArg = positionals[0];
243
260
 
244
261
  /** @type {import('./types.js').McpClientName[]} */
245
262
  let clients;
@@ -15,6 +15,7 @@ import {
15
15
  getBridgeTransport,
16
16
  getSocketPath,
17
17
  } from '../../native-host/src/config.js';
18
+ import { normalizeBridgeAuthToken, readBridgeAuthToken } from '../../native-host/src/auth-token.js';
18
19
  import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
19
20
 
20
21
  /** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
@@ -56,6 +57,22 @@ function createTimeoutError(method, timeoutMs) {
56
57
  return error;
57
58
  }
58
59
 
60
+ /**
61
+ * @param {net.Socket} socket
62
+ * @param {string} line
63
+ * @returns {Promise<void>}
64
+ */
65
+ async function writeSocketLine(socket, line) {
66
+ if (!socket.write(line)) {
67
+ await Promise.race([
68
+ once(socket, 'drain'),
69
+ once(socket, 'close').then(() => {
70
+ throw new Error('Bridge socket closed while writing.');
71
+ }),
72
+ ]);
73
+ }
74
+ }
75
+
59
76
  export class BridgeClient extends EventEmitter {
60
77
  /**
61
78
  * @param {BridgeClientOptions} [options={}]
@@ -68,6 +85,7 @@ export class BridgeClient extends EventEmitter {
68
85
  autoReconnect = false,
69
86
  restartDaemonOnVersionMismatch = true,
70
87
  restartDaemonFn = restartBridgeDaemon,
88
+ authToken = undefined,
71
89
  } = {}) {
72
90
  super();
73
91
  this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
@@ -78,6 +96,7 @@ export class BridgeClient extends EventEmitter {
78
96
  this.autoReconnect = autoReconnect;
79
97
  this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
80
98
  this.restartDaemonFn = restartDaemonFn;
99
+ this.authToken = authToken;
81
100
  this.socket = null;
82
101
  this.connected = false;
83
102
  this.protocolCompatibility = null;
@@ -111,6 +130,18 @@ export class BridgeClient extends EventEmitter {
111
130
  throw error;
112
131
  }
113
132
 
133
+ const registrationPromise = new Promise((resolve, reject) => {
134
+ const timeoutId = setTimeout(() => {
135
+ this.waiting.delete('registered');
136
+ reject(createTimeoutError('register', this.defaultTimeoutMs));
137
+ }, this.defaultTimeoutMs);
138
+ this.waiting.set('registered', {
139
+ resolve,
140
+ reject,
141
+ timeoutId,
142
+ });
143
+ });
144
+
114
145
  parseJsonLines(socket, (raw) => {
115
146
  const message = /** @type {ClientMessage} */ (raw);
116
147
  if (message.type === 'registered') {
@@ -124,6 +155,16 @@ export class BridgeClient extends EventEmitter {
124
155
  return;
125
156
  }
126
157
 
158
+ if (message.type === 'registration_failed') {
159
+ const pending = this.waiting.get('registered');
160
+ if (pending) {
161
+ this.waiting.delete('registered');
162
+ clearTimeout(pending.timeoutId);
163
+ pending.reject(new Error(message.error?.message || 'Bridge daemon registration failed.'));
164
+ }
165
+ return;
166
+ }
167
+
127
168
  if (message.type === 'agent.response') {
128
169
  const pending = this.waiting.get(message.response.id);
129
170
  if (pending) {
@@ -148,20 +189,31 @@ export class BridgeClient extends EventEmitter {
148
189
  // 'close' fires after 'error'; reconnect is triggered there.
149
190
  });
150
191
 
151
- this.socket.write(
152
- `${JSON.stringify({ type: 'register', role: 'agent', clientId: this.clientId })}\n`
153
- );
154
- await new Promise((resolve, reject) => {
155
- const timeoutId = setTimeout(() => {
192
+ const authToken =
193
+ this.authToken === undefined
194
+ ? this.transport.type === 'tcp'
195
+ ? await readBridgeAuthToken()
196
+ : null
197
+ : normalizeBridgeAuthToken(this.authToken);
198
+ try {
199
+ await writeSocketLine(
200
+ socket,
201
+ `${JSON.stringify({
202
+ type: 'register',
203
+ role: 'agent',
204
+ clientId: this.clientId,
205
+ ...(authToken ? { authToken } : {}),
206
+ })}\n`
207
+ );
208
+ } catch (error) {
209
+ const pending = this.waiting.get('registered');
210
+ if (pending) {
211
+ clearTimeout(pending.timeoutId);
156
212
  this.waiting.delete('registered');
157
- reject(createTimeoutError('register', this.defaultTimeoutMs));
158
- }, this.defaultTimeoutMs);
159
- this.waiting.set('registered', {
160
- resolve,
161
- reject,
162
- timeoutId,
163
- });
164
- });
213
+ }
214
+ throw error;
215
+ }
216
+ await registrationPromise;
165
217
 
166
218
  this.protocolCompatibility = null;
167
219
  this.protocolWarning = null;
@@ -244,13 +296,15 @@ export class BridgeClient extends EventEmitter {
244
296
  });
245
297
  });
246
298
 
247
- if (!this.socket.write(`${JSON.stringify({ type: 'agent.request', request })}\n`)) {
248
- await Promise.race([
249
- once(this.socket, 'drain'),
250
- once(this.socket, 'close').then(() => {
251
- throw new Error('Bridge socket closed while writing.');
252
- }),
253
- ]);
299
+ try {
300
+ await writeSocketLine(this.socket, `${JSON.stringify({ type: 'agent.request', request })}\n`);
301
+ } catch (error) {
302
+ const pending = this.waiting.get(request.id);
303
+ if (pending) {
304
+ clearTimeout(pending.timeoutId);
305
+ this.waiting.delete(request.id);
306
+ }
307
+ throw error;
254
308
  }
255
309
  const response = /** @type {BridgeResponse} */ (await responsePromise);
256
310
  return this.attachProtocolWarning(response);
@@ -237,11 +237,10 @@ export const CLI_HELP_SECTIONS = Object.freeze([
237
237
  {
238
238
  title: 'Setup',
239
239
  lines: [
240
- 'bbx install [--browser chrome|edge|brave|chromium|arc] [extension-id] Install native messaging manifest',
240
+ 'bbx install [extension-id] [--browser chrome|edge|brave|chromium|arc] [--all] Install native messaging manifest',
241
241
  'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
242
- 'bbx install [--all] [--browser <name>] [extension-id] Install native host manifest (--all for all supported browsers)',
243
242
  'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
244
- 'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf',
243
+ 'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
245
244
  'bbx status Check bridge connection',
246
245
  'bbx doctor Diagnose install, daemon, extension, and access readiness',
247
246
  'bbx restart Restart the local Browser Bridge daemon',
@@ -523,8 +523,13 @@ async function installJsonMcpConfig(clientName, configPath, stdout) {
523
523
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
524
524
  existing = parsed;
525
525
  }
526
- } catch {
527
- // File missing or unparseable - start fresh.
526
+ } catch (error) {
527
+ if (!isMissingFileError(error)) {
528
+ throw new Error(
529
+ `Cannot update ${configPath}: existing MCP config is not valid JSON. Fix or remove it first.`
530
+ );
531
+ }
532
+ // File missing - start fresh.
528
533
  }
529
534
 
530
535
  const shape = getMcpConfigShape(clientName);
@@ -661,3 +666,15 @@ export function parseInstalledMcpConfig(clientName, raw) {
661
666
  return { configured: false };
662
667
  }
663
668
  }
669
+
670
+ /**
671
+ * @param {unknown} error
672
+ * @returns {boolean}
673
+ */
674
+ function isMissingFileError(error) {
675
+ return Boolean(
676
+ error &&
677
+ typeof error === 'object' &&
678
+ /** @type {{ code?: unknown }} */ (error).code === 'ENOENT'
679
+ );
680
+ }
@@ -54,6 +54,13 @@ export type ClientMessage =
54
54
  role: 'agent' | 'extension';
55
55
  clientId?: string;
56
56
  }
57
+ | {
58
+ type: 'registration_failed';
59
+ error?: {
60
+ code?: string;
61
+ message?: string;
62
+ };
63
+ }
57
64
  | {
58
65
  type: 'agent.response';
59
66
  response: BridgeResponse;
@@ -73,6 +80,7 @@ export interface BridgeClientOptions {
73
80
  autoReconnect?: boolean;
74
81
  restartDaemonOnVersionMismatch?: boolean;
75
82
  restartDaemonFn?: typeof restartBridgeDaemon;
83
+ authToken?: string | null;
76
84
  }
77
85
 
78
86
  export interface ShortcutCommand {
@@ -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
+ }