@browserbridge/bbx 1.7.8 → 1.8.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 (32) hide show
  1. package/PRIVACY.md +30 -11
  2. package/README.md +9 -5
  3. package/package.json +9 -6
  4. package/packages/agent-client/src/cli-args.js +24 -0
  5. package/packages/agent-client/src/cli.js +46 -52
  6. package/packages/agent-client/src/client.js +140 -5
  7. package/packages/agent-client/src/command-registry.js +56 -53
  8. package/packages/agent-client/src/content-extract.js +198 -0
  9. package/packages/agent-client/src/remotes.js +1 -1
  10. package/packages/agent-client/src/runtime.js +768 -35
  11. package/packages/agent-client/src/types.ts +131 -6
  12. package/packages/mcp-server/src/guidance.js +1 -1
  13. package/packages/mcp-server/src/handlers-capture.js +41 -5
  14. package/packages/mcp-server/src/handlers-dom.js +8 -2
  15. package/packages/mcp-server/src/handlers-navigation.js +2 -1
  16. package/packages/mcp-server/src/handlers-page.js +41 -4
  17. package/packages/mcp-server/src/handlers-utils.js +67 -7
  18. package/packages/mcp-server/src/handlers.js +4 -4
  19. package/packages/mcp-server/src/server.js +102 -6
  20. package/packages/native-host/src/daemon.js +158 -6
  21. package/packages/native-host/src/native-host.js +3 -1
  22. package/packages/protocol/src/defaults.js +3 -0
  23. package/packages/protocol/src/errors.js +54 -1
  24. package/packages/protocol/src/protocol.js +297 -1
  25. package/packages/protocol/src/registry.js +66 -35
  26. package/packages/protocol/src/summary.js +87 -5
  27. package/packages/protocol/src/types.ts +234 -0
  28. package/skills/browser-bridge/SKILL.md +34 -13
  29. package/skills/browser-bridge/references/interaction.md +55 -11
  30. package/skills/browser-bridge/references/patch-workflow.md +2 -0
  31. package/skills/browser-bridge/references/protocol.md +157 -82
  32. package/skills/browser-bridge/references/token-efficiency.md +19 -13
@@ -169,6 +169,10 @@ export function createBridgeMcpServer() {
169
169
  description: 'Ping the bridge to verify daemon and extension connectivity.',
170
170
  inputSchema: {
171
171
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
172
+ intent: z
173
+ .enum(['inspect', 'interact', 'capture', 'navigate', 'debugger', 'general'])
174
+ .optional()
175
+ .describe('Reported operation family shown in the access prompt (defaults to general).'),
172
176
  },
173
177
  },
174
178
  handleHealthTool
@@ -206,7 +210,7 @@ export function createBridgeMcpServer() {
206
210
  {
207
211
  title: 'Browser DOM',
208
212
  description:
209
- 'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For full-page text, use browser_page action "text". accessibility_tree is debugger-backed - use query/find first.',
213
+ 'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For full-page text, use browser_page action "text". accessibility_tree is debugger-backed and depth-limited - use query/find first.',
210
214
  inputSchema: {
211
215
  action: z
212
216
  .enum([
@@ -230,7 +234,9 @@ export function createBridgeMcpServer() {
230
234
  selector: z
231
235
  .string()
232
236
  .optional()
233
- .describe('CSS selector (used if no elementRef; resolves to first match)'),
237
+ .describe(
238
+ 'CSS selector (used if no elementRef; accessibility_tree requires exactly one match)'
239
+ ),
234
240
  elementRef: z
235
241
  .string()
236
242
  .optional()
@@ -248,6 +254,16 @@ export function createBridgeMcpServer() {
248
254
  .positive()
249
255
  .optional()
250
256
  .describe(`Maximum tree depth (default: ${DEFAULT_MAX_DEPTH})`),
257
+ compact: z
258
+ .boolean()
259
+ .optional()
260
+ .describe('Remove ignored, empty, and decorative AX nodes while preserving descendants'),
261
+ interactiveOnly: z
262
+ .boolean()
263
+ .optional()
264
+ .describe(
265
+ 'Return semantically interactive AX nodes; actionable state is reported separately'
266
+ ),
251
267
  textBudget: z
252
268
  .number()
253
269
  .int()
@@ -327,7 +343,9 @@ export function createBridgeMcpServer() {
327
343
  properties: z
328
344
  .array(z.string())
329
345
  .optional()
330
- .describe('Style properties to fetch (omitting returns all - expensive)'),
346
+ .describe(
347
+ 'Style properties to fetch (omitting returns display, position, width, height, and color)'
348
+ ),
331
349
  x: z
332
350
  .number()
333
351
  .nonnegative()
@@ -348,16 +366,18 @@ export function createBridgeMcpServer() {
348
366
  {
349
367
  title: 'Browser Page State',
350
368
  description:
351
- 'Read page-level data: state (URL/title), evaluate (JS), console, storage, text, network, or performance. For element-level reads, use browser_dom. evaluate and performance are debugger-backed - prefer lighter reads first.',
369
+ 'Read page-level data, wait for load/URL conditions, or explicitly inspect/handle JavaScript dialogs. For element-level reads, use browser_dom. evaluate, performance, handle_dialog, and source=cdp network capture are debugger-backed.',
352
370
  inputSchema: {
353
371
  action: z
354
372
  .enum([
355
373
  'state',
356
374
  'evaluate',
357
375
  'console',
376
+ 'handle_dialog',
358
377
  'wait_for_load',
359
378
  'storage',
360
379
  'text',
380
+ 'extract_content',
361
381
  'network',
362
382
  'performance',
363
383
  ])
@@ -373,6 +393,23 @@ export function createBridgeMcpServer() {
373
393
  .optional()
374
394
  .describe('JavaScript expression to evaluate (for evaluate action)'),
375
395
  awaitPromise: z.boolean().optional().describe('Await returned promises (default: false)'),
396
+ dialogAction: z
397
+ .enum(['inspect', 'accept', 'dismiss'])
398
+ .optional()
399
+ .describe('Dialog operation on the current dialog (default: inspect)'),
400
+ promptText: z
401
+ .string()
402
+ .max(10_000)
403
+ .optional()
404
+ .describe('Prompt response text; only valid when dialogAction is accept'),
405
+ expectedDialogId: z
406
+ .string()
407
+ .min(1)
408
+ .max(128)
409
+ .optional()
410
+ .describe(
411
+ 'Optional stale-decision check immediately before CDP dispatch; Chrome cannot atomically bind the action to this identifier'
412
+ ),
376
413
  timeoutMs: z
377
414
  .number()
378
415
  .int()
@@ -412,7 +449,43 @@ export function createBridgeMcpServer() {
412
449
  .positive()
413
450
  .optional()
414
451
  .describe(`Max chars for page text (default: ${DEFAULT_PAGE_TEXT_BUDGET})`),
452
+ format: z
453
+ .enum(['text', 'markdown'])
454
+ .optional()
455
+ .describe('Semantic content format (default: text)'),
456
+ selector: z.string().optional().describe('Scope semantic extraction to one CSS selector'),
457
+ includeMetadata: z
458
+ .boolean()
459
+ .optional()
460
+ .describe('Include bounded article metadata (default: true)'),
461
+ consistency: z
462
+ .enum(['best_effort', 'settled'])
463
+ .optional()
464
+ .describe('Optionally wait for a short mutation-quiet interval'),
465
+ settleTimeoutMs: z
466
+ .number()
467
+ .int()
468
+ .positive()
469
+ .optional()
470
+ .describe('Maximum semantic extraction settlement wait (default: 2000)'),
415
471
  urlPattern: z.string().optional().describe('Filter network entries by URL pattern'),
472
+ source: z
473
+ .enum(['fetch-xhr', 'cdp'])
474
+ .optional()
475
+ .describe('Network source (default: fetch-xhr; cdp captures all resource types)'),
476
+ capture: z
477
+ .enum(['read', 'start', 'clear', 'stop'])
478
+ .optional()
479
+ .describe('Explicit CDP capture lifecycle action (default: read)'),
480
+ waitForLoad: z
481
+ .boolean()
482
+ .optional()
483
+ .describe('For wait_for_load, also require tab status complete (default: true)'),
484
+ url: z.string().optional().describe('URL condition for wait_for_load'),
485
+ urlMatch: z
486
+ .enum(['exact', 'contains', 'regex'])
487
+ .optional()
488
+ .describe('URL match mode for wait_for_load (default: exact)'),
416
489
  },
417
490
  },
418
491
  handlePageTool
@@ -467,7 +540,7 @@ export function createBridgeMcpServer() {
467
540
  {
468
541
  title: 'Browser Input',
469
542
  description:
470
- 'Simulate user input: click, focus, type, press keys, CDP key events, set checked, select options, hover, drag, or scroll into view. Reuse elementRef from prior queries.',
543
+ 'Dispatch browser input. Targeted click, focus, type, fill, press_key, set_checked, select_option, hover, and drag actions perform actionability checks and report resolution/execution metadata. cdp_press_key and scroll_into_view use separate contracts. Reuse elementRef values and verify application state after mutations.',
471
544
  inputSchema: {
472
545
  action: z
473
546
  .enum([
@@ -512,6 +585,18 @@ export function createBridgeMcpServer() {
512
585
  .enum(['auto', 'setter', 'keystrokes'])
513
586
  .optional()
514
587
  .describe('Fill strategy (default: auto)'),
588
+ executionMode: z
589
+ .enum(['dom', 'cdp'])
590
+ .optional()
591
+ .describe(
592
+ 'Input execution path (default: dom; CDP supports click, hover, drag, type, fill)'
593
+ ),
594
+ recoverStale: z
595
+ .boolean()
596
+ .optional()
597
+ .describe(
598
+ 'Strict bounded same-document stale elementRef recovery for targeted input actions (default: false; not used by cdp_press_key or scroll_into_view)'
599
+ ),
515
600
  clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
516
601
  submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
517
602
  key: z
@@ -567,7 +652,7 @@ export function createBridgeMcpServer() {
567
652
  {
568
653
  title: 'Browser Patch',
569
654
  description:
570
- 'Apply or rollback reversible style and DOM patches for live prototyping before editing source. Set verify=true to get computed results inline without a follow-up query.',
655
+ 'Apply or rollback document-local style and DOM patches for live prototyping before editing source. commit_baseline keeps current changes but discards rollback history. Set verify=true to get computed results inline.',
571
656
  inputSchema: {
572
657
  action: z
573
658
  .enum(['apply_styles', 'apply_dom', 'list', 'rollback', 'commit_baseline'])
@@ -652,6 +737,17 @@ export function createBridgeMcpServer() {
652
737
  })
653
738
  .optional()
654
739
  .describe('Viewport region for region action (keep crop tight)'),
740
+ format: z
741
+ .enum(['png', 'jpeg', 'webp'])
742
+ .optional()
743
+ .describe('Screenshot encoding (default: png)'),
744
+ quality: z
745
+ .number()
746
+ .int()
747
+ .min(0)
748
+ .max(100)
749
+ .optional()
750
+ .describe('JPEG/WebP quality from 0 to 100; ignored for PNG'),
655
751
  },
656
752
  },
657
753
  handleCaptureTool
@@ -53,7 +53,7 @@ setProtocolPackageVersion(DAEMON_VERSION);
53
53
  /** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
54
54
  /** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
55
55
  /** @typedef {'agent' | 'extension'} SocketRole */
56
- /** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
56
+ /** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __browserExtensionId?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
57
57
  /** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, protocolVersion?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
58
58
  /**
59
59
  * @typedef {{
@@ -85,6 +85,151 @@ function compareProtocolVersions(left, right) {
85
85
  return 0;
86
86
  }
87
87
 
88
+ /**
89
+ * @param {unknown} value
90
+ * @returns {string | undefined}
91
+ */
92
+ function normalizeBrowserExtensionId(value) {
93
+ return typeof value === 'string' && /^[a-p]{32}$/u.test(value) ? value : undefined;
94
+ }
95
+
96
+ const HEALTH_DIAGNOSTIC_COUNT_LIMIT = 10_000;
97
+ const HEALTH_ACCESS_REASONS = new Set([
98
+ 'enabled',
99
+ 'access_disabled',
100
+ 'enabled_window_missing',
101
+ 'no_routable_active_tab',
102
+ 'restricted_page',
103
+ ]);
104
+ const HEALTH_DEBUGGER_STATUSES = new Set(['idle', 'active']);
105
+ const HEALTH_DEBUGGER_REASONS = new Set([
106
+ 'debugger_conflict',
107
+ 'debugger_detached',
108
+ 'debugger_replaced',
109
+ 'debugger_canceled',
110
+ 'target_closed',
111
+ ]);
112
+ const HEALTH_CAPTURE_STATES = new Set(['stopped', 'armed', 'active', 'stop_failed']);
113
+
114
+ /**
115
+ * @param {unknown} value
116
+ * @returns {Record<string, unknown> | null}
117
+ */
118
+ function asHealthRecord(value) {
119
+ return value && typeof value === 'object' && !Array.isArray(value)
120
+ ? /** @type {Record<string, unknown>} */ (value)
121
+ : null;
122
+ }
123
+
124
+ /**
125
+ * @param {unknown} value
126
+ * @returns {number | undefined}
127
+ */
128
+ function normalizeHealthCount(value) {
129
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
130
+ ? Math.min(HEALTH_DIAGNOSTIC_COUNT_LIMIT, Math.trunc(value))
131
+ : undefined;
132
+ }
133
+
134
+ /**
135
+ * Copy only extension-owned health fields into the daemon response. This keeps
136
+ * transport, connectivity, daemon identity, and daemon protocol negotiation
137
+ * authoritative even if an extension response is malformed or hostile.
138
+ *
139
+ * @param {unknown} value
140
+ * @returns {Record<string, unknown>}
141
+ */
142
+ function normalizeExtensionHealthResult(value) {
143
+ const result = asHealthRecord(value);
144
+ if (!result) return {};
145
+
146
+ /** @type {Record<string, unknown>} */
147
+ const normalized = {};
148
+ if (result.extension === 'ok') normalized.extension = 'ok';
149
+
150
+ const access = asHealthRecord(result.access);
151
+ if (access) {
152
+ /** @type {Record<string, unknown>} */
153
+ const normalizedAccess = {};
154
+ if (typeof access.enabled === 'boolean') normalizedAccess.enabled = access.enabled;
155
+ if (
156
+ access.windowId === null ||
157
+ (Number.isSafeInteger(access.windowId) && Number(access.windowId) > 0)
158
+ ) {
159
+ normalizedAccess.windowId = access.windowId;
160
+ }
161
+ if (
162
+ access.routeTabId === null ||
163
+ (Number.isSafeInteger(access.routeTabId) && Number(access.routeTabId) > 0)
164
+ ) {
165
+ normalizedAccess.routeTabId = access.routeTabId;
166
+ }
167
+ if (typeof access.routeReady === 'boolean') normalizedAccess.routeReady = access.routeReady;
168
+ if (typeof access.reason === 'string' && HEALTH_ACCESS_REASONS.has(access.reason)) {
169
+ normalizedAccess.reason = access.reason;
170
+ }
171
+ normalized.access = normalizedAccess;
172
+ }
173
+
174
+ const debuggerDiagnostics = asHealthRecord(result.debugger);
175
+ if (debuggerDiagnostics) {
176
+ /** @type {Record<string, unknown>} */
177
+ const normalizedDebugger = {};
178
+ if (
179
+ typeof debuggerDiagnostics.status === 'string' &&
180
+ HEALTH_DEBUGGER_STATUSES.has(debuggerDiagnostics.status)
181
+ ) {
182
+ normalizedDebugger.status = debuggerDiagnostics.status;
183
+ }
184
+ for (const key of ['attachedTabCount', 'heldTabCount', 'pendingTabCount']) {
185
+ const count = normalizeHealthCount(debuggerDiagnostics[key]);
186
+ if (count !== undefined) normalizedDebugger[key] = count;
187
+ }
188
+ if (
189
+ typeof debuggerDiagnostics.recentReason === 'string' &&
190
+ HEALTH_DEBUGGER_REASONS.has(debuggerDiagnostics.recentReason)
191
+ ) {
192
+ normalizedDebugger.recentReason = debuggerDiagnostics.recentReason;
193
+ } else if (debuggerDiagnostics.recentReason === null) {
194
+ normalizedDebugger.recentReason = null;
195
+ }
196
+ normalized.debugger = normalizedDebugger;
197
+ }
198
+
199
+ const capture = asHealthRecord(result.capture);
200
+ if (capture) {
201
+ /** @type {Record<string, unknown>} */
202
+ const normalizedCapture = {};
203
+ if (typeof capture.state === 'string' && HEALTH_CAPTURE_STATES.has(capture.state)) {
204
+ normalizedCapture.state = capture.state;
205
+ }
206
+ for (const key of [
207
+ 'activeTabCount',
208
+ 'ownershipCount',
209
+ 'inflightCount',
210
+ 'interceptionActiveTabCount',
211
+ 'interceptionRuleCount',
212
+ ]) {
213
+ const count = normalizeHealthCount(capture[key]);
214
+ if (count !== undefined) normalizedCapture[key] = count;
215
+ }
216
+ normalized.capture = normalizedCapture;
217
+ }
218
+
219
+ const extensionSupportedVersions = Array.isArray(result.supported_versions)
220
+ ? result.supported_versions
221
+ .filter(
222
+ (version) =>
223
+ typeof version === 'string' &&
224
+ version.length <= 32 &&
225
+ /^\d+\.\d+(?:\.\d+)?(?:[-+][A-Za-z0-9.-]+)?$/u.test(version)
226
+ )
227
+ .slice(0, 10)
228
+ : [];
229
+ normalized.extension_supported_versions = extensionSupportedVersions;
230
+ return normalized;
231
+ }
232
+
88
233
  /**
89
234
  * @param {string | undefined} requestedVersion
90
235
  * @returns {{ supported_versions: readonly string[], deprecated_since?: string, migration_hint?: string }}
@@ -140,6 +285,7 @@ export function isWindowsNamedPipePath(socketPath) {
140
285
  * response?: import('../../protocol/src/types.js').BridgeResponse,
141
286
  * browserName?: string,
142
287
  * profileLabel?: string,
288
+ * browserExtensionId?: string,
143
289
  * accessEnabled?: boolean,
144
290
  * authToken?: string,
145
291
  * at?: number
@@ -192,7 +338,7 @@ export class BridgeDaemon {
192
338
  this.pendingTimeoutMs = DEFAULT_DAEMON_PENDING_TIMEOUT_MS;
193
339
  /** @type {Record<string, unknown>[]} */
194
340
  this.recentLog = [];
195
- /** @type {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }> | null} */
341
+ /** @type {Array<{ extensionId: string, browserExtensionId: string | null, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }> | null} */
196
342
  this.connectedExtensionsCache = null;
197
343
  /** @type {Promise<void> | null} */
198
344
  this.stopPromise = null;
@@ -302,7 +448,7 @@ export class BridgeDaemon {
302
448
  }
303
449
 
304
450
  /**
305
- * @returns {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }>}
451
+ * @returns {Array<{ extensionId: string, browserExtensionId: string | null, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }>}
306
452
  */
307
453
  getConnectedExtensionsSnapshot() {
308
454
  if (this.connectedExtensionsCache) {
@@ -312,6 +458,7 @@ export class BridgeDaemon {
312
458
  this.connectedExtensionsCache = Array.from(this.extensionSockets.entries()).map(
313
459
  ([extensionId, extSocket]) => ({
314
460
  extensionId,
461
+ browserExtensionId: extSocket.__browserExtensionId ?? null,
315
462
  browserName: extSocket.__browserName ?? null,
316
463
  profileLabel: extSocket.__profileLabel ?? null,
317
464
  accessEnabled: extSocket.__accessEnabled ?? false,
@@ -373,6 +520,7 @@ export class BridgeDaemon {
373
520
  typeof message.browserName === 'string' ? message.browserName : undefined;
374
521
  socket.__profileLabel =
375
522
  typeof message.profileLabel === 'string' ? message.profileLabel : undefined;
523
+ socket.__browserExtensionId = normalizeBrowserExtensionId(message.browserExtensionId);
376
524
  socket.__lastActiveAt = Date.now();
377
525
  this.extensionSockets.set(extensionId, socket);
378
526
  this.invalidateConnectedExtensionsCache();
@@ -975,6 +1123,11 @@ export class BridgeDaemon {
975
1123
  changed = changed || socket.__profileLabel !== message.profileLabel;
976
1124
  socket.__profileLabel = message.profileLabel;
977
1125
  }
1126
+ if (typeof message.browserExtensionId === 'string') {
1127
+ const browserExtensionId = normalizeBrowserExtensionId(message.browserExtensionId);
1128
+ changed = changed || socket.__browserExtensionId !== browserExtensionId;
1129
+ socket.__browserExtensionId = browserExtensionId;
1130
+ }
978
1131
  if (changed) {
979
1132
  this.invalidateConnectedExtensionsCache();
980
1133
  }
@@ -1061,15 +1214,14 @@ export class BridgeDaemon {
1061
1214
  socketPath: this.socketPath,
1062
1215
  transport: formatBridgeTransport(this.transport),
1063
1216
  connectedExtensions: this.getConnectedExtensionsSnapshot(),
1217
+ ...normalizeExtensionHealthResult(responseMessage.result),
1064
1218
  ...getVersionNegotiationPayload(pending.protocolVersion),
1065
- .../** @type {Record<string, unknown>} */ (responseMessage.result),
1066
1219
  daemonVersion: DAEMON_VERSION,
1067
1220
  daemon_supported_versions: getSupportedProtocolVersions(),
1068
1221
  proxy: this.getProxyStatusPayload(),
1069
1222
  },
1070
1223
  {
1071
- ...responseMessage.meta,
1072
- method: responseMessage.meta?.method ?? pending.method,
1224
+ method: pending.method,
1073
1225
  }
1074
1226
  )
1075
1227
  : responseMessage;
@@ -28,7 +28,8 @@ import { createNativeMessageReader, createNativeMessageWriter, writeJsonLine } f
28
28
  * @typedef {{
29
29
  * type?: string,
30
30
  * browserName?: string,
31
- * profileLabel?: string
31
+ * profileLabel?: string,
32
+ * browserExtensionId?: string
32
33
  * }} HostIdentityMessage
33
34
  */
34
35
 
@@ -238,6 +239,7 @@ export async function runNativeHost({
238
239
  type: 'extension.identity',
239
240
  browserName: message.browserName,
240
241
  profileLabel: message.profileLabel,
242
+ browserExtensionId: message.browserExtensionId,
241
243
  });
242
244
  return;
243
245
  }
@@ -11,6 +11,9 @@ export const DEFAULT_MAX_NODES = 25;
11
11
  export const DEFAULT_MAX_DEPTH = 4;
12
12
  export const DEFAULT_TEXT_BUDGET = 600;
13
13
  export const DEFAULT_PAGE_TEXT_BUDGET = 8000;
14
+ export const DEFAULT_EXTRACT_SETTLE_TIMEOUT_MS = 2000;
15
+ export const MAX_EXTRACT_SETTLE_TIMEOUT_MS = 10_000;
16
+ export const EXTRACT_SETTLE_QUIET_MS = 100;
14
17
  export const DEFAULT_WAIT_TIMEOUT_MS = 5000;
15
18
  export const DEFAULT_EVAL_TIMEOUT_MS = 5000;
16
19
  export const DEFAULT_NAV_TIMEOUT_MS = 15000;
@@ -6,6 +6,15 @@ export const ERROR_CODES = Object.freeze({
6
6
  ACCESS_DENIED: 'ACCESS_DENIED',
7
7
  TAB_MISMATCH: 'TAB_MISMATCH',
8
8
  ELEMENT_STALE: 'ELEMENT_STALE',
9
+ ELEMENT_AMBIGUOUS: 'ELEMENT_AMBIGUOUS',
10
+ ELEMENT_NOT_ACTIONABLE: 'ELEMENT_NOT_ACTIONABLE',
11
+ ELEMENT_OBSCURED: 'ELEMENT_OBSCURED',
12
+ ELEMENT_NOT_FOUND: 'ELEMENT_NOT_FOUND',
13
+ INPUT_UNSUPPORTED: 'INPUT_UNSUPPORTED',
14
+ INPUT_INVALID_TARGET: 'INPUT_INVALID_TARGET',
15
+ INPUT_FOCUS_CHANGED: 'INPUT_FOCUS_CHANGED',
16
+ DIALOG_NOT_OPEN: 'DIALOG_NOT_OPEN',
17
+ DIALOG_ACTION_CONFLICT: 'DIALOG_ACTION_CONFLICT',
9
18
  RESULT_TRUNCATED: 'RESULT_TRUNCATED',
10
19
  RATE_LIMITED: 'RATE_LIMITED',
11
20
  INTERNAL_ERROR: 'INTERNAL_ERROR',
@@ -32,7 +41,51 @@ export const ERROR_RECOVERY = Object.freeze({
32
41
  [ERROR_CODES.ELEMENT_STALE]: {
33
42
  retry: false,
34
43
  alternativeMethod: 'dom.query',
35
- hint: 'Element was removed from the DOM. Re-query with the same selector to get a fresh elementRef.',
44
+ hint: 'Element was removed from the DOM. Re-query for a fresh elementRef, or retry an input once with recoverStale=true when the same page and a strong unique descriptor still apply.',
45
+ },
46
+ [ERROR_CODES.ELEMENT_AMBIGUOUS]: {
47
+ retry: false,
48
+ alternativeMethod: 'dom.query',
49
+ hint: 'Multiple candidates matched, candidates tied, or a bounded stale-recovery scan could not prove uniqueness. Re-query and use a more specific selector or fresh elementRef.',
50
+ },
51
+ [ERROR_CODES.ELEMENT_NOT_ACTIONABLE]: {
52
+ retry: false,
53
+ alternativeMethod: 'dom.describe',
54
+ hint: 'The target is hidden, disabled, inert, or has no rendered area. Inspect it and choose an actionable target.',
55
+ },
56
+ [ERROR_CODES.ELEMENT_OBSCURED]: {
57
+ retry: false,
58
+ alternativeMethod: 'layout.hit_test',
59
+ hint: 'Another element blocks the target point. Inspect the blocker or wait for the overlay to close.',
60
+ },
61
+ [ERROR_CODES.ELEMENT_NOT_FOUND]: {
62
+ retry: false,
63
+ alternativeMethod: 'dom.query',
64
+ hint: 'No element matched the target selector. Check or narrow the selector with dom.query.',
65
+ },
66
+ [ERROR_CODES.INPUT_UNSUPPORTED]: {
67
+ retry: false,
68
+ hint: 'The requested execution mode does not support this input operation. Use executionMode=dom or choose a CDP-supported input method.',
69
+ },
70
+ [ERROR_CODES.INPUT_INVALID_TARGET]: {
71
+ retry: false,
72
+ alternativeMethod: 'dom.describe',
73
+ hint: 'The target is not compatible with this input operation or the requested option does not exist. Inspect the target and choose the appropriate control.',
74
+ },
75
+ [ERROR_CODES.INPUT_FOCUS_CHANGED]: {
76
+ retry: false,
77
+ alternativeMethod: 'dom.describe',
78
+ hint: 'Focus moved away from the resolved editable target before native text dispatch. Inspect focus handlers or target the control that retained focus.',
79
+ },
80
+ [ERROR_CODES.DIALOG_NOT_OPEN]: {
81
+ retry: false,
82
+ alternativeMethod: 'page.handle_dialog',
83
+ hint: 'No JavaScript dialog is currently observable. Inspect or trigger the dialog before trying another explicit action.',
84
+ },
85
+ [ERROR_CODES.DIALOG_ACTION_CONFLICT]: {
86
+ retry: false,
87
+ alternativeMethod: 'page.handle_dialog',
88
+ hint: 'The observed dialog changed before or during CDP dispatch. Do not repeat the action automatically; inspect the current dialog state first.',
36
89
  },
37
90
  [ERROR_CODES.TAB_MISMATCH]: {
38
91
  retry: false,