browser-debug-mcp-bridge 1.11.1 → 1.13.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 (33) hide show
  1. package/README.md +4 -2
  2. package/apps/mcp-server/dist/db/automation-repository.js +9 -4
  3. package/apps/mcp-server/dist/db/automation-repository.js.map +1 -1
  4. package/apps/mcp-server/dist/db/migrations.js +300 -1
  5. package/apps/mcp-server/dist/db/migrations.js.map +1 -1
  6. package/apps/mcp-server/dist/db/schema.js +226 -2
  7. package/apps/mcp-server/dist/db/schema.js.map +1 -1
  8. package/apps/mcp-server/dist/lighthouse-report.js +1001 -0
  9. package/apps/mcp-server/dist/lighthouse-report.js.map +1 -0
  10. package/apps/mcp-server/dist/main.js +249 -1
  11. package/apps/mcp-server/dist/main.js.map +1 -1
  12. package/apps/mcp-server/dist/mcp/server.js +3705 -311
  13. package/apps/mcp-server/dist/mcp/server.js.map +1 -1
  14. package/apps/mcp-server/dist/mcp/target-resolution.js +390 -0
  15. package/apps/mcp-server/dist/mcp/target-resolution.js.map +1 -0
  16. package/apps/mcp-server/dist/mcp/tool-loop-guard.js +655 -0
  17. package/apps/mcp-server/dist/mcp/tool-loop-guard.js.map +1 -0
  18. package/apps/mcp-server/dist/mock-store.js +408 -0
  19. package/apps/mcp-server/dist/mock-store.js.map +1 -0
  20. package/apps/mcp-server/dist/override-audit-contract.js +58 -0
  21. package/apps/mcp-server/dist/override-audit-contract.js.map +1 -1
  22. package/apps/mcp-server/dist/override-audit.js +100 -4
  23. package/apps/mcp-server/dist/override-audit.js.map +1 -1
  24. package/apps/mcp-server/dist/override-poc.js +4 -4
  25. package/apps/mcp-server/dist/override-poc.js.map +1 -1
  26. package/apps/mcp-server/dist/override-profile-generator.js +3 -9
  27. package/apps/mcp-server/dist/override-profile-generator.js.map +1 -1
  28. package/apps/mcp-server/dist/ssr-mock.js +480 -0
  29. package/apps/mcp-server/dist/ssr-mock.js.map +1 -0
  30. package/apps/mcp-server/dist/websocket/messages.js +5 -0
  31. package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
  32. package/apps/mcp-server/package.json +5 -0
  33. package/package.json +9 -4
@@ -5,8 +5,10 @@ import { createHash, randomUUID } from 'crypto';
5
5
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
6
6
  import { dirname, resolve } from 'path';
7
7
  import { z } from 'zod';
8
+ import { WorkflowTargetResolutionError, hasSemanticActionTargetMatcher, resolveWorkflowActionTarget, summarizeWorkflowTargetMatcher, } from './target-resolution.js';
8
9
  import { getConnection } from '../db/connection.js';
9
- import { diagnoseOverridePoc, insertOverridePlanAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, } from '../override-audit.js';
10
+ import { diagnoseOverridePoc, insertOverridePlanAudit, insertSsrMockAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, listSsrMockAudits, } from '../override-audit.js';
11
+ import { deleteMockRoute, getMockRoute, listMockHits, listMockRoutes, listMockRuns, upsertMockRoute } from '../mock-store.js';
10
12
  import { createOverrideProfileConfig, OVERRIDE_PROFILE_ADAPTERS, } from '../override-profile-generator.js';
11
13
  import { assertOverrideResponseRequestCaptureSafe, classifyOverrideResponseRequestCapability, } from '../override-capabilities.js';
12
14
  import { getOverridePocConfigSummary } from '../override-poc.js';
@@ -15,6 +17,9 @@ import { mapNextOverrideAssetsWithDrift } from '../next-asset-mapper.js';
15
17
  import { planNextSourceOverride } from '../next-source-override-planner.js';
16
18
  import { listObservedOverrideAssets, persistObservedOverrideAssets } from '../override-observed-assets.js';
17
19
  import { planOverrideResponsePatch } from '../override-response-planner.js';
20
+ import { applySsrMockConfig, discoverSsrMockability, removeSsrMockConfig } from '../ssr-mock.js';
21
+ import { getLighthouseReport, getLighthouseReportAsset, listLighthouseReports, normalizeLighthouseAsset, planLighthouseFixes, runLighthouseReport, } from '../lighthouse-report.js';
22
+ import { createToolLoopGuard } from './tool-loop-guard.js';
18
23
  function createDefaultMcpLogger() {
19
24
  const write = (level, message, payload) => {
20
25
  process.stderr.write(`${message} ${JSON.stringify({ level, ...payload })}\n`);
@@ -31,12 +36,93 @@ function createDefaultMcpLogger() {
31
36
  },
32
37
  };
33
38
  }
39
+ const UIActionTargetScopeSchema = z.enum(['buttons', 'links', 'inputs', 'modals', 'focused']);
40
+ const UIActionLocatorMatcherSchema = z.union([
41
+ z.string().min(1),
42
+ z.object({
43
+ pattern: z.string().min(1),
44
+ flags: z.string().regex(/^[imsu]*$/).optional(),
45
+ }),
46
+ ]);
47
+ const UIActionLocatorStepSchema = z.object({
48
+ kind: z.enum(['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText']),
49
+ value: UIActionLocatorMatcherSchema.optional(),
50
+ role: z.string().min(1).optional(),
51
+ name: UIActionLocatorMatcherSchema.optional(),
52
+ exact: z.boolean().optional(),
53
+ relation: z.enum(['filter', 'descendant', 'ancestor']).optional(),
54
+ }).superRefine((value, ctx) => {
55
+ if (value.kind === 'role' && !value.role && !value.value) {
56
+ ctx.addIssue({
57
+ code: z.ZodIssueCode.custom,
58
+ message: 'role locator step requires role or value',
59
+ path: ['role'],
60
+ });
61
+ }
62
+ if (value.kind !== 'role' && !value.value) {
63
+ ctx.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ message: `${value.kind} locator step requires value`,
66
+ path: ['value'],
67
+ });
68
+ }
69
+ });
70
+ const UIActionLocatorSchema = z.object({
71
+ scope: UIActionTargetScopeSchema.optional(),
72
+ frame: z.object({
73
+ selector: z.string().min(1).optional(),
74
+ urlContains: z.string().min(1).optional(),
75
+ titleContains: z.string().min(1).optional(),
76
+ }).optional(),
77
+ steps: z.array(UIActionLocatorStepSchema).min(1).max(8),
78
+ });
79
+ const UIActionCoordinateTargetSchema = z.object({
80
+ x: z.number().finite(),
81
+ y: z.number().finite(),
82
+ frameId: z.number().int().min(0).optional(),
83
+ });
34
84
  const LiveUIActionTargetSchema = z.object({
35
85
  selector: z.string().min(1).optional(),
36
86
  elementRef: z.string().min(1).optional(),
87
+ coordinates: UIActionCoordinateTargetSchema.optional(),
37
88
  tabId: z.number().int().min(0).optional(),
38
89
  frameId: z.number().int().min(0).optional(),
39
90
  url: z.string().url().optional(),
91
+ locator: UIActionLocatorSchema.optional(),
92
+ frameUrlContains: z.string().min(1).optional(),
93
+ frameTitleContains: z.string().min(1).optional(),
94
+ testId: z.string().min(1).optional(),
95
+ scope: UIActionTargetScopeSchema.optional(),
96
+ textContains: z.string().min(1).optional(),
97
+ labelContains: z.string().min(1).optional(),
98
+ titleContains: z.string().min(1).optional(),
99
+ role: z.string().min(1).optional(),
100
+ name: z.string().min(1).optional(),
101
+ placeholder: z.string().min(1).optional(),
102
+ altText: z.string().min(1).optional(),
103
+ tagName: z.string().min(1).optional(),
104
+ type: z.string().min(1).optional(),
105
+ exact: z.boolean().optional(),
106
+ nth: z.number().int().min(0).optional(),
107
+ first: z.boolean().optional(),
108
+ last: z.boolean().optional(),
109
+ strict: z.boolean().optional(),
110
+ visible: z.boolean().optional(),
111
+ disabled: z.boolean().optional(),
112
+ selected: z.boolean().optional(),
113
+ pressed: z.boolean().optional(),
114
+ expanded: z.boolean().optional(),
115
+ readOnly: z.boolean().optional(),
116
+ requiredField: z.boolean().optional(),
117
+ }).superRefine((value, ctx) => {
118
+ const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
119
+ if (positionFields > 1) {
120
+ ctx.addIssue({
121
+ code: z.ZodIssueCode.custom,
122
+ message: 'target can use only one of nth, first, or last',
123
+ path: ['target'],
124
+ });
125
+ }
40
126
  });
41
127
  const LiveUIActionBaseSchema = z.object({
42
128
  traceId: z.string().min(1).optional(),
@@ -50,6 +136,10 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
50
136
  clickCount: z.number().int().min(1).max(3).optional(),
51
137
  }).optional(),
52
138
  }),
139
+ LiveUIActionBaseSchema.extend({
140
+ action: z.literal('hover'),
141
+ input: z.object({}).optional(),
142
+ }),
53
143
  LiveUIActionBaseSchema.extend({
54
144
  action: z.literal('input'),
55
145
  input: z.object({
@@ -95,20 +185,34 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
95
185
  ]);
96
186
  const UIWorkflowModeSchema = z.enum(['safe', 'fast']);
97
187
  const UIWorkflowFailureStrategySchema = z.enum(['stop', 'continue', 'retry_once']);
98
- const UIWorkflowActionTargetScopeSchema = z.enum(['buttons', 'inputs', 'modals', 'focused']);
188
+ const UIWorkflowActionTargetScopeSchema = UIActionTargetScopeSchema;
99
189
  const UIWorkflowActionTargetSchema = z.object({
100
190
  selector: z.string().min(1).optional(),
101
191
  elementRef: z.string().min(1).optional(),
192
+ coordinates: UIActionCoordinateTargetSchema.optional(),
102
193
  tabId: z.number().int().min(0).optional(),
103
194
  frameId: z.number().int().min(0).optional(),
104
195
  url: z.string().url().optional(),
196
+ locator: UIActionLocatorSchema.optional(),
197
+ frameUrlContains: z.string().min(1).optional(),
198
+ frameTitleContains: z.string().min(1).optional(),
105
199
  testId: z.string().min(1).optional(),
106
200
  scope: UIWorkflowActionTargetScopeSchema.optional(),
107
201
  textContains: z.string().min(1).optional(),
108
202
  labelContains: z.string().min(1).optional(),
109
203
  titleContains: z.string().min(1).optional(),
204
+ role: z.string().min(1).optional(),
205
+ name: z.string().min(1).optional(),
206
+ placeholder: z.string().min(1).optional(),
207
+ altText: z.string().min(1).optional(),
110
208
  tagName: z.string().min(1).optional(),
111
209
  type: z.string().min(1).optional(),
210
+ exact: z.boolean().optional(),
211
+ nth: z.number().int().min(0).optional(),
212
+ first: z.boolean().optional(),
213
+ last: z.boolean().optional(),
214
+ strict: z.boolean().optional(),
215
+ visible: z.boolean().optional(),
112
216
  disabled: z.boolean().optional(),
113
217
  selected: z.boolean().optional(),
114
218
  pressed: z.boolean().optional(),
@@ -118,13 +222,28 @@ const UIWorkflowActionTargetSchema = z.object({
118
222
  }).superRefine((value, ctx) => {
119
223
  if (!value.selector
120
224
  && !value.elementRef
225
+ && !value.coordinates
226
+ && !value.locator
227
+ && !value.scope
121
228
  && !value.testId
122
229
  && !value.textContains
123
230
  && !value.labelContains
124
- && !value.titleContains) {
231
+ && !value.titleContains
232
+ && !value.role
233
+ && !value.name
234
+ && !value.placeholder
235
+ && !value.altText) {
236
+ ctx.addIssue({
237
+ code: z.ZodIssueCode.custom,
238
+ message: 'target requires selector, elementRef, coordinates, locator, scope, testId, textContains, labelContains, titleContains, role, name, placeholder, or altText',
239
+ path: ['target'],
240
+ });
241
+ }
242
+ const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
243
+ if (positionFields > 1) {
125
244
  ctx.addIssue({
126
245
  code: z.ZodIssueCode.custom,
127
- message: 'target requires selector, elementRef, testId, textContains, labelContains, or titleContains',
246
+ message: 'target can use only one of nth, first, or last',
128
247
  path: ['target'],
129
248
  });
130
249
  }
@@ -163,6 +282,10 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
163
282
  clickCount: z.number().int().min(1).max(3).optional(),
164
283
  }).optional(),
165
284
  }),
285
+ UIWorkflowActionBaseSchema.extend({
286
+ action: z.literal('hover'),
287
+ input: z.object({}).optional(),
288
+ }),
166
289
  UIWorkflowActionBaseSchema.extend({
167
290
  action: z.literal('input'),
168
291
  input: z.object({
@@ -207,14 +330,22 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
207
330
  }),
208
331
  ]);
209
332
  const UIWorkflowPageStateMatcherSchema = z.object({
210
- scope: z.enum(['buttons', 'inputs', 'modals', 'focused', 'page']),
333
+ scope: z.enum(['buttons', 'links', 'inputs', 'modals', 'focused', 'page']),
211
334
  selector: z.string().optional(),
212
335
  testId: z.string().optional(),
213
336
  textContains: z.string().optional(),
214
337
  labelContains: z.string().optional(),
215
338
  titleContains: z.string().optional(),
339
+ role: z.string().optional(),
340
+ name: z.string().optional(),
341
+ placeholder: z.string().optional(),
342
+ altText: z.string().optional(),
343
+ exact: z.boolean().optional(),
344
+ frameUrlContains: z.string().optional(),
345
+ frameTitleContains: z.string().optional(),
216
346
  urlContains: z.string().optional(),
217
347
  language: z.string().optional(),
348
+ visible: z.boolean().optional(),
218
349
  disabled: z.boolean().optional(),
219
350
  selected: z.boolean().optional(),
220
351
  pressed: z.boolean().optional(),
@@ -247,10 +378,201 @@ const UIWorkflowAssertStepSchema = UIWorkflowStepBaseSchema.extend({
247
378
  kind: z.literal('assert'),
248
379
  matcher: UIWorkflowPageStateMatcherSchema,
249
380
  });
381
+ const AutomationWaitBaseSchema = z.object({
382
+ timeoutMs: z.number().int().min(100).max(120000).optional(),
383
+ pollIntervalMs: z.number().int().min(50).max(5000).optional(),
384
+ });
385
+ const AutomationWaitUrlSchema = AutomationWaitBaseSchema.extend({
386
+ waitKind: z.literal('url'),
387
+ urlContains: z.string().min(1).optional(),
388
+ urlRegex: z.string().min(1).optional(),
389
+ exactUrl: z.string().min(1).optional(),
390
+ }).superRefine((value, ctx) => {
391
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl) {
392
+ ctx.addIssue({
393
+ code: z.ZodIssueCode.custom,
394
+ message: 'url wait requires urlContains, urlRegex, or exactUrl',
395
+ path: ['wait'],
396
+ });
397
+ }
398
+ });
399
+ const AutomationWaitNavigationSchema = AutomationWaitBaseSchema.extend({
400
+ waitKind: z.literal('navigation'),
401
+ urlContains: z.string().min(1).optional(),
402
+ urlRegex: z.string().min(1).optional(),
403
+ exactUrl: z.string().min(1).optional(),
404
+ fromUrlContains: z.string().min(1).optional(),
405
+ fromUrlRegex: z.string().min(1).optional(),
406
+ trigger: z.string().min(1).optional(),
407
+ sinceTs: z.number().int().min(0).optional(),
408
+ tabId: z.number().int().min(0).optional(),
409
+ }).superRefine((value, ctx) => {
410
+ if (!value.urlContains
411
+ && !value.urlRegex
412
+ && !value.exactUrl
413
+ && !value.fromUrlContains
414
+ && !value.fromUrlRegex
415
+ && !value.trigger) {
416
+ ctx.addIssue({
417
+ code: z.ZodIssueCode.custom,
418
+ message: 'navigation wait requires a URL, from-URL, or trigger predicate',
419
+ path: ['wait'],
420
+ });
421
+ }
422
+ });
423
+ const AutomationWaitNavigationLifecycleSchema = AutomationWaitBaseSchema.extend({
424
+ waitKind: z.literal('navigation_lifecycle'),
425
+ state: z.enum(['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle']).default('load'),
426
+ urlContains: z.string().min(1).optional(),
427
+ urlRegex: z.string().min(1).optional(),
428
+ exactUrl: z.string().min(1).optional(),
429
+ tabId: z.number().int().min(0).optional(),
430
+ });
431
+ const AutomationWaitLoadStateSchema = AutomationWaitBaseSchema.extend({
432
+ waitKind: z.literal('load_state'),
433
+ state: z.enum(['domcontentloaded', 'load']).default('load'),
434
+ urlContains: z.string().min(1).optional(),
435
+ urlRegex: z.string().min(1).optional(),
436
+ exactUrl: z.string().min(1).optional(),
437
+ });
438
+ const AutomationWaitSelectorStateSchema = AutomationWaitBaseSchema.extend({
439
+ waitKind: z.literal('selector_state'),
440
+ selector: z.string().min(1),
441
+ state: z.enum(['attached', 'detached', 'visible', 'hidden']).default('visible'),
442
+ frameId: z.number().int().min(0).default(0),
443
+ });
444
+ const AutomationWaitConsoleSchema = AutomationWaitBaseSchema.extend({
445
+ waitKind: z.literal('console'),
446
+ levels: z.array(z.string().min(1)).optional(),
447
+ contains: z.string().min(1).optional(),
448
+ sinceTs: z.number().int().min(0).optional(),
449
+ includeRuntimeErrors: z.boolean().optional(),
450
+ });
451
+ const AutomationWaitDialogSchema = AutomationWaitBaseSchema.extend({
452
+ waitKind: z.literal('dialog'),
453
+ type: z.enum(['alert', 'confirm', 'prompt', 'beforeunload']).optional(),
454
+ messageContains: z.string().min(1).optional(),
455
+ urlContains: z.string().min(1).optional(),
456
+ action: z.enum(['none', 'accept', 'dismiss']).default('none'),
457
+ promptText: z.string().optional(),
458
+ tabId: z.number().int().min(0).optional(),
459
+ });
460
+ const AutomationWaitStableLayoutSchema = AutomationWaitBaseSchema.extend({
461
+ waitKind: z.literal('stable_layout'),
462
+ selector: z.string().min(1).optional(),
463
+ stableMs: z.number().int().min(100).max(10000).default(500),
464
+ tabId: z.number().int().min(0).optional(),
465
+ });
466
+ const AutomationWaitDownloadSchema = AutomationWaitBaseSchema.extend({
467
+ waitKind: z.literal('download'),
468
+ urlContains: z.string().min(1).optional(),
469
+ urlRegex: z.string().min(1).optional(),
470
+ exactUrl: z.string().min(1).optional(),
471
+ filenameContains: z.string().min(1).optional(),
472
+ filenameRegex: z.string().min(1).optional(),
473
+ state: z.enum(['started', 'completed']).default('started'),
474
+ tabId: z.number().int().min(0).optional(),
475
+ }).superRefine((value, ctx) => {
476
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.filenameContains && !value.filenameRegex) {
477
+ ctx.addIssue({
478
+ code: z.ZodIssueCode.custom,
479
+ message: 'download wait requires a URL or filename predicate',
480
+ path: ['wait'],
481
+ });
482
+ }
483
+ });
484
+ const AutomationWaitPopupSchema = AutomationWaitBaseSchema.extend({
485
+ waitKind: z.literal('popup'),
486
+ urlContains: z.string().min(1).optional(),
487
+ urlRegex: z.string().min(1).optional(),
488
+ exactUrl: z.string().min(1).optional(),
489
+ openerTabId: z.number().int().min(0).optional(),
490
+ }).superRefine((value, ctx) => {
491
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && value.openerTabId === undefined) {
492
+ ctx.addIssue({
493
+ code: z.ZodIssueCode.custom,
494
+ message: 'popup wait requires a URL predicate or openerTabId',
495
+ path: ['wait'],
496
+ });
497
+ }
498
+ });
499
+ const AutomationWaitNetworkQuietSchema = AutomationWaitBaseSchema.extend({
500
+ waitKind: z.literal('network_quiet'),
501
+ quietMs: z.number().int().min(100).max(10000).default(500),
502
+ urlContains: z.string().min(1).optional(),
503
+ method: z.string().min(1).optional(),
504
+ tabId: z.number().int().min(0).optional(),
505
+ });
506
+ const AutomationWaitNetworkBaseSchema = AutomationWaitBaseSchema.extend({
507
+ urlContains: z.string().min(1).optional(),
508
+ urlRegex: z.string().min(1).optional(),
509
+ exactUrl: z.string().min(1).optional(),
510
+ method: z.string().min(1).optional(),
511
+ traceId: z.string().min(1).optional(),
512
+ initiator: z.enum(['fetch', 'xhr', 'img', 'script', 'other']).optional(),
513
+ requestContentType: z.string().min(1).optional(),
514
+ sinceTs: z.number().int().min(0).optional(),
515
+ tabId: z.number().int().min(0).optional(),
516
+ includeBodies: z.boolean().optional(),
517
+ });
518
+ const AutomationWaitRequestSchema = AutomationWaitNetworkBaseSchema.extend({
519
+ waitKind: z.literal('request'),
520
+ }).superRefine((value, ctx) => {
521
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
522
+ ctx.addIssue({
523
+ code: z.ZodIssueCode.custom,
524
+ message: 'request wait requires urlContains, urlRegex, exactUrl, or traceId',
525
+ path: ['wait'],
526
+ });
527
+ }
528
+ });
529
+ const AutomationWaitResponseSchema = AutomationWaitNetworkBaseSchema.extend({
530
+ waitKind: z.literal('response'),
531
+ statusIn: z.array(z.number().int().min(100).max(599)).optional(),
532
+ statusGte: z.number().int().min(100).max(599).optional(),
533
+ statusLt: z.number().int().min(100).max(600).optional(),
534
+ responseContentType: z.string().min(1).optional(),
535
+ errorType: z.string().min(1).optional(),
536
+ }).superRefine((value, ctx) => {
537
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
538
+ ctx.addIssue({
539
+ code: z.ZodIssueCode.custom,
540
+ message: 'response wait requires urlContains, urlRegex, exactUrl, or traceId',
541
+ path: ['wait'],
542
+ });
543
+ }
544
+ if (value.statusGte !== undefined && value.statusLt !== undefined && value.statusGte >= value.statusLt) {
545
+ ctx.addIssue({
546
+ code: z.ZodIssueCode.custom,
547
+ message: 'statusGte must be less than statusLt',
548
+ path: ['statusGte'],
549
+ });
550
+ }
551
+ });
552
+ const AutomationWaitSpecSchema = z.discriminatedUnion('waitKind', [
553
+ AutomationWaitUrlSchema,
554
+ AutomationWaitNavigationSchema,
555
+ AutomationWaitNavigationLifecycleSchema,
556
+ AutomationWaitLoadStateSchema,
557
+ AutomationWaitSelectorStateSchema,
558
+ AutomationWaitConsoleSchema,
559
+ AutomationWaitDialogSchema,
560
+ AutomationWaitStableLayoutSchema,
561
+ AutomationWaitDownloadSchema,
562
+ AutomationWaitPopupSchema,
563
+ AutomationWaitNetworkQuietSchema,
564
+ AutomationWaitRequestSchema,
565
+ AutomationWaitResponseSchema,
566
+ ]);
567
+ const UIWorkflowGenericWaitStepSchema = UIWorkflowStepBaseSchema.extend({
568
+ kind: z.literal('wait'),
569
+ wait: AutomationWaitSpecSchema,
570
+ });
250
571
  const UIWorkflowStepSchema = z.discriminatedUnion('kind', [
251
572
  UIWorkflowActionStepSchema,
252
573
  UIWorkflowWaitForStepSchema,
253
574
  UIWorkflowAssertStepSchema,
575
+ UIWorkflowGenericWaitStepSchema,
254
576
  ]);
255
577
  const RunUIStepsSchema = z.object({
256
578
  sessionId: z.string().min(1),
@@ -263,6 +585,91 @@ const RunUIStepsSchema = z.object({
263
585
  function createUIWorkflowTraceId() {
264
586
  return `uiworkflow-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
265
587
  }
588
+ const LOCATOR_MATCHER_TOOL_SCHEMA = {
589
+ anyOf: [
590
+ { type: 'string' },
591
+ {
592
+ type: 'object',
593
+ required: ['pattern'],
594
+ properties: {
595
+ pattern: { type: 'string' },
596
+ flags: { type: 'string' },
597
+ },
598
+ },
599
+ ],
600
+ };
601
+ const ACTION_LOCATOR_TOOL_SCHEMA = {
602
+ type: 'object',
603
+ required: ['steps'],
604
+ properties: {
605
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
606
+ frame: {
607
+ type: 'object',
608
+ properties: {
609
+ selector: { type: 'string' },
610
+ urlContains: { type: 'string' },
611
+ titleContains: { type: 'string' },
612
+ },
613
+ },
614
+ steps: {
615
+ type: 'array',
616
+ minItems: 1,
617
+ maxItems: 8,
618
+ items: {
619
+ type: 'object',
620
+ required: ['kind'],
621
+ properties: {
622
+ kind: { type: 'string', enum: ['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText'] },
623
+ value: LOCATOR_MATCHER_TOOL_SCHEMA,
624
+ role: { type: 'string' },
625
+ name: LOCATOR_MATCHER_TOOL_SCHEMA,
626
+ exact: { type: 'boolean' },
627
+ relation: { type: 'string', enum: ['filter', 'descendant', 'ancestor'] },
628
+ },
629
+ },
630
+ },
631
+ },
632
+ };
633
+ const AUTOMATION_WAIT_TOOL_SCHEMA = {
634
+ type: 'object',
635
+ required: ['waitKind'],
636
+ properties: {
637
+ waitKind: { type: 'string', enum: ['url', 'navigation', 'navigation_lifecycle', 'load_state', 'selector_state', 'console', 'dialog', 'stable_layout', 'download', 'popup', 'network_quiet', 'request', 'response'] },
638
+ timeoutMs: { type: 'number' },
639
+ pollIntervalMs: { type: 'number' },
640
+ urlContains: { type: 'string' },
641
+ urlRegex: { type: 'string' },
642
+ exactUrl: { type: 'string' },
643
+ fromUrlContains: { type: 'string' },
644
+ fromUrlRegex: { type: 'string' },
645
+ trigger: { type: 'string' },
646
+ state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle', 'attached', 'detached', 'visible', 'hidden', 'started', 'completed'] },
647
+ selector: { type: 'string' },
648
+ frameId: { type: 'number' },
649
+ levels: { type: 'array', items: { type: 'string' } },
650
+ contains: { type: 'string' },
651
+ sinceTs: { type: 'number' },
652
+ includeRuntimeErrors: { type: 'boolean' },
653
+ action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
654
+ promptText: { type: 'string' },
655
+ stableMs: { type: 'number' },
656
+ filenameContains: { type: 'string' },
657
+ filenameRegex: { type: 'string' },
658
+ openerTabId: { type: 'number' },
659
+ quietMs: { type: 'number' },
660
+ method: { type: 'string' },
661
+ traceId: { type: 'string' },
662
+ initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
663
+ requestContentType: { type: 'string' },
664
+ responseContentType: { type: 'string' },
665
+ statusIn: { type: 'array', items: { type: 'number' } },
666
+ statusGte: { type: 'number' },
667
+ statusLt: { type: 'number' },
668
+ errorType: { type: 'string' },
669
+ includeBodies: { type: 'boolean' },
670
+ tabId: { type: 'number' },
671
+ },
672
+ };
266
673
  const TOOL_SCHEMAS = {
267
674
  list_sessions: {
268
675
  type: 'object',
@@ -451,6 +858,7 @@ const TOOL_SCHEMAS = {
451
858
  properties: {
452
859
  sessionId: { type: 'string' },
453
860
  selector: { type: 'string' },
861
+ frameId: { type: 'number' },
454
862
  properties: { type: 'array', items: { type: 'string' } },
455
863
  },
456
864
  },
@@ -460,6 +868,7 @@ const TOOL_SCHEMAS = {
460
868
  properties: {
461
869
  sessionId: { type: 'string' },
462
870
  selector: { type: 'string' },
871
+ frameId: { type: 'number' },
463
872
  },
464
873
  },
465
874
  get_page_state: {
@@ -470,6 +879,7 @@ const TOOL_SCHEMAS = {
470
879
  maxItems: { type: 'number' },
471
880
  maxTextLength: { type: 'number' },
472
881
  includeButtons: { type: 'boolean' },
882
+ includeLinks: { type: 'boolean' },
473
883
  includeInputs: { type: 'boolean' },
474
884
  includeModals: { type: 'boolean' },
475
885
  },
@@ -481,7 +891,7 @@ const TOOL_SCHEMAS = {
481
891
  sessionId: { type: 'string' },
482
892
  kinds: {
483
893
  type: 'array',
484
- items: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
894
+ items: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
485
895
  },
486
896
  maxItems: { type: 'number' },
487
897
  maxTextLength: { type: 'number' },
@@ -501,14 +911,22 @@ const TOOL_SCHEMAS = {
501
911
  required: ['sessionId', 'scope'],
502
912
  properties: {
503
913
  sessionId: { type: 'string' },
504
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
914
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
505
915
  selector: { type: 'string' },
506
916
  testId: { type: 'string' },
507
917
  textContains: { type: 'string' },
508
918
  labelContains: { type: 'string' },
509
919
  titleContains: { type: 'string' },
920
+ role: { type: 'string' },
921
+ name: { type: 'string' },
922
+ placeholder: { type: 'string' },
923
+ altText: { type: 'string' },
924
+ exact: { type: 'boolean' },
925
+ frameUrlContains: { type: 'string' },
926
+ frameTitleContains: { type: 'string' },
510
927
  urlContains: { type: 'string' },
511
928
  language: { type: 'string' },
929
+ visible: { type: 'boolean' },
512
930
  disabled: { type: 'boolean' },
513
931
  selected: { type: 'boolean' },
514
932
  pressed: { type: 'boolean' },
@@ -528,14 +946,22 @@ const TOOL_SCHEMAS = {
528
946
  required: ['sessionId', 'scope'],
529
947
  properties: {
530
948
  sessionId: { type: 'string' },
531
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
949
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
532
950
  selector: { type: 'string' },
533
951
  testId: { type: 'string' },
534
952
  textContains: { type: 'string' },
535
953
  labelContains: { type: 'string' },
536
954
  titleContains: { type: 'string' },
955
+ role: { type: 'string' },
956
+ name: { type: 'string' },
957
+ placeholder: { type: 'string' },
958
+ altText: { type: 'string' },
959
+ exact: { type: 'boolean' },
960
+ frameUrlContains: { type: 'string' },
961
+ frameTitleContains: { type: 'string' },
537
962
  urlContains: { type: 'string' },
538
963
  language: { type: 'string' },
964
+ visible: { type: 'boolean' },
539
965
  disabled: { type: 'boolean' },
540
966
  selected: { type: 'boolean' },
541
967
  pressed: { type: 'boolean' },
@@ -552,140 +978,352 @@ const TOOL_SCHEMAS = {
552
978
  pollIntervalMs: { type: 'number' },
553
979
  },
554
980
  },
555
- capture_ui_snapshot: {
981
+ preflight_automation_flow: {
556
982
  type: 'object',
557
983
  required: ['sessionId'],
558
984
  properties: {
559
985
  sessionId: { type: 'string' },
560
- selector: { type: 'string' },
561
- trigger: { type: 'string' },
562
- mode: { type: 'string' },
563
- styleMode: { type: 'string' },
564
- maxDepth: { type: 'number' },
565
- maxBytes: { type: 'number' },
566
- maxAncestors: { type: 'number' },
567
- includeDom: { type: 'boolean' },
568
- includeStyles: { type: 'boolean' },
569
- includePngDataUrl: { type: 'boolean' },
986
+ expectedUrlContains: { type: 'string' },
987
+ requireSensitiveAutomation: { type: 'boolean' },
988
+ plannedActions: { type: 'array', items: { type: 'string' } },
989
+ includePageState: { type: 'boolean' },
990
+ maxItems: { type: 'number' },
991
+ maxTextLength: { type: 'number' },
570
992
  },
571
993
  },
572
- get_live_console_logs: {
994
+ wait_for_url: {
573
995
  type: 'object',
574
996
  required: ['sessionId'],
575
997
  properties: {
576
998
  sessionId: { type: 'string' },
577
- url: { type: 'string' },
578
- tabId: { type: 'number' },
579
- levels: { type: 'array', items: { type: 'string' } },
580
- contains: { type: 'string' },
999
+ urlContains: { type: 'string' },
1000
+ urlRegex: { type: 'string' },
1001
+ exactUrl: { type: 'string' },
1002
+ timeoutMs: { type: 'number' },
1003
+ pollIntervalMs: { type: 'number' },
1004
+ },
1005
+ },
1006
+ wait_for_navigation: {
1007
+ type: 'object',
1008
+ required: ['sessionId'],
1009
+ properties: {
1010
+ sessionId: { type: 'string' },
1011
+ urlContains: { type: 'string' },
1012
+ urlRegex: { type: 'string' },
1013
+ exactUrl: { type: 'string' },
1014
+ fromUrlContains: { type: 'string' },
1015
+ fromUrlRegex: { type: 'string' },
1016
+ trigger: { type: 'string' },
581
1017
  sinceTs: { type: 'number' },
582
- includeRuntimeErrors: { type: 'boolean' },
583
- dedupeWindowMs: { type: 'number' },
584
- limit: { type: 'number' },
585
- responseProfile: { type: 'string' },
586
- includeArgs: { type: 'boolean' },
587
- maxResponseBytes: { type: 'number' },
1018
+ tabId: { type: 'number' },
1019
+ timeoutMs: { type: 'number' },
1020
+ pollIntervalMs: { type: 'number' },
588
1021
  },
589
1022
  },
590
- list_override_profiles: {
1023
+ wait_for_navigation_lifecycle: {
591
1024
  type: 'object',
592
- properties: {},
1025
+ required: ['sessionId'],
1026
+ properties: {
1027
+ sessionId: { type: 'string' },
1028
+ state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle'] },
1029
+ urlContains: { type: 'string' },
1030
+ urlRegex: { type: 'string' },
1031
+ exactUrl: { type: 'string' },
1032
+ tabId: { type: 'number' },
1033
+ timeoutMs: { type: 'number' },
1034
+ pollIntervalMs: { type: 'number' },
1035
+ },
593
1036
  },
594
- create_override_profile: {
1037
+ wait_for_load_state: {
595
1038
  type: 'object',
596
- required: ['targetBaseUrl'],
1039
+ required: ['sessionId'],
597
1040
  properties: {
598
- adapter: { type: 'string' },
599
- mode: { type: 'string' },
600
- targetBaseUrl: { type: 'string' },
601
- projectRoot: { type: 'string' },
602
- assetRoot: { type: 'string' },
603
- nextDir: { type: 'string' },
604
- configPath: { type: 'string' },
605
- profileId: { type: 'string' },
606
- profileName: { type: 'string' },
607
- enabled: { type: 'boolean' },
608
- profileEnabled: { type: 'boolean' },
609
- autoReload: { type: 'boolean' },
610
- includeManifestFiles: { type: 'boolean' },
611
- includeStaticFiles: { type: 'boolean' },
612
- extensions: { type: 'array', items: { type: 'string' } },
613
- maxRules: { type: 'number' },
614
- writeConfig: { type: 'boolean' },
615
- overwrite: { type: 'boolean' },
1041
+ sessionId: { type: 'string' },
1042
+ state: { type: 'string', enum: ['domcontentloaded', 'load'] },
1043
+ urlContains: { type: 'string' },
1044
+ urlRegex: { type: 'string' },
1045
+ exactUrl: { type: 'string' },
1046
+ timeoutMs: { type: 'number' },
1047
+ pollIntervalMs: { type: 'number' },
616
1048
  },
617
1049
  },
618
- validate_override_profile: {
1050
+ wait_for_selector_state: {
619
1051
  type: 'object',
1052
+ required: ['sessionId', 'selector'],
620
1053
  properties: {
621
- profileId: { type: 'string' },
1054
+ sessionId: { type: 'string' },
1055
+ selector: { type: 'string' },
1056
+ state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] },
1057
+ frameId: { type: 'number' },
1058
+ timeoutMs: { type: 'number' },
1059
+ pollIntervalMs: { type: 'number' },
622
1060
  },
623
1061
  },
624
- preflight_overrides: {
1062
+ wait_for_console: {
625
1063
  type: 'object',
626
1064
  required: ['sessionId'],
627
1065
  properties: {
628
1066
  sessionId: { type: 'string' },
629
- profileId: { type: 'string' },
1067
+ levels: { type: 'array', items: { type: 'string' } },
1068
+ contains: { type: 'string' },
1069
+ sinceTs: { type: 'number' },
1070
+ includeRuntimeErrors: { type: 'boolean' },
1071
+ timeoutMs: { type: 'number' },
1072
+ pollIntervalMs: { type: 'number' },
630
1073
  },
631
1074
  },
632
- observe_override_assets: {
1075
+ wait_for_dialog: {
633
1076
  type: 'object',
634
1077
  required: ['sessionId'],
635
1078
  properties: {
636
1079
  sessionId: { type: 'string' },
1080
+ type: { type: 'string', enum: ['alert', 'confirm', 'prompt', 'beforeunload'] },
1081
+ messageContains: { type: 'string' },
1082
+ urlContains: { type: 'string' },
1083
+ action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
1084
+ promptText: { type: 'string' },
637
1085
  tabId: { type: 'number' },
638
- includePerformance: { type: 'boolean' },
1086
+ timeoutMs: { type: 'number' },
1087
+ pollIntervalMs: { type: 'number' },
639
1088
  },
640
1089
  },
641
- capture_override_response_body: {
1090
+ wait_for_stable_layout: {
642
1091
  type: 'object',
643
1092
  required: ['sessionId'],
644
1093
  properties: {
645
1094
  sessionId: { type: 'string' },
1095
+ selector: { type: 'string' },
1096
+ stableMs: { type: 'number' },
646
1097
  tabId: { type: 'number' },
647
- targetUrl: { type: 'string' },
648
- targetAssetUrl: { type: 'string' },
649
- captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
650
- triggerReload: { type: 'boolean' },
651
- matchMode: { type: 'string', enum: ['exact', 'prefix'] },
652
- ruleType: { type: 'string' },
653
- requestMethod: { type: 'string' },
654
- requestHeaders: { type: 'object' },
655
1098
  timeoutMs: { type: 'number' },
656
- maxBodyBytes: { type: 'number' },
657
- includeBody: { type: 'boolean' },
1099
+ pollIntervalMs: { type: 'number' },
658
1100
  },
659
1101
  },
660
- list_observed_override_assets: {
1102
+ wait_for_download: {
661
1103
  type: 'object',
662
1104
  required: ['sessionId'],
663
1105
  properties: {
664
1106
  sessionId: { type: 'string' },
665
- limit: { type: 'number' },
666
- sinceTimestamp: { type: 'number' },
1107
+ urlContains: { type: 'string' },
1108
+ urlRegex: { type: 'string' },
1109
+ exactUrl: { type: 'string' },
1110
+ filenameContains: { type: 'string' },
1111
+ filenameRegex: { type: 'string' },
1112
+ state: { type: 'string', enum: ['started', 'completed'] },
1113
+ tabId: { type: 'number' },
1114
+ timeoutMs: { type: 'number' },
1115
+ pollIntervalMs: { type: 'number' },
667
1116
  },
668
1117
  },
669
- map_next_override_assets: {
1118
+ wait_for_popup: {
670
1119
  type: 'object',
671
- required: ['projectRoot'],
1120
+ required: ['sessionId'],
672
1121
  properties: {
673
1122
  sessionId: { type: 'string' },
674
- tabId: { type: 'number' },
675
- projectRoot: { type: 'string' },
676
- nextDir: { type: 'string' },
677
- route: { type: 'string' },
678
- sourcePaths: { type: 'array', items: { type: 'string' } },
679
- observedAssets: { type: 'array', items: { type: 'object' } },
680
- maxResults: { type: 'number' },
681
- fetchProductionAssets: { type: 'boolean' },
682
- productionFetchTimeoutMs: { type: 'number' },
683
- maxProductionAssetBytes: { type: 'number' },
684
- maxDriftCandidates: { type: 'number' },
685
- productionFetchConcurrency: { type: 'number' },
1123
+ urlContains: { type: 'string' },
1124
+ urlRegex: { type: 'string' },
1125
+ exactUrl: { type: 'string' },
1126
+ openerTabId: { type: 'number' },
1127
+ timeoutMs: { type: 'number' },
1128
+ pollIntervalMs: { type: 'number' },
686
1129
  },
687
1130
  },
688
- plan_override_response_patch: {
1131
+ wait_for_request: {
1132
+ type: 'object',
1133
+ required: ['sessionId'],
1134
+ properties: {
1135
+ sessionId: { type: 'string' },
1136
+ urlContains: { type: 'string' },
1137
+ urlRegex: { type: 'string' },
1138
+ exactUrl: { type: 'string' },
1139
+ method: { type: 'string' },
1140
+ traceId: { type: 'string' },
1141
+ initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
1142
+ requestContentType: { type: 'string' },
1143
+ sinceTs: { type: 'number' },
1144
+ tabId: { type: 'number' },
1145
+ includeBodies: { type: 'boolean' },
1146
+ timeoutMs: { type: 'number' },
1147
+ pollIntervalMs: { type: 'number' },
1148
+ },
1149
+ },
1150
+ wait_for_response: {
1151
+ type: 'object',
1152
+ required: ['sessionId'],
1153
+ properties: {
1154
+ sessionId: { type: 'string' },
1155
+ urlContains: { type: 'string' },
1156
+ urlRegex: { type: 'string' },
1157
+ exactUrl: { type: 'string' },
1158
+ method: { type: 'string' },
1159
+ traceId: { type: 'string' },
1160
+ initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
1161
+ requestContentType: { type: 'string' },
1162
+ responseContentType: { type: 'string' },
1163
+ statusIn: { type: 'array', items: { type: 'number' } },
1164
+ statusGte: { type: 'number' },
1165
+ statusLt: { type: 'number' },
1166
+ errorType: { type: 'string' },
1167
+ sinceTs: { type: 'number' },
1168
+ tabId: { type: 'number' },
1169
+ includeBodies: { type: 'boolean' },
1170
+ timeoutMs: { type: 'number' },
1171
+ pollIntervalMs: { type: 'number' },
1172
+ },
1173
+ },
1174
+ wait_for_network_quiet: {
1175
+ type: 'object',
1176
+ required: ['sessionId'],
1177
+ properties: {
1178
+ sessionId: { type: 'string' },
1179
+ quietMs: { type: 'number' },
1180
+ urlContains: { type: 'string' },
1181
+ method: { type: 'string' },
1182
+ tabId: { type: 'number' },
1183
+ timeoutMs: { type: 'number' },
1184
+ pollIntervalMs: { type: 'number' },
1185
+ },
1186
+ },
1187
+ capture_ui_snapshot: {
1188
+ type: 'object',
1189
+ required: ['sessionId'],
1190
+ properties: {
1191
+ sessionId: { type: 'string' },
1192
+ selector: { type: 'string' },
1193
+ trigger: { type: 'string' },
1194
+ mode: { type: 'string' },
1195
+ styleMode: { type: 'string' },
1196
+ maxDepth: { type: 'number' },
1197
+ maxBytes: { type: 'number' },
1198
+ maxAncestors: { type: 'number' },
1199
+ includeDom: { type: 'boolean' },
1200
+ includeStyles: { type: 'boolean' },
1201
+ includePngDataUrl: { type: 'boolean' },
1202
+ },
1203
+ },
1204
+ get_live_console_logs: {
1205
+ type: 'object',
1206
+ required: ['sessionId'],
1207
+ properties: {
1208
+ sessionId: { type: 'string' },
1209
+ url: { type: 'string' },
1210
+ tabId: { type: 'number' },
1211
+ levels: { type: 'array', items: { type: 'string' } },
1212
+ contains: { type: 'string' },
1213
+ sinceTs: { type: 'number' },
1214
+ includeRuntimeErrors: { type: 'boolean' },
1215
+ dedupeWindowMs: { type: 'number' },
1216
+ limit: { type: 'number' },
1217
+ responseProfile: { type: 'string' },
1218
+ includeArgs: { type: 'boolean' },
1219
+ maxResponseBytes: { type: 'number' },
1220
+ },
1221
+ },
1222
+ list_override_profiles: {
1223
+ type: 'object',
1224
+ properties: {
1225
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1226
+ },
1227
+ },
1228
+ create_override_profile: {
1229
+ type: 'object',
1230
+ required: ['targetBaseUrl'],
1231
+ properties: {
1232
+ adapter: { type: 'string' },
1233
+ mode: { type: 'string' },
1234
+ targetBaseUrl: { type: 'string' },
1235
+ projectRoot: { type: 'string' },
1236
+ assetRoot: { type: 'string' },
1237
+ nextDir: { type: 'string' },
1238
+ configPath: { type: 'string' },
1239
+ profileId: { type: 'string' },
1240
+ profileName: { type: 'string' },
1241
+ enabled: { type: 'boolean' },
1242
+ profileEnabled: { type: 'boolean' },
1243
+ autoReload: { type: 'boolean' },
1244
+ includeManifestFiles: { type: 'boolean' },
1245
+ includeStaticFiles: { type: 'boolean' },
1246
+ extensions: { type: 'array', items: { type: 'string' } },
1247
+ maxRules: { type: 'number' },
1248
+ writeConfig: { type: 'boolean' },
1249
+ overwrite: { type: 'boolean' },
1250
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1251
+ includeConfigJson: { type: 'boolean' },
1252
+ },
1253
+ },
1254
+ validate_override_profile: {
1255
+ type: 'object',
1256
+ properties: {
1257
+ profileId: { type: 'string' },
1258
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1259
+ },
1260
+ },
1261
+ preflight_overrides: {
1262
+ type: 'object',
1263
+ required: ['sessionId'],
1264
+ properties: {
1265
+ sessionId: { type: 'string' },
1266
+ profileId: { type: 'string' },
1267
+ },
1268
+ },
1269
+ observe_override_assets: {
1270
+ type: 'object',
1271
+ required: ['sessionId'],
1272
+ properties: {
1273
+ sessionId: { type: 'string' },
1274
+ tabId: { type: 'number' },
1275
+ includePerformance: { type: 'boolean' },
1276
+ },
1277
+ },
1278
+ capture_override_response_body: {
1279
+ type: 'object',
1280
+ required: ['sessionId'],
1281
+ properties: {
1282
+ sessionId: { type: 'string' },
1283
+ tabId: { type: 'number' },
1284
+ targetUrl: { type: 'string' },
1285
+ targetAssetUrl: { type: 'string' },
1286
+ captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
1287
+ triggerReload: { type: 'boolean' },
1288
+ matchMode: { type: 'string', enum: ['exact', 'prefix'] },
1289
+ ruleType: { type: 'string' },
1290
+ requestMethod: { type: 'string' },
1291
+ requestHeaders: { type: 'object' },
1292
+ timeoutMs: { type: 'number' },
1293
+ maxBodyBytes: { type: 'number' },
1294
+ includeBody: { type: 'boolean' },
1295
+ },
1296
+ },
1297
+ list_observed_override_assets: {
1298
+ type: 'object',
1299
+ required: ['sessionId'],
1300
+ properties: {
1301
+ sessionId: { type: 'string' },
1302
+ limit: { type: 'number' },
1303
+ sinceTimestamp: { type: 'number' },
1304
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1305
+ },
1306
+ },
1307
+ map_next_override_assets: {
1308
+ type: 'object',
1309
+ required: ['projectRoot'],
1310
+ properties: {
1311
+ sessionId: { type: 'string' },
1312
+ tabId: { type: 'number' },
1313
+ projectRoot: { type: 'string' },
1314
+ nextDir: { type: 'string' },
1315
+ route: { type: 'string' },
1316
+ sourcePaths: { type: 'array', items: { type: 'string' } },
1317
+ observedAssets: { type: 'array', items: { type: 'object' } },
1318
+ maxResults: { type: 'number' },
1319
+ fetchProductionAssets: { type: 'boolean' },
1320
+ productionFetchTimeoutMs: { type: 'number' },
1321
+ maxProductionAssetBytes: { type: 'number' },
1322
+ maxDriftCandidates: { type: 'number' },
1323
+ productionFetchConcurrency: { type: 'number' },
1324
+ },
1325
+ },
1326
+ plan_override_response_patch: {
689
1327
  type: 'object',
690
1328
  properties: {
691
1329
  sessionId: { type: 'string' },
@@ -805,96 +1443,326 @@ const TOOL_SCHEMAS = {
805
1443
  runId: { type: 'string' },
806
1444
  },
807
1445
  },
808
- explain_last_failure: {
1446
+ discover_ssr_mockability: {
809
1447
  type: 'object',
810
- required: ['sessionId'],
1448
+ required: ['projectRoot'],
811
1449
  properties: {
812
- sessionId: { type: 'string' },
813
- lookbackSeconds: { type: 'number' },
1450
+ projectRoot: { type: 'string' },
1451
+ targetUrl: { type: 'string' },
1452
+ apiHost: { type: 'string' },
1453
+ maxFiles: { type: 'number' },
814
1454
  },
815
1455
  },
816
- get_event_correlation: {
1456
+ apply_ssr_mock_config: {
817
1457
  type: 'object',
818
- required: ['sessionId', 'eventId'],
1458
+ required: ['projectRoot', 'envVarName', 'mockBaseUrl'],
819
1459
  properties: {
820
- sessionId: { type: 'string' },
821
- eventId: { type: 'string' },
822
- windowSeconds: { type: 'number' },
1460
+ projectRoot: { type: 'string' },
1461
+ envVarName: { type: 'string' },
1462
+ mockBaseUrl: { type: 'string' },
1463
+ envFilePath: { type: 'string' },
1464
+ rollbackId: { type: 'string' },
823
1465
  },
824
1466
  },
825
- list_snapshots: {
1467
+ remove_ssr_mock_config: {
826
1468
  type: 'object',
827
- required: ['sessionId'],
1469
+ required: ['envFilePath', 'envVarName'],
828
1470
  properties: {
829
- sessionId: { type: 'string' },
830
- trigger: { type: 'string' },
831
- sinceTimestamp: { type: 'number' },
832
- untilTimestamp: { type: 'number' },
1471
+ envFilePath: { type: 'string' },
1472
+ envVarName: { type: 'string' },
1473
+ rollbackId: { type: 'string' },
1474
+ },
1475
+ },
1476
+ get_ssr_mock_audit_log: {
1477
+ type: 'object',
1478
+ properties: {
1479
+ projectRoot: { type: 'string' },
1480
+ rollbackId: { type: 'string' },
1481
+ envVarName: { type: 'string' },
833
1482
  limit: { type: 'number' },
834
1483
  offset: { type: 'number' },
835
1484
  maxResponseBytes: { type: 'number' },
836
1485
  },
837
1486
  },
838
- get_snapshot_for_event: {
1487
+ create_mock_route: {
839
1488
  type: 'object',
840
- required: ['sessionId', 'eventId'],
1489
+ required: ['targetUrl'],
841
1490
  properties: {
842
- sessionId: { type: 'string' },
843
- eventId: { type: 'string' },
844
- maxDeltaMs: { type: 'number' },
1491
+ routeId: { type: 'string' },
1492
+ enabled: { type: 'boolean' },
1493
+ mode: { type: 'string' },
1494
+ method: { type: 'string' },
1495
+ matchMode: { type: 'string' },
1496
+ targetUrl: { type: 'string' },
1497
+ statusCode: { type: 'number' },
1498
+ responseHeaders: { type: 'object' },
1499
+ bodyJson: {},
1500
+ bodyText: { type: 'string' },
1501
+ bodyBase64: { type: 'string' },
1502
+ bodyFilePath: { type: 'string' },
1503
+ delayMs: { type: 'number' },
1504
+ sourceKind: { type: 'string' },
1505
+ sessionScope: { type: 'string' },
1506
+ projectRoot: { type: 'string' },
1507
+ ttlMs: { type: 'number' },
845
1508
  },
846
1509
  },
847
- get_snapshot_asset: {
1510
+ update_mock_route: {
848
1511
  type: 'object',
849
- required: ['sessionId', 'snapshotId'],
1512
+ required: ['routeId'],
850
1513
  properties: {
851
- sessionId: { type: 'string' },
852
- snapshotId: { type: 'string' },
853
- asset: { type: 'string' },
1514
+ routeId: { type: 'string' },
1515
+ enabled: { type: 'boolean' },
1516
+ mode: { type: 'string' },
1517
+ method: { type: 'string' },
1518
+ matchMode: { type: 'string' },
1519
+ targetUrl: { type: 'string' },
1520
+ statusCode: { type: 'number' },
1521
+ responseHeaders: { type: 'object' },
1522
+ bodyJson: {},
1523
+ bodyText: { type: 'string' },
1524
+ bodyBase64: { type: 'string' },
1525
+ bodyFilePath: { type: 'string' },
1526
+ delayMs: { type: 'number' },
1527
+ sourceKind: { type: 'string' },
1528
+ sessionScope: { type: 'string' },
1529
+ projectRoot: { type: 'string' },
1530
+ ttlMs: { type: 'number' },
1531
+ },
1532
+ },
1533
+ delete_mock_route: {
1534
+ type: 'object',
1535
+ required: ['routeId'],
1536
+ properties: {
1537
+ routeId: { type: 'string' },
1538
+ },
1539
+ },
1540
+ list_mock_routes: {
1541
+ type: 'object',
1542
+ properties: {
1543
+ projectRoot: { type: 'string' },
1544
+ mode: { type: 'string' },
1545
+ enabled: { type: 'boolean' },
1546
+ limit: { type: 'number' },
854
1547
  offset: { type: 'number' },
855
- maxBytes: { type: 'number' },
856
- encoding: { type: 'string' },
1548
+ maxResponseBytes: { type: 'number' },
857
1549
  },
858
1550
  },
859
- list_automation_runs: {
1551
+ get_mock_route: {
1552
+ type: 'object',
1553
+ required: ['routeId'],
1554
+ properties: {
1555
+ routeId: { type: 'string' },
1556
+ },
1557
+ },
1558
+ get_mock_run_log: {
860
1559
  type: 'object',
861
- required: ['sessionId'],
862
1560
  properties: {
1561
+ routeId: { type: 'string' },
863
1562
  sessionId: { type: 'string' },
864
- status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
865
- action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
866
- traceId: { type: 'string' },
867
1563
  limit: { type: 'number' },
868
1564
  offset: { type: 'number' },
869
1565
  maxResponseBytes: { type: 'number' },
870
1566
  },
871
1567
  },
872
- get_automation_run: {
1568
+ get_mock_hit_log: {
873
1569
  type: 'object',
874
- required: ['sessionId', 'runId'],
875
1570
  properties: {
876
- sessionId: { type: 'string' },
1571
+ routeId: { type: 'string' },
877
1572
  runId: { type: 'string' },
878
- stepLimit: { type: 'number' },
879
- stepOffset: { type: 'number' },
1573
+ limit: { type: 'number' },
1574
+ offset: { type: 'number' },
880
1575
  maxResponseBytes: { type: 'number' },
881
1576
  },
882
1577
  },
883
- execute_ui_action: {
1578
+ get_mock_status: {
1579
+ type: 'object',
1580
+ properties: {
1581
+ routeId: { type: 'string' },
1582
+ projectRoot: { type: 'string' },
1583
+ },
1584
+ },
1585
+ explain_last_failure: {
1586
+ type: 'object',
1587
+ required: ['sessionId'],
1588
+ properties: {
1589
+ sessionId: { type: 'string' },
1590
+ lookbackSeconds: { type: 'number' },
1591
+ },
1592
+ },
1593
+ get_event_correlation: {
1594
+ type: 'object',
1595
+ required: ['sessionId', 'eventId'],
1596
+ properties: {
1597
+ sessionId: { type: 'string' },
1598
+ eventId: { type: 'string' },
1599
+ windowSeconds: { type: 'number' },
1600
+ },
1601
+ },
1602
+ list_snapshots: {
1603
+ type: 'object',
1604
+ required: ['sessionId'],
1605
+ properties: {
1606
+ sessionId: { type: 'string' },
1607
+ trigger: { type: 'string' },
1608
+ sinceTimestamp: { type: 'number' },
1609
+ untilTimestamp: { type: 'number' },
1610
+ limit: { type: 'number' },
1611
+ offset: { type: 'number' },
1612
+ maxResponseBytes: { type: 'number' },
1613
+ },
1614
+ },
1615
+ get_snapshot_for_event: {
1616
+ type: 'object',
1617
+ required: ['sessionId', 'eventId'],
1618
+ properties: {
1619
+ sessionId: { type: 'string' },
1620
+ eventId: { type: 'string' },
1621
+ maxDeltaMs: { type: 'number' },
1622
+ },
1623
+ },
1624
+ get_snapshot_asset: {
1625
+ type: 'object',
1626
+ required: ['sessionId', 'snapshotId'],
1627
+ properties: {
1628
+ sessionId: { type: 'string' },
1629
+ snapshotId: { type: 'string' },
1630
+ asset: { type: 'string' },
1631
+ offset: { type: 'number' },
1632
+ maxBytes: { type: 'number' },
1633
+ encoding: { type: 'string' },
1634
+ },
1635
+ },
1636
+ run_lighthouse_report: {
1637
+ type: 'object',
1638
+ properties: {
1639
+ sessionId: { type: 'string' },
1640
+ url: { type: 'string' },
1641
+ formFactor: { type: 'string', enum: ['mobile', 'desktop'] },
1642
+ categories: {
1643
+ type: 'array',
1644
+ items: { type: 'string', enum: ['performance', 'accessibility', 'best-practices', 'seo', 'pwa'] },
1645
+ },
1646
+ maxWaitForLoadMs: { type: 'number' },
1647
+ chromeFlags: { type: 'array', items: { type: 'string' } },
1648
+ },
1649
+ },
1650
+ list_lighthouse_reports: {
1651
+ type: 'object',
1652
+ properties: {
1653
+ sessionId: { type: 'string' },
1654
+ urlContains: { type: 'string' },
1655
+ status: { type: 'string', enum: ['succeeded', 'failed'] },
1656
+ limit: { type: 'number' },
1657
+ offset: { type: 'number' },
1658
+ },
1659
+ },
1660
+ get_lighthouse_report: {
1661
+ type: 'object',
1662
+ required: ['reportId'],
1663
+ properties: {
1664
+ reportId: { type: 'string' },
1665
+ },
1666
+ },
1667
+ get_lighthouse_report_asset: {
1668
+ type: 'object',
1669
+ required: ['reportId'],
1670
+ properties: {
1671
+ reportId: { type: 'string' },
1672
+ asset: { type: 'string', enum: ['json', 'html'] },
1673
+ offset: { type: 'number' },
1674
+ maxBytes: { type: 'number' },
1675
+ encoding: { type: 'string', enum: ['base64', 'raw'] },
1676
+ },
1677
+ },
1678
+ plan_lighthouse_fixes: {
1679
+ type: 'object',
1680
+ required: ['reportId'],
1681
+ properties: {
1682
+ reportId: { type: 'string' },
1683
+ minPriority: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
1684
+ limit: { type: 'number' },
1685
+ projectRoot: { type: 'string' },
1686
+ routePath: { type: 'string' },
1687
+ sourceCandidateLimit: { type: 'number' },
1688
+ },
1689
+ },
1690
+ list_automation_runs: {
1691
+ type: 'object',
1692
+ required: ['sessionId'],
1693
+ properties: {
1694
+ sessionId: { type: 'string' },
1695
+ status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
1696
+ action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
1697
+ traceId: { type: 'string' },
1698
+ limit: { type: 'number' },
1699
+ offset: { type: 'number' },
1700
+ maxResponseBytes: { type: 'number' },
1701
+ },
1702
+ },
1703
+ get_automation_run: {
1704
+ type: 'object',
1705
+ required: ['sessionId', 'runId'],
1706
+ properties: {
1707
+ sessionId: { type: 'string' },
1708
+ runId: { type: 'string' },
1709
+ stepLimit: { type: 'number' },
1710
+ stepOffset: { type: 'number' },
1711
+ maxResponseBytes: { type: 'number' },
1712
+ },
1713
+ },
1714
+ execute_ui_action: {
884
1715
  type: 'object',
885
1716
  required: ['sessionId', 'action'],
886
1717
  properties: {
887
1718
  sessionId: { type: 'string' },
888
- action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
1719
+ action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
889
1720
  traceId: { type: 'string' },
890
1721
  target: {
891
1722
  type: 'object',
892
1723
  properties: {
893
1724
  selector: { type: 'string' },
894
1725
  elementRef: { type: 'string' },
1726
+ coordinates: {
1727
+ type: 'object',
1728
+ properties: {
1729
+ x: { type: 'number' },
1730
+ y: { type: 'number' },
1731
+ frameId: { type: 'number' },
1732
+ },
1733
+ },
895
1734
  tabId: { type: 'number' },
896
1735
  frameId: { type: 'number' },
897
1736
  url: { type: 'string' },
1737
+ locator: ACTION_LOCATOR_TOOL_SCHEMA,
1738
+ frameUrlContains: { type: 'string' },
1739
+ frameTitleContains: { type: 'string' },
1740
+ testId: { type: 'string' },
1741
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
1742
+ textContains: { type: 'string' },
1743
+ labelContains: { type: 'string' },
1744
+ titleContains: { type: 'string' },
1745
+ role: { type: 'string' },
1746
+ name: { type: 'string' },
1747
+ placeholder: { type: 'string' },
1748
+ altText: { type: 'string' },
1749
+ exact: { type: 'boolean' },
1750
+ nth: { type: 'number' },
1751
+ first: { type: 'boolean' },
1752
+ last: { type: 'boolean' },
1753
+ strict: { type: 'boolean' },
1754
+ tagName: { type: 'string' },
1755
+ type: { type: 'string' },
1756
+ visible: { type: 'boolean' },
1757
+ enabled: { type: 'boolean' },
1758
+ disabled: { type: 'boolean' },
1759
+ editable: { type: 'boolean' },
1760
+ checked: { type: 'boolean' },
1761
+ selected: { type: 'boolean' },
1762
+ pressed: { type: 'boolean' },
1763
+ expanded: { type: 'boolean' },
1764
+ readOnly: { type: 'boolean' },
1765
+ requiredField: { type: 'boolean' },
898
1766
  },
899
1767
  },
900
1768
  input: { type: 'object' },
@@ -917,14 +1785,22 @@ const TOOL_SCHEMAS = {
917
1785
  type: 'object',
918
1786
  required: ['scope'],
919
1787
  properties: {
920
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
1788
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
921
1789
  selector: { type: 'string' },
922
1790
  testId: { type: 'string' },
923
1791
  textContains: { type: 'string' },
924
1792
  labelContains: { type: 'string' },
925
1793
  titleContains: { type: 'string' },
1794
+ role: { type: 'string' },
1795
+ name: { type: 'string' },
1796
+ placeholder: { type: 'string' },
1797
+ altText: { type: 'string' },
1798
+ exact: { type: 'boolean' },
1799
+ frameUrlContains: { type: 'string' },
1800
+ frameTitleContains: { type: 'string' },
926
1801
  urlContains: { type: 'string' },
927
1802
  language: { type: 'string' },
1803
+ visible: { type: 'boolean' },
928
1804
  disabled: { type: 'boolean' },
929
1805
  selected: { type: 'boolean' },
930
1806
  pressed: { type: 'boolean' },
@@ -961,7 +1837,7 @@ const TOOL_SCHEMAS = {
961
1837
  properties: {
962
1838
  id: { type: 'string' },
963
1839
  note: { type: 'string' },
964
- kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
1840
+ kind: { type: 'string', enum: ['action', 'waitFor', 'assert', 'wait'] },
965
1841
  action: { type: 'string' },
966
1842
  traceId: { type: 'string' },
967
1843
  target: {
@@ -969,16 +1845,37 @@ const TOOL_SCHEMAS = {
969
1845
  properties: {
970
1846
  selector: { type: 'string' },
971
1847
  elementRef: { type: 'string' },
1848
+ coordinates: {
1849
+ type: 'object',
1850
+ properties: {
1851
+ x: { type: 'number' },
1852
+ y: { type: 'number' },
1853
+ frameId: { type: 'number' },
1854
+ },
1855
+ },
972
1856
  tabId: { type: 'number' },
973
1857
  frameId: { type: 'number' },
974
1858
  url: { type: 'string' },
1859
+ locator: ACTION_LOCATOR_TOOL_SCHEMA,
1860
+ frameUrlContains: { type: 'string' },
1861
+ frameTitleContains: { type: 'string' },
975
1862
  testId: { type: 'string' },
976
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
1863
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
977
1864
  textContains: { type: 'string' },
978
1865
  labelContains: { type: 'string' },
979
1866
  titleContains: { type: 'string' },
1867
+ role: { type: 'string' },
1868
+ name: { type: 'string' },
1869
+ placeholder: { type: 'string' },
1870
+ altText: { type: 'string' },
1871
+ exact: { type: 'boolean' },
1872
+ nth: { type: 'number' },
1873
+ first: { type: 'boolean' },
1874
+ last: { type: 'boolean' },
1875
+ strict: { type: 'boolean' },
980
1876
  tagName: { type: 'string' },
981
1877
  type: { type: 'string' },
1878
+ visible: { type: 'boolean' },
982
1879
  disabled: { type: 'boolean' },
983
1880
  selected: { type: 'boolean' },
984
1881
  pressed: { type: 'boolean' },
@@ -1012,14 +1909,22 @@ const TOOL_SCHEMAS = {
1012
1909
  matcher: {
1013
1910
  type: 'object',
1014
1911
  properties: {
1015
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
1912
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
1016
1913
  selector: { type: 'string' },
1017
1914
  testId: { type: 'string' },
1018
1915
  textContains: { type: 'string' },
1019
1916
  labelContains: { type: 'string' },
1020
1917
  titleContains: { type: 'string' },
1918
+ role: { type: 'string' },
1919
+ name: { type: 'string' },
1920
+ placeholder: { type: 'string' },
1921
+ altText: { type: 'string' },
1922
+ exact: { type: 'boolean' },
1923
+ frameUrlContains: { type: 'string' },
1924
+ frameTitleContains: { type: 'string' },
1021
1925
  urlContains: { type: 'string' },
1022
1926
  language: { type: 'string' },
1927
+ visible: { type: 'boolean' },
1023
1928
  disabled: { type: 'boolean' },
1024
1929
  selected: { type: 'boolean' },
1025
1930
  pressed: { type: 'boolean' },
@@ -1036,6 +1941,7 @@ const TOOL_SCHEMAS = {
1036
1941
  pollIntervalMs: { type: 'number' },
1037
1942
  },
1038
1943
  },
1944
+ wait: AUTOMATION_WAIT_TOOL_SCHEMA,
1039
1945
  },
1040
1946
  },
1041
1947
  },
@@ -1062,11 +1968,25 @@ const TOOL_DESCRIPTIONS = {
1062
1968
  get_computed_styles: 'Read computed CSS styles for an element',
1063
1969
  get_layout_metrics: 'Read viewport and element layout metrics',
1064
1970
  get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
1065
- get_interactive_elements: 'Read compact live element references for buttons, inputs, modals, and focused elements',
1971
+ get_interactive_elements: 'Read compact live element references for buttons, links, inputs, modals, and focused elements',
1066
1972
  get_live_session_health: 'Read live transport health and session binding details for one session',
1067
1973
  set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
1068
1974
  assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
1069
1975
  wait_for_page_state: 'Poll compact page state until a structured assertion becomes true',
1976
+ preflight_automation_flow: 'Check live-session readiness and production risks before running an automation flow',
1977
+ wait_for_url: 'Poll the live page URL until it matches an exact, contains, or regex condition',
1978
+ wait_for_navigation: 'Poll persisted navigation events until a matching URL or trigger is observed',
1979
+ wait_for_navigation_lifecycle: 'Wait for a live navigation lifecycle milestone such as commit, load, or network idle',
1980
+ wait_for_load_state: 'Poll the live page document readiness until domcontentloaded or load is reached',
1981
+ wait_for_selector_state: 'Poll a selector until it is attached, detached, visible, or hidden',
1982
+ wait_for_console: 'Poll live console logs until a matching message appears',
1983
+ wait_for_dialog: 'Wait for a native JavaScript dialog and optionally accept or dismiss it',
1984
+ wait_for_stable_layout: 'Wait until the page or selector layout stays unchanged for a stable window',
1985
+ wait_for_download: 'Wait for a download started by the bound tab and optionally until completion',
1986
+ wait_for_popup: 'Wait for a popup tab or window opened from the bound session tab',
1987
+ wait_for_network_quiet: 'Wait until persisted network activity is quiet for a bounded window',
1988
+ wait_for_request: 'Poll persisted network activity until a matching request is observed',
1989
+ wait_for_response: 'Poll persisted network activity until a matching response is observed',
1070
1990
  capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
1071
1991
  get_live_console_logs: 'Read in-memory live console logs for a connected session',
1072
1992
  list_override_profiles: 'List configured browser override profiles',
@@ -1085,11 +2005,28 @@ const TOOL_DESCRIPTIONS = {
1085
2005
  get_override_request_log: 'Read persisted browser override request audit rows',
1086
2006
  get_override_plan_log: 'Read persisted generated override plan audit rows with previews, hashes, and rollback metadata',
1087
2007
  diagnose_overrides: 'Diagnose persisted browser override runs and failure indicators',
2008
+ discover_ssr_mockability: 'Inspect a local project for env-driven or central-client SSR mock injection points',
2009
+ apply_ssr_mock_config: 'Apply a temporary SSR mock base URL by commenting the old env value and writing a managed replacement',
2010
+ remove_ssr_mock_config: 'Restore or remove a managed SSR mock env patch from a local env file',
2011
+ get_ssr_mock_audit_log: 'Read persisted SSR mock discovery and env patch audit rows',
2012
+ create_mock_route: 'Create or persist a reusable browser or SSR mock route',
2013
+ update_mock_route: 'Update an existing mock route definition',
2014
+ delete_mock_route: 'Delete a persisted mock route',
2015
+ list_mock_routes: 'List persisted mock route definitions',
2016
+ get_mock_route: 'Read one persisted mock route definition',
2017
+ get_mock_run_log: 'Read persisted mock route run records',
2018
+ get_mock_hit_log: 'Read persisted mock route hit records',
2019
+ get_mock_status: 'Summarize persisted mock route, run, and hit state',
1088
2020
  explain_last_failure: 'Explain the latest failure timeline',
1089
2021
  get_event_correlation: 'Correlate related events by window',
1090
2022
  list_snapshots: 'List snapshot metadata by session/time/trigger',
1091
2023
  get_snapshot_for_event: 'Find snapshot most related to an event',
1092
2024
  get_snapshot_asset: 'Read bounded binary chunks for snapshot assets',
2025
+ run_lighthouse_report: 'Run an official Lighthouse report for a URL or session URL and persist JSON/HTML artifacts',
2026
+ list_lighthouse_reports: 'List persisted Lighthouse report metadata',
2027
+ get_lighthouse_report: 'Read one persisted Lighthouse report summary',
2028
+ get_lighthouse_report_asset: 'Read bounded chunks from a persisted Lighthouse JSON or HTML report artifact',
2029
+ plan_lighthouse_fixes: 'Create a prioritized fix plan from a persisted Lighthouse report',
1093
2030
  list_automation_runs: 'List first-class automation runs from dedicated automation tables',
1094
2031
  get_automation_run: 'Inspect one automation run with bounded step details',
1095
2032
  execute_ui_action: 'Execute one live UI action in the current bound extension session',
@@ -1113,6 +2050,7 @@ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
1113
2050
  const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
1114
2051
  const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
1115
2052
  const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
2053
+ const DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS = 5_000;
1116
2054
  const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
1117
2055
  const OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE = 'OVERRIDE_LIVE_COMMAND_TIMEOUT';
1118
2056
  const OVERRIDE_LIVE_COMMAND_FAILED_CODE = 'OVERRIDE_LIVE_COMMAND_FAILED';
@@ -1182,6 +2120,23 @@ function resolveMaxResponseBytes(value) {
1182
2120
  }
1183
2121
  return Math.min(floored, MAX_RESPONSE_BYTES);
1184
2122
  }
2123
+ function createSsrMockAuditRecord(input) {
2124
+ return {
2125
+ auditId: randomUUID(),
2126
+ createdAt: Date.now(),
2127
+ action: input.action,
2128
+ status: input.status,
2129
+ projectRoot: input.projectRoot,
2130
+ targetUrl: input.targetUrl ?? null,
2131
+ apiHost: input.apiHost ?? null,
2132
+ envVarName: input.envVarName ?? null,
2133
+ envFilePath: input.envFilePath ?? null,
2134
+ mockBaseUrl: input.mockBaseUrl ?? null,
2135
+ rollbackId: input.rollbackId ?? null,
2136
+ summary: input.summary ?? null,
2137
+ result: input.result ?? null,
2138
+ };
2139
+ }
1185
2140
  function estimateJsonBytes(value) {
1186
2141
  return Buffer.byteLength(JSON.stringify(value), 'utf-8');
1187
2142
  }
@@ -1395,7 +2350,7 @@ function buildOverrideProfileRecords() {
1395
2350
  active: profile.profileId === summary.activeProfileId,
1396
2351
  configEnabled: summary.configEnabled,
1397
2352
  enabled: profile.enabled,
1398
- effectiveEnabled: summary.configEnabled && profile.enabled && profile.enabledRuleCount > 0,
2353
+ effectiveEnabled: profile.enabled && profile.enabledRuleCount > 0,
1399
2354
  autoReload: profile.autoReload,
1400
2355
  configPath: summary.configPath,
1401
2356
  fileExists: profile.fileExists,
@@ -1414,6 +2369,72 @@ function resolveOverrideProfileRecord(value) {
1414
2369
  }
1415
2370
  return profile;
1416
2371
  }
2372
+ function resolveOverrideResponseProfile(value) {
2373
+ return value === 'full' ? 'full' : 'compact';
2374
+ }
2375
+ function compactOverrideRule(rule) {
2376
+ if (!isRecord(rule)) {
2377
+ return {};
2378
+ }
2379
+ return {
2380
+ ruleId: rule.ruleId,
2381
+ enabled: rule.enabled,
2382
+ ruleType: rule.ruleType,
2383
+ requestMethod: rule.requestMethod,
2384
+ matchMode: rule.matchMode,
2385
+ targetAssetUrl: rule.targetAssetUrl,
2386
+ localFilePath: rule.localFilePath,
2387
+ contentType: rule.contentType,
2388
+ fileExists: rule.fileExists,
2389
+ integrity: rule.integrity,
2390
+ };
2391
+ }
2392
+ function compactOverrideProfile(profile, ruleLimit = 10) {
2393
+ const rules = Array.isArray(profile.rules) ? profile.rules : [];
2394
+ return {
2395
+ profileId: profile.profileId,
2396
+ name: profile.name,
2397
+ active: profile.active,
2398
+ configEnabled: profile.configEnabled,
2399
+ enabled: profile.enabled,
2400
+ effectiveEnabled: profile.effectiveEnabled,
2401
+ autoReload: profile.autoReload,
2402
+ configPath: profile.configPath,
2403
+ fileExists: profile.fileExists,
2404
+ ruleCount: profile.ruleCount,
2405
+ enabledRuleCount: profile.enabledRuleCount,
2406
+ rules: rules.slice(0, ruleLimit).map(compactOverrideRule),
2407
+ rulesOmitted: Math.max(0, rules.length - ruleLimit),
2408
+ };
2409
+ }
2410
+ function serializeOverrideProfile(profile, responseProfile) {
2411
+ return responseProfile === 'full' ? profile : compactOverrideProfile(profile);
2412
+ }
2413
+ function compactObservedOverrideAsset(asset) {
2414
+ if (!isRecord(asset)) {
2415
+ return {};
2416
+ }
2417
+ return {
2418
+ observedAssetId: asset.observedAssetId,
2419
+ lastSeenAt: asset.lastSeenAt,
2420
+ tabId: asset.tabId,
2421
+ url: asset.url,
2422
+ ruleType: asset.ruleType,
2423
+ requestMethod: asset.requestMethod,
2424
+ resourceType: asset.resourceType,
2425
+ contentType: asset.contentType,
2426
+ statusCode: asset.statusCode,
2427
+ pathname: asset.pathname,
2428
+ assetPath: asset.assetPath,
2429
+ kind: asset.kind,
2430
+ integrity: asset.integrity,
2431
+ fromDom: asset.fromDom,
2432
+ fromPerformance: asset.fromPerformance,
2433
+ fromNavigation: asset.fromNavigation,
2434
+ fromFetch: asset.fromFetch,
2435
+ serviceWorkerControlled: asset.serviceWorkerControlled,
2436
+ };
2437
+ }
1417
2438
  const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
1418
2439
  function sha256Text(value) {
1419
2440
  return createHash('sha256').update(value, 'utf8').digest('hex');
@@ -1582,13 +2603,6 @@ function buildOverrideProfileIssues(profile) {
1582
2603
  const rules = Array.isArray(profile.rules)
1583
2604
  ? profile.rules.filter((rule) => isRecord(rule))
1584
2605
  : [];
1585
- if (profile.configEnabled !== true) {
1586
- issues.push({
1587
- code: 'CONFIG_DISABLED',
1588
- severity: 'warning',
1589
- message: 'The override config is disabled and cannot replace requests until enabled.',
1590
- });
1591
- }
1592
2606
  if (profile.enabled !== true) {
1593
2607
  issues.push({
1594
2608
  code: 'PROFILE_DISABLED',
@@ -1677,12 +2691,6 @@ function buildOverrideProfileNextActions(profile, issues) {
1677
2691
  message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
1678
2692
  }];
1679
2693
  }
1680
- if (profile.configEnabled !== true) {
1681
- return [{
1682
- code: 'ENABLE_CONFIG',
1683
- message: 'Set the root override config enabled=true after reviewing the profile.',
1684
- }];
1685
- }
1686
2694
  if (profile.enabled !== true) {
1687
2695
  return [{
1688
2696
  code: 'ENABLE_PROFILE',
@@ -2385,6 +3393,94 @@ function normalizeOptionalString(value) {
2385
3393
  const trimmed = value.trim();
2386
3394
  return trimmed.length > 0 ? trimmed : undefined;
2387
3395
  }
3396
+ function normalizeMockRouteMode(value) {
3397
+ return value === 'ssr' || value === 'both' ? value : 'browser';
3398
+ }
3399
+ function normalizeMockRouteSourceKind(value) {
3400
+ return value === 'captured' || value === 'patched' ? value : 'manual';
3401
+ }
3402
+ function normalizeMockRouteBody(input) {
3403
+ if (Object.prototype.hasOwnProperty.call(input, 'bodyJson')) {
3404
+ return {
3405
+ bodyKind: 'json',
3406
+ bodyJson: input.bodyJson,
3407
+ };
3408
+ }
3409
+ const bodyText = normalizeOptionalString(input.bodyText);
3410
+ if (bodyText !== undefined) {
3411
+ return {
3412
+ bodyKind: 'text',
3413
+ bodyText,
3414
+ };
3415
+ }
3416
+ const bodyBase64 = normalizeOptionalString(input.bodyBase64);
3417
+ if (bodyBase64 !== undefined) {
3418
+ return {
3419
+ bodyKind: 'base64',
3420
+ bodyBase64,
3421
+ };
3422
+ }
3423
+ const bodyFilePath = normalizeOptionalString(input.bodyFilePath);
3424
+ if (bodyFilePath !== undefined) {
3425
+ return {
3426
+ bodyKind: 'file',
3427
+ bodyFilePath,
3428
+ };
3429
+ }
3430
+ return null;
3431
+ }
3432
+ function createMockRouteRecord(input, existing) {
3433
+ const now = Date.now();
3434
+ const targetUrl = normalizeOptionalString(input.targetUrl) ?? existing?.targetUrl;
3435
+ if (!targetUrl) {
3436
+ throw new Error('targetUrl is required');
3437
+ }
3438
+ const body = normalizeMockRouteBody(input);
3439
+ const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) && input.ttlMs > 0
3440
+ ? Math.floor(input.ttlMs)
3441
+ : existing?.ttlMs ?? null;
3442
+ return {
3443
+ routeId: normalizeOptionalString(input.routeId) ?? existing?.routeId ?? randomUUID(),
3444
+ createdAt: existing?.createdAt ?? now,
3445
+ updatedAt: now,
3446
+ enabled: typeof input.enabled === 'boolean' ? input.enabled : existing?.enabled ?? false,
3447
+ mode: normalizeMockRouteMode(input.mode ?? existing?.mode),
3448
+ method: normalizeHttpMethod(input.method) ?? existing?.method ?? 'GET',
3449
+ matchMode: input.matchMode === 'prefix' ? 'prefix' : existing?.matchMode ?? 'exact',
3450
+ targetUrl,
3451
+ statusCode: typeof input.statusCode === 'number' && Number.isFinite(input.statusCode)
3452
+ ? Math.max(100, Math.min(599, Math.floor(input.statusCode)))
3453
+ : existing?.statusCode ?? 200,
3454
+ responseHeaders: Object.keys(normalizeMockHeaders(input.responseHeaders)).length > 0
3455
+ ? normalizeMockHeaders(input.responseHeaders)
3456
+ : existing?.responseHeaders ?? {},
3457
+ bodyKind: body?.bodyKind ?? existing?.bodyKind ?? 'json',
3458
+ bodyJson: body?.bodyJson ?? existing?.bodyJson,
3459
+ bodyText: body?.bodyText ?? existing?.bodyText ?? null,
3460
+ bodyBase64: body?.bodyBase64 ?? existing?.bodyBase64 ?? null,
3461
+ bodyFilePath: body?.bodyFilePath ?? existing?.bodyFilePath ?? null,
3462
+ delayMs: typeof input.delayMs === 'number' && Number.isFinite(input.delayMs) && input.delayMs >= 0
3463
+ ? Math.floor(input.delayMs)
3464
+ : existing?.delayMs ?? 0,
3465
+ sourceKind: normalizeMockRouteSourceKind(input.sourceKind ?? existing?.sourceKind),
3466
+ sessionScope: normalizeOptionalString(input.sessionScope) ?? existing?.sessionScope ?? null,
3467
+ projectRoot: normalizeOptionalString(input.projectRoot) ?? existing?.projectRoot ?? null,
3468
+ ttlMs,
3469
+ expiresAt: ttlMs !== null ? now + ttlMs : null,
3470
+ };
3471
+ }
3472
+ function normalizeMockHeaders(value) {
3473
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
3474
+ return {};
3475
+ }
3476
+ const headers = {};
3477
+ for (const [key, raw] of Object.entries(value)) {
3478
+ if (typeof raw === 'string') {
3479
+ headers[key.toLowerCase()] = raw;
3480
+ }
3481
+ }
3482
+ return headers;
3483
+ }
2388
3484
  function normalizeStatusIn(value) {
2389
3485
  if (!Array.isArray(value)) {
2390
3486
  return [];
@@ -2558,6 +3654,7 @@ function mapAutomationRunRecord(row) {
2558
3654
  : undefined,
2559
3655
  stopReason: row.stop_reason ?? undefined,
2560
3656
  target: parseJsonOrUndefined(row.target_summary_json),
3657
+ diagnostics: parseJsonOrUndefined(row.diagnostics_json),
2561
3658
  failure: parseJsonOrUndefined(row.failure_json),
2562
3659
  redaction: parseJsonOrUndefined(row.redaction_json),
2563
3660
  stepCount: row.step_count,
@@ -2582,6 +3679,7 @@ function mapAutomationStepRecord(row) {
2582
3679
  durationMs: row.duration_ms ?? undefined,
2583
3680
  tabId: row.tab_id ?? undefined,
2584
3681
  target: parseJsonOrUndefined(row.target_summary_json),
3682
+ diagnostics: parseJsonOrUndefined(row.diagnostics_json),
2585
3683
  redaction: parseJsonOrUndefined(row.redaction_json),
2586
3684
  failure: parseJsonOrUndefined(row.failure_json),
2587
3685
  inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
@@ -2592,6 +3690,109 @@ function mapAutomationStepRecord(row) {
2592
3690
  source: 'automation_steps',
2593
3691
  };
2594
3692
  }
3693
+ function asRecordOrUndefined(value) {
3694
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
3695
+ ? value
3696
+ : undefined;
3697
+ }
3698
+ function buildFailureEvidenceSummary(failureEvidence) {
3699
+ if (!failureEvidence) {
3700
+ return undefined;
3701
+ }
3702
+ const snapshot = asRecordOrUndefined(failureEvidence.snapshot);
3703
+ const snapshotRoot = asRecordOrUndefined(snapshot?.snapshot);
3704
+ return {
3705
+ captured: failureEvidence.captured === true,
3706
+ error: typeof failureEvidence.error === 'string' ? failureEvidence.error : undefined,
3707
+ limitsApplied: asRecordOrUndefined(failureEvidence.limitsApplied),
3708
+ snapshot: snapshot
3709
+ ? {
3710
+ timestamp: typeof snapshot.timestamp === 'number' ? snapshot.timestamp : undefined,
3711
+ trigger: typeof snapshot.trigger === 'string' ? snapshot.trigger : undefined,
3712
+ selector: typeof snapshot.selector === 'string' ? snapshot.selector : undefined,
3713
+ url: typeof snapshot.url === 'string' ? snapshot.url : undefined,
3714
+ mode: snapshot.mode,
3715
+ hasDom: Boolean(snapshotRoot && 'dom' in snapshotRoot),
3716
+ hasStyles: Boolean(snapshotRoot && 'styles' in snapshotRoot),
3717
+ hasPng: Boolean(snapshot.png),
3718
+ }
3719
+ : undefined,
3720
+ };
3721
+ }
3722
+ function findRelatedFailureSnapshot(db, sessionId, failureEvidence) {
3723
+ const snapshotSummary = asRecordOrUndefined(buildFailureEvidenceSummary(failureEvidence)?.snapshot);
3724
+ if (!snapshotSummary) {
3725
+ return undefined;
3726
+ }
3727
+ const timestamp = typeof snapshotSummary.timestamp === 'number' ? snapshotSummary.timestamp : undefined;
3728
+ const selector = typeof snapshotSummary.selector === 'string' ? snapshotSummary.selector : undefined;
3729
+ const url = typeof snapshotSummary.url === 'string' ? snapshotSummary.url : undefined;
3730
+ const where = ['session_id = ?'];
3731
+ const params = [sessionId];
3732
+ if (selector) {
3733
+ where.push('selector = ?');
3734
+ params.push(selector);
3735
+ }
3736
+ if (url) {
3737
+ where.push('url = ?');
3738
+ params.push(url);
3739
+ }
3740
+ if (timestamp !== undefined) {
3741
+ where.push('ts BETWEEN ? AND ?');
3742
+ params.push(timestamp - 10_000, timestamp + 10_000);
3743
+ }
3744
+ const row = db.prepare(`SELECT snapshot_id, trigger_event_id, ts, selector, url
3745
+ FROM snapshots
3746
+ WHERE ${where.join(' AND ')}
3747
+ ORDER BY ${timestamp !== undefined ? 'ABS(ts - ?) ASC,' : ''} ts DESC
3748
+ LIMIT 1`).get(...params, ...(timestamp !== undefined ? [timestamp] : []));
3749
+ if (!row) {
3750
+ return undefined;
3751
+ }
3752
+ return {
3753
+ snapshotId: row.snapshot_id,
3754
+ triggerEventId: row.trigger_event_id ?? undefined,
3755
+ timestamp: row.ts,
3756
+ selector: row.selector ?? undefined,
3757
+ url: row.url ?? undefined,
3758
+ };
3759
+ }
3760
+ function mergeAutomationDiagnosticsEvidence(db, options) {
3761
+ if (!options.traceId) {
3762
+ return;
3763
+ }
3764
+ const failureEvidence = buildFailureEvidenceSummary(options.failureEvidence);
3765
+ const linkedSnapshot = findRelatedFailureSnapshot(db, options.sessionId, options.failureEvidence);
3766
+ if (!failureEvidence && !linkedSnapshot && !options.cdpFailure) {
3767
+ return;
3768
+ }
3769
+ const updateDiagnosticsJson = (tableName, keyColumn, keyValue, existingJson) => {
3770
+ const existing = asRecordOrUndefined(parseJsonOrUndefined(existingJson)) ?? {};
3771
+ const merged = {
3772
+ ...existing,
3773
+ ...(options.cdpFailure ? { cdpFailure: options.cdpFailure } : {}),
3774
+ ...(failureEvidence ? { failureEvidence } : {}),
3775
+ ...(linkedSnapshot ? { linkedSnapshot } : {}),
3776
+ };
3777
+ db.prepare(`UPDATE ${tableName} SET diagnostics_json = ?, updated_at = ? WHERE ${keyColumn} = ?`).run(JSON.stringify(merged), Date.now(), keyValue);
3778
+ };
3779
+ const runRow = db.prepare(`SELECT run_id, diagnostics_json
3780
+ FROM automation_runs
3781
+ WHERE session_id = ? AND trace_id = ?
3782
+ ORDER BY started_at DESC, updated_at DESC
3783
+ LIMIT 1`).get(options.sessionId, options.traceId);
3784
+ if (runRow) {
3785
+ updateDiagnosticsJson('automation_runs', 'run_id', runRow.run_id, runRow.diagnostics_json);
3786
+ }
3787
+ const stepRow = db.prepare(`SELECT step_id, diagnostics_json
3788
+ FROM automation_steps
3789
+ WHERE session_id = ? AND trace_id = ?
3790
+ ORDER BY step_order DESC, updated_at DESC
3791
+ LIMIT 1`).get(options.sessionId, options.traceId);
3792
+ if (stepRow) {
3793
+ updateDiagnosticsJson('automation_steps', 'step_id', stepRow.step_id, stepRow.diagnostics_json);
3794
+ }
3795
+ }
2595
3796
  function formatUrlPath(url) {
2596
3797
  try {
2597
3798
  const parsed = new URL(url);
@@ -2724,15 +3925,25 @@ function resolveViewportDimension(value, axis) {
2724
3925
  }
2725
3926
  return floored;
2726
3927
  }
2727
- class WorkflowTargetResolutionError extends Error {
2728
- code;
2729
- details;
2730
- constructor(code, message, details) {
2731
- super(message);
2732
- this.name = 'WorkflowTargetResolutionError';
2733
- this.code = code;
2734
- this.details = details;
3928
+ function buildWaitTimeoutDiagnostics(options) {
3929
+ const diagnostics = {
3930
+ waitKind: options.waitKind,
3931
+ timeoutMs: options.timeoutMs,
3932
+ waitedMs: options.waitedMs,
3933
+ attempts: options.attempts,
3934
+ pollIntervalMs: options.pollIntervalMs,
3935
+ matcherSummary: options.matcherSummary,
3936
+ };
3937
+ if (options.lastObserved !== undefined) {
3938
+ diagnostics.lastObserved = options.lastObserved;
3939
+ }
3940
+ if (typeof options.candidateCount === 'number') {
3941
+ diagnostics.candidateCount = options.candidateCount;
3942
+ }
3943
+ if (Array.isArray(options.sampledCandidates) && options.sampledCandidates.length > 0) {
3944
+ diagnostics.sampledCandidates = options.sampledCandidates;
2735
3945
  }
3946
+ return diagnostics;
2736
3947
  }
2737
3948
  function resolveOptionalMatcherString(value) {
2738
3949
  if (typeof value !== 'string') {
@@ -2755,10 +3966,10 @@ function resolveOptionalMatcherCount(value, field) {
2755
3966
  return floored;
2756
3967
  }
2757
3968
  function resolvePageStateScope(value) {
2758
- if (value === 'buttons' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
3969
+ if (value === 'buttons' || value === 'links' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
2759
3970
  return value;
2760
3971
  }
2761
- throw new Error('scope must be one of buttons, inputs, modals, focused, or page');
3972
+ throw new Error('scope must be one of buttons, links, inputs, modals, focused, or page');
2762
3973
  }
2763
3974
  function resolvePageStateMatcher(input) {
2764
3975
  const matcher = {
@@ -2768,6 +3979,13 @@ function resolvePageStateMatcher(input) {
2768
3979
  textContains: resolveOptionalMatcherString(input.textContains),
2769
3980
  labelContains: resolveOptionalMatcherString(input.labelContains),
2770
3981
  titleContains: resolveOptionalMatcherString(input.titleContains),
3982
+ role: resolveOptionalMatcherString(input.role)?.toLowerCase(),
3983
+ name: resolveOptionalMatcherString(input.name),
3984
+ placeholder: resolveOptionalMatcherString(input.placeholder),
3985
+ altText: resolveOptionalMatcherString(input.altText),
3986
+ exact: resolveOptionalMatcherBoolean(input.exact),
3987
+ frameUrlContains: resolveOptionalMatcherString(input.frameUrlContains),
3988
+ frameTitleContains: resolveOptionalMatcherString(input.frameTitleContains),
2771
3989
  urlContains: resolveOptionalMatcherString(input.urlContains),
2772
3990
  language: resolveOptionalMatcherString(input.language),
2773
3991
  disabled: resolveOptionalMatcherBoolean(input.disabled),
@@ -2778,6 +3996,7 @@ function resolvePageStateMatcher(input) {
2778
3996
  requiredField: resolveOptionalMatcherBoolean(input.requiredField),
2779
3997
  tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
2780
3998
  type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
3999
+ visible: resolveOptionalMatcherBoolean(input.visible),
2781
4000
  countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
2782
4001
  countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
2783
4002
  };
@@ -2792,6 +4011,19 @@ function includesNormalized(value, needle) {
2792
4011
  }
2793
4012
  return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
2794
4013
  }
4014
+ function matchesTextValue(value, expected, exact) {
4015
+ if (!expected) {
4016
+ return true;
4017
+ }
4018
+ if (typeof value !== 'string') {
4019
+ return false;
4020
+ }
4021
+ const normalizedValue = value.trim().toLowerCase();
4022
+ const normalizedExpected = expected.trim().toLowerCase();
4023
+ return exact === true
4024
+ ? normalizedValue === normalizedExpected
4025
+ : normalizedValue.includes(normalizedExpected);
4026
+ }
2795
4027
  function equalsNormalized(value, expected) {
2796
4028
  if (!expected) {
2797
4029
  return true;
@@ -2805,7 +4037,7 @@ function equalsOptionalBoolean(value, expected) {
2805
4037
  return value === expected;
2806
4038
  }
2807
4039
  function pickPageStateScopeItems(payload, scope) {
2808
- if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
4040
+ if (scope === 'buttons' || scope === 'links' || scope === 'inputs' || scope === 'modals') {
2809
4041
  const value = payload[scope];
2810
4042
  return asRecordArray(value);
2811
4043
  }
@@ -2818,13 +4050,20 @@ function pickPageStateScopeItems(payload, scope) {
2818
4050
  function matchesPageStateItem(item, matcher) {
2819
4051
  return (includesNormalized(item.selector, matcher.selector)
2820
4052
  && equalsNormalized(item.testId, matcher.testId)
2821
- && includesNormalized(item.text, matcher.textContains)
2822
- && includesNormalized(item.label, matcher.labelContains)
2823
- && includesNormalized(item.title, matcher.titleContains)
4053
+ && matchesTextValue(item.text, matcher.textContains, matcher.exact)
4054
+ && matchesTextValue(item.label, matcher.labelContains, matcher.exact)
4055
+ && matchesTextValue(item.title, matcher.titleContains, matcher.exact)
4056
+ && equalsNormalized(item.role, matcher.role)
4057
+ && matchesTextValue(item.name, matcher.name, matcher.exact)
4058
+ && matchesTextValue(item.placeholder, matcher.placeholder, matcher.exact)
4059
+ && matchesTextValue(item.altText, matcher.altText, matcher.exact)
4060
+ && includesNormalized(item.frameUrl, matcher.frameUrlContains)
4061
+ && includesNormalized(item.frameTitle, matcher.frameTitleContains)
2824
4062
  && includesNormalized(item.url, matcher.urlContains)
2825
4063
  && equalsNormalized(item.language, matcher.language)
2826
4064
  && equalsNormalized(item.tagName, matcher.tagName)
2827
4065
  && equalsNormalized(item.type, matcher.type)
4066
+ && equalsOptionalBoolean(item.visible, matcher.visible)
2828
4067
  && equalsOptionalBoolean(item.disabled, matcher.disabled)
2829
4068
  && equalsOptionalBoolean(item.selected, matcher.selected)
2830
4069
  && equalsOptionalBoolean(item.pressed, matcher.pressed)
@@ -2881,7 +4120,7 @@ function createPageChangeSummary(previousCapture, currentCapture) {
2881
4120
  const previousSummary = previous?.summary;
2882
4121
  const currentSummary = current.summary;
2883
4122
  const summaryDelta = {};
2884
- for (const key of ['buttons', 'inputs', 'modals']) {
4123
+ for (const key of ['buttons', 'links', 'inputs', 'modals']) {
2885
4124
  const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
2886
4125
  const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
2887
4126
  if (previousValue !== currentValue && currentValue !== undefined) {
@@ -2910,13 +4149,13 @@ function createPageChangeSummary(previousCapture, currentCapture) {
2910
4149
  }
2911
4150
  function resolveInteractiveKinds(value) {
2912
4151
  if (!Array.isArray(value) || value.length === 0) {
2913
- return ['buttons', 'inputs', 'modals', 'focused'];
4152
+ return ['buttons', 'links', 'inputs', 'modals', 'focused'];
2914
4153
  }
2915
- const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
4154
+ const allowed = new Set(['buttons', 'links', 'inputs', 'modals', 'focused']);
2916
4155
  const kinds = value
2917
4156
  .filter((entry) => typeof entry === 'string' && allowed.has(entry))
2918
4157
  .map((entry) => entry);
2919
- return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'inputs', 'modals', 'focused'];
4158
+ return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'links', 'inputs', 'modals', 'focused'];
2920
4159
  }
2921
4160
  function collectInteractiveElementRefs(payload, kinds, maxItems) {
2922
4161
  const refs = [];
@@ -3036,132 +4275,1345 @@ async function waitForPageStateCondition(sessionId, input, capturePageState) {
3036
4275
  const { lastCapture: _lastCapture, ...waited } = detailed;
3037
4276
  return waited;
3038
4277
  }
3039
- function candidateTextForWorkflowTarget(item) {
3040
- return [item.text, item.label, item.title]
3041
- .filter((value) => typeof value === 'string' && value.trim().length > 0)
3042
- .join(' ')
3043
- .trim();
3044
- }
3045
- function describeWorkflowTargetCandidate(item) {
3046
- return {
3047
- text: candidateTextForWorkflowTarget(item) || undefined,
3048
- testId: typeof item.testId === 'string' ? item.testId : undefined,
3049
- selector: typeof item.selector === 'string' ? item.selector : undefined,
3050
- tagName: typeof item.tagName === 'string' ? item.tagName : undefined,
3051
- type: typeof item.type === 'string' ? item.type : undefined,
3052
- disabled: typeof item.disabled === 'boolean' ? item.disabled : undefined,
3053
- selected: typeof item.selected === 'boolean' ? item.selected : undefined,
3054
- };
3055
- }
3056
- function pickWorkflowTargetItems(payload, scope) {
3057
- if (scope) {
3058
- return pickPageStateScopeItems(payload, scope);
3059
- }
3060
- return [
3061
- ...pickPageStateScopeItems(payload, 'buttons'),
3062
- ...pickPageStateScopeItems(payload, 'inputs'),
3063
- ...pickPageStateScopeItems(payload, 'modals'),
3064
- ...pickPageStateScopeItems(payload, 'focused'),
3065
- ];
3066
- }
3067
- function matchesWorkflowActionTarget(item, target) {
3068
- return (equalsNormalized(item.testId, target.testId)
3069
- && includesNormalized(item.text, target.textContains)
3070
- && includesNormalized(item.label, target.labelContains)
3071
- && includesNormalized(item.title, target.titleContains)
3072
- && equalsNormalized(item.tagName, target.tagName)
3073
- && equalsNormalized(item.type, target.type)
3074
- && equalsOptionalBoolean(item.disabled, target.disabled)
3075
- && equalsOptionalBoolean(item.selected, target.selected)
3076
- && equalsOptionalBoolean(item.pressed, target.pressed)
3077
- && equalsOptionalBoolean(item.expanded, target.expanded)
3078
- && equalsOptionalBoolean(item.readOnly, target.readOnly)
3079
- && equalsOptionalBoolean(item.required, target.requiredField)
3080
- && (typeof item.elementRef === 'string' || typeof item.selector === 'string'));
3081
- }
3082
- function summarizeWorkflowTargetMatcher(target) {
3083
- return {
3084
- scope: target.scope,
3085
- selector: target.selector,
3086
- elementRef: target.elementRef,
3087
- testId: target.testId,
3088
- textContains: target.textContains,
3089
- labelContains: target.labelContains,
3090
- titleContains: target.titleContains,
3091
- tagName: target.tagName,
3092
- type: target.type,
3093
- disabled: target.disabled,
3094
- selected: target.selected,
3095
- pressed: target.pressed,
3096
- expanded: target.expanded,
3097
- readOnly: target.readOnly,
3098
- requiredField: target.requiredField,
3099
- };
3100
- }
3101
- async function resolveWorkflowActionTarget(sessionId, target, capturePageState, existingCapture) {
3102
- if (!target) {
3103
- return {
3104
- resolution: {
3105
- strategy: 'none',
3106
- },
3107
- };
4278
+ function compileWaitRegex(value, fieldName) {
4279
+ if (!value) {
4280
+ return undefined;
3108
4281
  }
3109
- if (target.elementRef || target.selector) {
3110
- return {
3111
- target: {
3112
- elementRef: target.elementRef,
3113
- selector: target.selector,
3114
- tabId: target.tabId,
3115
- frameId: target.frameId,
3116
- url: target.url,
3117
- },
3118
- resolution: {
3119
- strategy: target.elementRef ? 'elementRef' : 'selector',
3120
- matcher: summarizeWorkflowTargetMatcher(target),
3121
- },
3122
- };
4282
+ try {
4283
+ return new RegExp(value);
4284
+ }
4285
+ catch {
4286
+ throw new Error(`${fieldName} must be a valid regular expression`);
3123
4287
  }
3124
- const capture = existingCapture ?? await capturePageState(sessionId, {
3125
- includeButtons: target.scope ? target.scope === 'buttons' : true,
3126
- includeInputs: target.scope ? target.scope === 'inputs' : true,
3127
- includeModals: target.scope ? target.scope === 'modals' : true,
3128
- maxItems: 100,
3129
- maxTextLength: 120,
4288
+ }
4289
+ function matchesUrlWait(url, wait) {
4290
+ return matchesUrlPredicates(url, {
4291
+ exactUrl: wait.exactUrl,
4292
+ urlContains: wait.urlContains,
4293
+ urlRegex: wait.urlRegex,
3130
4294
  });
3131
- const candidates = pickWorkflowTargetItems(capture.payload, target.scope)
3132
- .filter((item) => matchesWorkflowActionTarget(item, target));
3133
- if (candidates.length === 0) {
3134
- throw new WorkflowTargetResolutionError('workflow_target_not_found', 'No interactive element matched the workflow target.', {
3135
- matcher: summarizeWorkflowTargetMatcher(target),
3136
- searchedScope: target.scope ?? 'all-interactive',
3137
- sampledCandidates: pickWorkflowTargetItems(capture.payload, target.scope)
3138
- .slice(0, 5)
3139
- .map((item) => describeWorkflowTargetCandidate(item)),
3140
- });
4295
+ }
4296
+ function matchesUrlPredicates(url, predicates) {
4297
+ if (typeof url !== 'string') {
4298
+ return false;
3141
4299
  }
3142
- if (candidates.length > 1) {
3143
- throw new WorkflowTargetResolutionError('workflow_target_ambiguous', `Workflow target matched ${candidates.length} elements; refine the matcher.`, {
3144
- matcher: summarizeWorkflowTargetMatcher(target),
3145
- matchedCandidateCount: candidates.length,
3146
- sampledCandidates: candidates.slice(0, 5).map((item) => describeWorkflowTargetCandidate(item)),
4300
+ if (predicates.exactUrl && url !== predicates.exactUrl) {
4301
+ return false;
4302
+ }
4303
+ if (predicates.urlContains && !url.includes(predicates.urlContains)) {
4304
+ return false;
4305
+ }
4306
+ const regex = compileWaitRegex(predicates.urlRegex, predicates.regexFieldName ?? 'urlRegex');
4307
+ if (regex && !regex.test(url)) {
4308
+ return false;
4309
+ }
4310
+ return true;
4311
+ }
4312
+ function resolveAutomationWaitSinceTs(value) {
4313
+ return resolveOptionalTimestamp(value) ?? Math.max(0, Date.now() - DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS);
4314
+ }
4315
+ function isElementMissingError(error) {
4316
+ const message = error instanceof Error ? error.message : String(error);
4317
+ return /no element found for selector/i.test(message);
4318
+ }
4319
+ async function captureSelectorState(captureClient, sessionId, selector, frameId = 0) {
4320
+ try {
4321
+ const [styleCapture, layoutCapture] = await Promise.all([
4322
+ executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties: ['display', 'visibility', 'opacity'] }, 3_000),
4323
+ executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000),
4324
+ ]);
4325
+ const stylePayload = ensureCaptureSuccess(styleCapture, sessionId);
4326
+ const layoutPayload = ensureCaptureSuccess(layoutCapture, sessionId);
4327
+ const properties = isRecord(stylePayload.properties) ? stylePayload.properties : {};
4328
+ const element = isRecord(layoutPayload.element) ? layoutPayload.element : {};
4329
+ const width = typeof element.width === 'number' ? element.width : 0;
4330
+ const height = typeof element.height === 'number' ? element.height : 0;
4331
+ const display = typeof properties.display === 'string' ? properties.display : undefined;
4332
+ const visibility = typeof properties.visibility === 'string' ? properties.visibility : undefined;
4333
+ const opacityText = typeof properties.opacity === 'string' ? properties.opacity : undefined;
4334
+ const opacity = opacityText !== undefined ? Number.parseFloat(opacityText) : undefined;
4335
+ const visible = display !== 'none'
4336
+ && visibility !== 'hidden'
4337
+ && visibility !== 'collapse'
4338
+ && opacity !== 0
4339
+ && width > 0
4340
+ && height > 0;
4341
+ return {
4342
+ selector,
4343
+ frameId,
4344
+ attached: true,
4345
+ visible,
4346
+ styles: properties,
4347
+ element,
4348
+ viewport: layoutPayload.viewport,
4349
+ };
4350
+ }
4351
+ catch (error) {
4352
+ if (isElementMissingError(error)) {
4353
+ return {
4354
+ selector,
4355
+ frameId,
4356
+ attached: false,
4357
+ visible: false,
4358
+ missing: true,
4359
+ message: error instanceof Error ? error.message : String(error),
4360
+ };
4361
+ }
4362
+ throw error;
4363
+ }
4364
+ }
4365
+ function selectorStateMatches(state, expectedState) {
4366
+ const attached = state.attached === true;
4367
+ const visible = state.visible === true;
4368
+ switch (expectedState) {
4369
+ case 'attached':
4370
+ return attached;
4371
+ case 'detached':
4372
+ return !attached;
4373
+ case 'visible':
4374
+ return attached && visible;
4375
+ case 'hidden':
4376
+ return !attached || !visible;
4377
+ }
4378
+ }
4379
+ async function waitForUrlCondition(sessionId, wait, capturePageState) {
4380
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4381
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4382
+ const startedAt = Date.now();
4383
+ const deadline = startedAt + timeoutMs;
4384
+ let attempts = 0;
4385
+ let lastPage;
4386
+ while (Date.now() <= deadline) {
4387
+ attempts += 1;
4388
+ const capture = await capturePageState(sessionId, {
4389
+ includeButtons: false,
4390
+ includeLinks: false,
4391
+ includeInputs: false,
4392
+ includeModals: false,
4393
+ maxItems: 1,
4394
+ maxTextLength: 40,
3147
4395
  });
4396
+ lastPage = {
4397
+ url: capture.payload.url,
4398
+ title: capture.payload.title,
4399
+ language: capture.payload.language,
4400
+ viewport: capture.payload.viewport,
4401
+ };
4402
+ if (matchesUrlWait(capture.payload.url, wait)) {
4403
+ return {
4404
+ waitKind: 'url',
4405
+ matched: true,
4406
+ waitedMs: Date.now() - startedAt,
4407
+ attempts,
4408
+ timeoutMs,
4409
+ pollIntervalMs,
4410
+ evidence: { page: lastPage },
4411
+ };
4412
+ }
4413
+ await sleep(pollIntervalMs);
4414
+ }
4415
+ return {
4416
+ waitKind: 'url',
4417
+ matched: false,
4418
+ waitedMs: Date.now() - startedAt,
4419
+ attempts,
4420
+ timeoutMs,
4421
+ pollIntervalMs,
4422
+ evidence: {
4423
+ page: lastPage,
4424
+ expected: wait,
4425
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4426
+ waitKind: 'url',
4427
+ timeoutMs,
4428
+ waitedMs: Date.now() - startedAt,
4429
+ attempts,
4430
+ pollIntervalMs,
4431
+ matcherSummary: {
4432
+ exactUrl: wait.exactUrl,
4433
+ urlContains: wait.urlContains,
4434
+ urlRegex: wait.urlRegex,
4435
+ },
4436
+ lastObserved: lastPage,
4437
+ }),
4438
+ },
4439
+ error: {
4440
+ code: 'url_wait_timeout',
4441
+ message: 'Timed out waiting for the page URL to match the requested condition.',
4442
+ },
4443
+ };
4444
+ }
4445
+ function pageReadyStateMatches(readyState, expectedState) {
4446
+ if (readyState !== 'loading' && readyState !== 'interactive' && readyState !== 'complete') {
4447
+ return false;
4448
+ }
4449
+ if (expectedState === 'domcontentloaded') {
4450
+ return readyState === 'interactive' || readyState === 'complete';
4451
+ }
4452
+ return readyState === 'complete';
4453
+ }
4454
+ async function waitForLoadStateCondition(sessionId, wait, capturePageState) {
4455
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4456
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4457
+ const expectedState = wait.state ?? 'load';
4458
+ const startedAt = Date.now();
4459
+ const deadline = startedAt + timeoutMs;
4460
+ let attempts = 0;
4461
+ let lastPage;
4462
+ while (Date.now() <= deadline) {
4463
+ attempts += 1;
4464
+ const capture = await capturePageState(sessionId, {
4465
+ includeButtons: false,
4466
+ includeLinks: false,
4467
+ includeInputs: false,
4468
+ includeModals: false,
4469
+ maxItems: 1,
4470
+ maxTextLength: 40,
4471
+ });
4472
+ lastPage = {
4473
+ url: capture.payload.url,
4474
+ title: capture.payload.title,
4475
+ readyState: capture.payload.readyState,
4476
+ language: capture.payload.language,
4477
+ viewport: capture.payload.viewport,
4478
+ };
4479
+ const urlMatches = matchesUrlPredicates(typeof capture.payload.url === 'string' ? capture.payload.url : undefined, {
4480
+ exactUrl: wait.exactUrl,
4481
+ urlContains: wait.urlContains,
4482
+ urlRegex: wait.urlRegex,
4483
+ });
4484
+ if (urlMatches && pageReadyStateMatches(capture.payload.readyState, expectedState)) {
4485
+ return {
4486
+ waitKind: 'load_state',
4487
+ matched: true,
4488
+ waitedMs: Date.now() - startedAt,
4489
+ attempts,
4490
+ timeoutMs,
4491
+ pollIntervalMs,
4492
+ evidence: {
4493
+ state: expectedState,
4494
+ page: lastPage,
4495
+ },
4496
+ };
4497
+ }
4498
+ await sleep(pollIntervalMs);
4499
+ }
4500
+ return {
4501
+ waitKind: 'load_state',
4502
+ matched: false,
4503
+ waitedMs: Date.now() - startedAt,
4504
+ attempts,
4505
+ timeoutMs,
4506
+ pollIntervalMs,
4507
+ evidence: {
4508
+ state: expectedState,
4509
+ page: lastPage,
4510
+ expected: wait,
4511
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4512
+ waitKind: 'load_state',
4513
+ timeoutMs,
4514
+ waitedMs: Date.now() - startedAt,
4515
+ attempts,
4516
+ pollIntervalMs,
4517
+ matcherSummary: {
4518
+ state: expectedState,
4519
+ exactUrl: wait.exactUrl,
4520
+ urlContains: wait.urlContains,
4521
+ urlRegex: wait.urlRegex,
4522
+ },
4523
+ lastObserved: lastPage,
4524
+ }),
4525
+ },
4526
+ error: {
4527
+ code: 'load_state_wait_timeout',
4528
+ message: `Timed out waiting for page load state "${expectedState}".`,
4529
+ },
4530
+ };
4531
+ }
4532
+ async function waitForSelectorStateCondition(sessionId, wait, captureClient) {
4533
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4534
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4535
+ const expectedState = wait.state ?? 'visible';
4536
+ const startedAt = Date.now();
4537
+ const deadline = startedAt + timeoutMs;
4538
+ let attempts = 0;
4539
+ let lastState;
4540
+ while (Date.now() <= deadline) {
4541
+ attempts += 1;
4542
+ lastState = await captureSelectorState(captureClient, sessionId, wait.selector, wait.frameId);
4543
+ if (selectorStateMatches(lastState, expectedState)) {
4544
+ return {
4545
+ waitKind: 'selector_state',
4546
+ matched: true,
4547
+ waitedMs: Date.now() - startedAt,
4548
+ attempts,
4549
+ timeoutMs,
4550
+ pollIntervalMs,
4551
+ evidence: {
4552
+ selector: wait.selector,
4553
+ state: expectedState,
4554
+ selectorState: lastState,
4555
+ },
4556
+ };
4557
+ }
4558
+ await sleep(pollIntervalMs);
4559
+ }
4560
+ return {
4561
+ waitKind: 'selector_state',
4562
+ matched: false,
4563
+ waitedMs: Date.now() - startedAt,
4564
+ attempts,
4565
+ timeoutMs,
4566
+ pollIntervalMs,
4567
+ evidence: {
4568
+ selector: wait.selector,
4569
+ state: expectedState,
4570
+ selectorState: lastState,
4571
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4572
+ waitKind: 'selector_state',
4573
+ timeoutMs,
4574
+ waitedMs: Date.now() - startedAt,
4575
+ attempts,
4576
+ pollIntervalMs,
4577
+ matcherSummary: {
4578
+ selector: wait.selector,
4579
+ state: expectedState,
4580
+ frameId: wait.frameId,
4581
+ },
4582
+ lastObserved: lastState,
4583
+ }),
4584
+ },
4585
+ error: {
4586
+ code: 'selector_state_wait_timeout',
4587
+ message: `Timed out waiting for selector "${wait.selector}" to become ${expectedState}.`,
4588
+ },
4589
+ };
4590
+ }
4591
+ async function waitForConsoleCondition(sessionId, wait, captureClient) {
4592
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4593
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4594
+ const levels = resolveLiveConsoleLevels(wait.levels);
4595
+ const contains = normalizeOptionalString(wait.contains);
4596
+ const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
4597
+ const includeRuntimeErrors = wait.includeRuntimeErrors !== false;
4598
+ const startedAt = Date.now();
4599
+ const deadline = startedAt + timeoutMs;
4600
+ let attempts = 0;
4601
+ let lastLogs = [];
4602
+ while (Date.now() <= deadline) {
4603
+ attempts += 1;
4604
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
4605
+ levels,
4606
+ contains,
4607
+ sinceTs,
4608
+ includeRuntimeErrors,
4609
+ limit: 10,
4610
+ }, 3_000);
4611
+ const payload = ensureCaptureSuccess(capture, sessionId);
4612
+ lastLogs = asRecordArray(payload.logs);
4613
+ if (lastLogs.length > 0) {
4614
+ return {
4615
+ waitKind: 'console',
4616
+ matched: true,
4617
+ waitedMs: Date.now() - startedAt,
4618
+ attempts,
4619
+ timeoutMs,
4620
+ pollIntervalMs,
4621
+ evidence: {
4622
+ filters: { levels, contains, sinceTs, includeRuntimeErrors },
4623
+ logs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
4624
+ },
4625
+ };
4626
+ }
4627
+ await sleep(pollIntervalMs);
3148
4628
  }
3149
- const candidate = candidates[0];
3150
4629
  return {
3151
- target: {
3152
- elementRef: typeof candidate.elementRef === 'string' ? candidate.elementRef : undefined,
3153
- selector: typeof candidate.selector === 'string' ? candidate.selector : undefined,
3154
- tabId: target.tabId,
3155
- frameId: target.frameId,
3156
- url: target.url,
3157
- },
3158
- resolution: {
3159
- strategy: typeof candidate.elementRef === 'string' ? 'semantic_elementRef' : 'semantic_selector',
3160
- matcher: summarizeWorkflowTargetMatcher(target),
3161
- matchedCandidateCount: candidates.length,
3162
- matched: describeWorkflowTargetCandidate(candidate),
3163
- },
3164
- pageCapture: capture,
4630
+ waitKind: 'console',
4631
+ matched: false,
4632
+ waitedMs: Date.now() - startedAt,
4633
+ attempts,
4634
+ timeoutMs,
4635
+ pollIntervalMs,
4636
+ evidence: {
4637
+ filters: { levels, contains, sinceTs, includeRuntimeErrors },
4638
+ sampledLogs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
4639
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4640
+ waitKind: 'console',
4641
+ timeoutMs,
4642
+ waitedMs: Date.now() - startedAt,
4643
+ attempts,
4644
+ pollIntervalMs,
4645
+ matcherSummary: { levels, contains, sinceTs, includeRuntimeErrors },
4646
+ lastObserved: lastLogs[0],
4647
+ candidateCount: lastLogs.length,
4648
+ sampledCandidates: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
4649
+ }),
4650
+ },
4651
+ error: {
4652
+ code: 'console_wait_timeout',
4653
+ message: 'Timed out waiting for a matching live console log.',
4654
+ },
4655
+ };
4656
+ }
4657
+ async function waitForDialogCondition(sessionId, wait, captureClient) {
4658
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4659
+ const startedAt = Date.now();
4660
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DIALOG', {
4661
+ type: wait.type,
4662
+ messageContains: wait.messageContains,
4663
+ urlContains: wait.urlContains,
4664
+ action: wait.action,
4665
+ promptText: wait.promptText,
4666
+ tabId: wait.tabId,
4667
+ timeoutMs,
4668
+ }, timeoutMs + 2_000);
4669
+ const payload = ensureCaptureSuccess(capture, sessionId);
4670
+ const matched = payload.matched === true;
4671
+ return {
4672
+ waitKind: 'dialog',
4673
+ matched,
4674
+ waitedMs: Date.now() - startedAt,
4675
+ attempts: 1,
4676
+ timeoutMs,
4677
+ pollIntervalMs: timeoutMs,
4678
+ evidence: {
4679
+ filters: {
4680
+ type: wait.type,
4681
+ messageContains: wait.messageContains,
4682
+ urlContains: wait.urlContains,
4683
+ action: wait.action,
4684
+ tabId: wait.tabId,
4685
+ },
4686
+ dialog: matched ? payload : undefined,
4687
+ expected: matched ? undefined : payload.expected ?? wait,
4688
+ timeoutDiagnostics: matched
4689
+ ? undefined
4690
+ : buildWaitTimeoutDiagnostics({
4691
+ waitKind: 'dialog',
4692
+ timeoutMs,
4693
+ waitedMs: Date.now() - startedAt,
4694
+ attempts: 1,
4695
+ pollIntervalMs: timeoutMs,
4696
+ matcherSummary: {
4697
+ type: wait.type,
4698
+ messageContains: wait.messageContains,
4699
+ urlContains: wait.urlContains,
4700
+ action: wait.action,
4701
+ tabId: wait.tabId,
4702
+ },
4703
+ lastObserved: payload.lastObserved,
4704
+ candidateCount: typeof payload.observedCount === 'number' ? payload.observedCount : undefined,
4705
+ }),
4706
+ },
4707
+ error: matched
4708
+ ? undefined
4709
+ : {
4710
+ code: 'dialog_wait_timeout',
4711
+ message: 'Timed out waiting for a matching JavaScript dialog.',
4712
+ },
4713
+ };
4714
+ }
4715
+ async function waitForStableLayoutCondition(sessionId, wait, captureClient) {
4716
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4717
+ const stableMs = wait.stableMs;
4718
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4719
+ const startedAt = Date.now();
4720
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_STABLE_LAYOUT', {
4721
+ selector: wait.selector,
4722
+ stableMs,
4723
+ tabId: wait.tabId,
4724
+ timeoutMs,
4725
+ pollIntervalMs,
4726
+ }, timeoutMs + 2_000);
4727
+ const payload = ensureCaptureSuccess(capture, sessionId);
4728
+ const matched = payload.matched === true;
4729
+ return {
4730
+ waitKind: 'stable_layout',
4731
+ matched,
4732
+ waitedMs: Date.now() - startedAt,
4733
+ attempts: 1,
4734
+ timeoutMs,
4735
+ pollIntervalMs,
4736
+ evidence: {
4737
+ filters: {
4738
+ selector: wait.selector,
4739
+ stableMs,
4740
+ tabId: wait.tabId,
4741
+ },
4742
+ layout: payload,
4743
+ timeoutDiagnostics: matched
4744
+ ? undefined
4745
+ : buildWaitTimeoutDiagnostics({
4746
+ waitKind: 'stable_layout',
4747
+ timeoutMs,
4748
+ waitedMs: Date.now() - startedAt,
4749
+ attempts: typeof payload.attempts === 'number' ? payload.attempts : 1,
4750
+ pollIntervalMs,
4751
+ matcherSummary: {
4752
+ selector: wait.selector,
4753
+ stableMs,
4754
+ tabId: wait.tabId,
4755
+ },
4756
+ lastObserved: payload.snapshot,
4757
+ }),
4758
+ },
4759
+ error: matched
4760
+ ? undefined
4761
+ : {
4762
+ code: 'stable_layout_wait_timeout',
4763
+ message: 'Timed out waiting for layout to stay stable.',
4764
+ },
4765
+ };
4766
+ }
4767
+ function mapNavigationWaitEvent(row) {
4768
+ const payload = readJsonPayload(row.payload_json);
4769
+ return {
4770
+ eventId: row.event_id,
4771
+ sessionId: row.session_id,
4772
+ timestamp: row.ts,
4773
+ tabId: row.tab_id ?? undefined,
4774
+ origin: row.origin ?? undefined,
4775
+ url: resolveLastUrl(payload),
4776
+ from: typeof payload.from === 'string' ? payload.from : undefined,
4777
+ trigger: typeof payload.trigger === 'string' ? payload.trigger : undefined,
4778
+ payload,
4779
+ };
4780
+ }
4781
+ function navigationEventMatches(row, wait) {
4782
+ const payload = readJsonPayload(row.payload_json);
4783
+ const toUrl = resolveLastUrl(payload);
4784
+ const fromUrl = typeof payload.from === 'string' ? payload.from : undefined;
4785
+ const trigger = typeof payload.trigger === 'string' ? payload.trigger : undefined;
4786
+ if (!matchesUrlPredicates(toUrl, {
4787
+ exactUrl: wait.exactUrl,
4788
+ urlContains: wait.urlContains,
4789
+ urlRegex: wait.urlRegex,
4790
+ })) {
4791
+ return false;
4792
+ }
4793
+ if (wait.fromUrlContains || wait.fromUrlRegex) {
4794
+ if (!matchesUrlPredicates(fromUrl, {
4795
+ urlContains: wait.fromUrlContains,
4796
+ urlRegex: wait.fromUrlRegex,
4797
+ regexFieldName: 'fromUrlRegex',
4798
+ })) {
4799
+ return false;
4800
+ }
4801
+ }
4802
+ if (wait.trigger && trigger !== wait.trigger) {
4803
+ return false;
4804
+ }
4805
+ return true;
4806
+ }
4807
+ function queryNavigationWaitCandidates(db, options) {
4808
+ const where = ['session_id = ?', "type = 'nav'", 'ts >= ?'];
4809
+ const params = [options.sessionId, options.sinceTs];
4810
+ if (options.tabId !== undefined) {
4811
+ where.push('tab_id = ?');
4812
+ params.push(options.tabId);
4813
+ }
4814
+ return db.prepare(`SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
4815
+ FROM events
4816
+ WHERE ${where.join(' AND ')}
4817
+ ORDER BY ts ASC
4818
+ LIMIT 200`).all(...params);
4819
+ }
4820
+ async function waitForNavigationCondition(sessionId, wait, db) {
4821
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4822
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4823
+ const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
4824
+ const tabId = resolveOptionalTabId(wait.tabId);
4825
+ const startedAt = Date.now();
4826
+ const deadline = startedAt + timeoutMs;
4827
+ let attempts = 0;
4828
+ let lastEvents = [];
4829
+ while (Date.now() <= deadline) {
4830
+ attempts += 1;
4831
+ lastEvents = queryNavigationWaitCandidates(db, { sessionId, sinceTs, tabId });
4832
+ const matched = lastEvents.find((row) => navigationEventMatches(row, wait));
4833
+ if (matched) {
4834
+ return {
4835
+ waitKind: 'navigation',
4836
+ matched: true,
4837
+ waitedMs: Date.now() - startedAt,
4838
+ attempts,
4839
+ timeoutMs,
4840
+ pollIntervalMs,
4841
+ evidence: {
4842
+ filters: {
4843
+ urlContains: wait.urlContains,
4844
+ urlRegex: wait.urlRegex,
4845
+ exactUrl: wait.exactUrl,
4846
+ fromUrlContains: wait.fromUrlContains,
4847
+ fromUrlRegex: wait.fromUrlRegex,
4848
+ trigger: wait.trigger,
4849
+ sinceTs,
4850
+ tabId,
4851
+ },
4852
+ navigation: mapNavigationWaitEvent(matched),
4853
+ },
4854
+ };
4855
+ }
4856
+ await sleep(pollIntervalMs);
4857
+ }
4858
+ return {
4859
+ waitKind: 'navigation',
4860
+ matched: false,
4861
+ waitedMs: Date.now() - startedAt,
4862
+ attempts,
4863
+ timeoutMs,
4864
+ pollIntervalMs,
4865
+ evidence: {
4866
+ filters: {
4867
+ urlContains: wait.urlContains,
4868
+ urlRegex: wait.urlRegex,
4869
+ exactUrl: wait.exactUrl,
4870
+ fromUrlContains: wait.fromUrlContains,
4871
+ fromUrlRegex: wait.fromUrlRegex,
4872
+ trigger: wait.trigger,
4873
+ sinceTs,
4874
+ tabId,
4875
+ },
4876
+ sampledEvents: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
4877
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4878
+ waitKind: 'navigation',
4879
+ timeoutMs,
4880
+ waitedMs: Date.now() - startedAt,
4881
+ attempts,
4882
+ pollIntervalMs,
4883
+ matcherSummary: {
4884
+ urlContains: wait.urlContains,
4885
+ urlRegex: wait.urlRegex,
4886
+ exactUrl: wait.exactUrl,
4887
+ fromUrlContains: wait.fromUrlContains,
4888
+ fromUrlRegex: wait.fromUrlRegex,
4889
+ trigger: wait.trigger,
4890
+ sinceTs,
4891
+ tabId,
4892
+ },
4893
+ lastObserved: lastEvents.length > 0 ? mapNavigationWaitEvent(lastEvents[lastEvents.length - 1]) : undefined,
4894
+ candidateCount: lastEvents.length,
4895
+ sampledCandidates: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
4896
+ }),
4897
+ },
4898
+ error: {
4899
+ code: 'navigation_wait_timeout',
4900
+ message: 'Timed out waiting for a matching navigation event.',
4901
+ },
4902
+ };
4903
+ }
4904
+ async function waitForNavigationLifecycleCondition(sessionId, wait, captureClient) {
4905
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4906
+ const startedAt = Date.now();
4907
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_NAVIGATION_LIFECYCLE', {
4908
+ state: wait.state,
4909
+ urlContains: wait.urlContains,
4910
+ urlRegex: wait.urlRegex,
4911
+ exactUrl: wait.exactUrl,
4912
+ tabId: wait.tabId,
4913
+ timeoutMs,
4914
+ }, timeoutMs + 2_000);
4915
+ const payload = ensureCaptureSuccess(capture, sessionId);
4916
+ const matched = payload.matched === true;
4917
+ return {
4918
+ waitKind: 'navigation_lifecycle',
4919
+ matched,
4920
+ waitedMs: Date.now() - startedAt,
4921
+ attempts: 1,
4922
+ timeoutMs,
4923
+ pollIntervalMs: timeoutMs,
4924
+ evidence: {
4925
+ filters: {
4926
+ state: wait.state,
4927
+ urlContains: wait.urlContains,
4928
+ urlRegex: wait.urlRegex,
4929
+ exactUrl: wait.exactUrl,
4930
+ tabId: wait.tabId,
4931
+ },
4932
+ lifecycle: matched ? payload : undefined,
4933
+ expected: matched ? undefined : payload.expected ?? wait,
4934
+ timeoutDiagnostics: matched
4935
+ ? undefined
4936
+ : buildWaitTimeoutDiagnostics({
4937
+ waitKind: 'navigation_lifecycle',
4938
+ timeoutMs,
4939
+ waitedMs: Date.now() - startedAt,
4940
+ attempts: 1,
4941
+ pollIntervalMs: timeoutMs,
4942
+ matcherSummary: {
4943
+ state: wait.state,
4944
+ urlContains: wait.urlContains,
4945
+ urlRegex: wait.urlRegex,
4946
+ exactUrl: wait.exactUrl,
4947
+ tabId: wait.tabId,
4948
+ },
4949
+ lastObserved: payload.lastObserved,
4950
+ candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
4951
+ }),
4952
+ },
4953
+ error: matched
4954
+ ? undefined
4955
+ : {
4956
+ code: 'navigation_lifecycle_wait_timeout',
4957
+ message: 'Timed out waiting for a matching navigation lifecycle event.',
4958
+ },
4959
+ };
4960
+ }
4961
+ async function waitForDownloadCondition(sessionId, wait, captureClient) {
4962
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4963
+ const startedAt = Date.now();
4964
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DOWNLOAD', {
4965
+ urlContains: wait.urlContains,
4966
+ urlRegex: wait.urlRegex,
4967
+ exactUrl: wait.exactUrl,
4968
+ filenameContains: wait.filenameContains,
4969
+ filenameRegex: wait.filenameRegex,
4970
+ state: wait.state,
4971
+ tabId: wait.tabId,
4972
+ timeoutMs,
4973
+ }, timeoutMs + 2_000);
4974
+ const payload = ensureCaptureSuccess(capture, sessionId);
4975
+ const matched = payload.matched === true;
4976
+ return {
4977
+ waitKind: 'download',
4978
+ matched,
4979
+ waitedMs: Date.now() - startedAt,
4980
+ attempts: 1,
4981
+ timeoutMs,
4982
+ pollIntervalMs: timeoutMs,
4983
+ evidence: {
4984
+ filters: {
4985
+ urlContains: wait.urlContains,
4986
+ urlRegex: wait.urlRegex,
4987
+ exactUrl: wait.exactUrl,
4988
+ filenameContains: wait.filenameContains,
4989
+ filenameRegex: wait.filenameRegex,
4990
+ state: wait.state,
4991
+ tabId: wait.tabId,
4992
+ },
4993
+ download: matched ? payload : undefined,
4994
+ expected: matched ? undefined : payload.expected ?? wait,
4995
+ timeoutDiagnostics: matched
4996
+ ? undefined
4997
+ : buildWaitTimeoutDiagnostics({
4998
+ waitKind: 'download',
4999
+ timeoutMs,
5000
+ waitedMs: Date.now() - startedAt,
5001
+ attempts: 1,
5002
+ pollIntervalMs: timeoutMs,
5003
+ matcherSummary: {
5004
+ urlContains: wait.urlContains,
5005
+ urlRegex: wait.urlRegex,
5006
+ exactUrl: wait.exactUrl,
5007
+ filenameContains: wait.filenameContains,
5008
+ filenameRegex: wait.filenameRegex,
5009
+ state: wait.state,
5010
+ tabId: wait.tabId,
5011
+ },
5012
+ lastObserved: payload.lastObserved ?? payload.lastMatchedDownload,
5013
+ candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
5014
+ }),
5015
+ },
5016
+ error: matched
5017
+ ? undefined
5018
+ : {
5019
+ code: 'download_wait_timeout',
5020
+ message: 'Timed out waiting for a matching download.',
5021
+ },
5022
+ };
5023
+ }
5024
+ async function waitForPopupCondition(sessionId, wait, captureClient) {
5025
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
5026
+ const startedAt = Date.now();
5027
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_POPUP', {
5028
+ urlContains: wait.urlContains,
5029
+ urlRegex: wait.urlRegex,
5030
+ exactUrl: wait.exactUrl,
5031
+ openerTabId: wait.openerTabId,
5032
+ timeoutMs,
5033
+ }, timeoutMs + 2_000);
5034
+ const payload = ensureCaptureSuccess(capture, sessionId);
5035
+ const matched = payload.matched === true;
5036
+ return {
5037
+ waitKind: 'popup',
5038
+ matched,
5039
+ waitedMs: Date.now() - startedAt,
5040
+ attempts: 1,
5041
+ timeoutMs,
5042
+ pollIntervalMs: timeoutMs,
5043
+ evidence: {
5044
+ filters: {
5045
+ urlContains: wait.urlContains,
5046
+ urlRegex: wait.urlRegex,
5047
+ exactUrl: wait.exactUrl,
5048
+ openerTabId: wait.openerTabId,
5049
+ },
5050
+ popup: matched ? payload : undefined,
5051
+ expected: matched ? undefined : payload.expected ?? wait,
5052
+ timeoutDiagnostics: matched
5053
+ ? undefined
5054
+ : buildWaitTimeoutDiagnostics({
5055
+ waitKind: 'popup',
5056
+ timeoutMs,
5057
+ waitedMs: Date.now() - startedAt,
5058
+ attempts: 1,
5059
+ pollIntervalMs: timeoutMs,
5060
+ matcherSummary: {
5061
+ urlContains: wait.urlContains,
5062
+ urlRegex: wait.urlRegex,
5063
+ exactUrl: wait.exactUrl,
5064
+ openerTabId: wait.openerTabId,
5065
+ },
5066
+ lastObserved: payload.lastObserved,
5067
+ candidateCount: typeof payload.observedPopupCount === 'number' ? payload.observedPopupCount : undefined,
5068
+ sampledCandidates: Array.isArray(payload.pendingTabIds) ? payload.pendingTabIds : undefined,
5069
+ }),
5070
+ },
5071
+ error: matched
5072
+ ? undefined
5073
+ : {
5074
+ code: 'popup_wait_timeout',
5075
+ message: 'Timed out waiting for a matching popup tab.',
5076
+ },
5077
+ };
5078
+ }
5079
+ function normalizeNetworkWaitFilters(wait) {
5080
+ const responseWait = wait.waitKind === 'response' ? wait : undefined;
5081
+ return {
5082
+ urlContains: normalizeOptionalString(wait.urlContains),
5083
+ urlRegex: normalizeOptionalString(wait.urlRegex),
5084
+ exactUrl: normalizeOptionalString(wait.exactUrl),
5085
+ method: normalizeHttpMethod(wait.method),
5086
+ traceId: normalizeOptionalString(wait.traceId),
5087
+ initiator: normalizeOptionalString(wait.initiator),
5088
+ requestContentType: normalizeOptionalString(wait.requestContentType),
5089
+ responseContentType: normalizeOptionalString(responseWait?.responseContentType),
5090
+ statusIn: responseWait ? normalizeStatusIn(responseWait.statusIn) : [],
5091
+ statusGte: responseWait?.statusGte,
5092
+ statusLt: responseWait?.statusLt,
5093
+ errorType: normalizeOptionalString(responseWait?.errorType),
5094
+ sinceTs: resolveAutomationWaitSinceTs(wait.sinceTs),
5095
+ tabId: resolveOptionalTabId(wait.tabId),
5096
+ includeBodies: wait.includeBodies === true,
5097
+ };
5098
+ }
5099
+ function queryNetworkWaitCandidates(db, sessionId, filters) {
5100
+ const where = ['session_id = ?', 'ts_start >= ?'];
5101
+ const params = [sessionId, filters.sinceTs];
5102
+ if (filters.exactUrl) {
5103
+ where.push('url = ?');
5104
+ params.push(filters.exactUrl);
5105
+ }
5106
+ else if (filters.urlContains) {
5107
+ where.push('url LIKE ?');
5108
+ params.push(`%${filters.urlContains}%`);
5109
+ }
5110
+ if (filters.method) {
5111
+ where.push('method = ?');
5112
+ params.push(filters.method);
5113
+ }
5114
+ if (filters.traceId) {
5115
+ where.push('trace_id = ?');
5116
+ params.push(filters.traceId);
5117
+ }
5118
+ if (filters.initiator) {
5119
+ where.push('initiator = ?');
5120
+ params.push(filters.initiator);
5121
+ }
5122
+ if (filters.requestContentType) {
5123
+ where.push('request_content_type LIKE ?');
5124
+ params.push(`%${filters.requestContentType}%`);
5125
+ }
5126
+ if (filters.responseContentType) {
5127
+ where.push('response_content_type LIKE ?');
5128
+ params.push(`%${filters.responseContentType}%`);
5129
+ }
5130
+ const statusIn = Array.isArray(filters.statusIn) ? filters.statusIn : [];
5131
+ if (statusIn.length > 0) {
5132
+ where.push(`status IN (${statusIn.map(() => '?').join(', ')})`);
5133
+ params.push(...statusIn);
5134
+ }
5135
+ if (typeof filters.statusGte === 'number') {
5136
+ where.push('status >= ?');
5137
+ params.push(filters.statusGte);
5138
+ }
5139
+ if (typeof filters.statusLt === 'number') {
5140
+ where.push('status < ?');
5141
+ params.push(filters.statusLt);
5142
+ }
5143
+ if (filters.tabId !== undefined) {
5144
+ where.push('tab_id = ?');
5145
+ params.push(filters.tabId);
5146
+ }
5147
+ return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
5148
+ FROM network
5149
+ WHERE ${where.join(' AND ')}
5150
+ ORDER BY ts_start ASC
5151
+ LIMIT 200`).all(...params);
5152
+ }
5153
+ function networkCallMatchesFilters(row, filters) {
5154
+ if (!matchesUrlPredicates(row.url, {
5155
+ exactUrl: typeof filters.exactUrl === 'string' ? filters.exactUrl : undefined,
5156
+ urlContains: typeof filters.urlContains === 'string' ? filters.urlContains : undefined,
5157
+ urlRegex: typeof filters.urlRegex === 'string' ? filters.urlRegex : undefined,
5158
+ })) {
5159
+ return false;
5160
+ }
5161
+ if (typeof filters.errorType === 'string' && classifyNetworkFailure(row.status, row.error_class) !== filters.errorType) {
5162
+ return false;
5163
+ }
5164
+ return true;
5165
+ }
5166
+ async function waitForNetworkMatchCondition(sessionId, wait, db) {
5167
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
5168
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
5169
+ const filters = normalizeNetworkWaitFilters(wait);
5170
+ const startedAt = Date.now();
5171
+ const deadline = startedAt + timeoutMs;
5172
+ let attempts = 0;
5173
+ let lastCalls = [];
5174
+ while (Date.now() <= deadline) {
5175
+ attempts += 1;
5176
+ lastCalls = queryNetworkWaitCandidates(db, sessionId, filters);
5177
+ const matched = lastCalls.find((row) => networkCallMatchesFilters(row, filters));
5178
+ if (matched) {
5179
+ return {
5180
+ waitKind: wait.waitKind,
5181
+ matched: true,
5182
+ waitedMs: Date.now() - startedAt,
5183
+ attempts,
5184
+ timeoutMs,
5185
+ pollIntervalMs,
5186
+ evidence: {
5187
+ filters,
5188
+ call: mapNetworkCallRecord(matched, filters.includeBodies === true),
5189
+ },
5190
+ };
5191
+ }
5192
+ await sleep(pollIntervalMs);
5193
+ }
5194
+ return {
5195
+ waitKind: wait.waitKind,
5196
+ matched: false,
5197
+ waitedMs: Date.now() - startedAt,
5198
+ attempts,
5199
+ timeoutMs,
5200
+ pollIntervalMs,
5201
+ evidence: {
5202
+ filters,
5203
+ sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
5204
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
5205
+ waitKind: wait.waitKind,
5206
+ timeoutMs,
5207
+ waitedMs: Date.now() - startedAt,
5208
+ attempts,
5209
+ pollIntervalMs,
5210
+ matcherSummary: filters,
5211
+ lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[lastCalls.length - 1], false) : undefined,
5212
+ candidateCount: lastCalls.length,
5213
+ sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
5214
+ }),
5215
+ },
5216
+ error: {
5217
+ code: wait.waitKind === 'request' ? 'request_wait_timeout' : 'response_wait_timeout',
5218
+ message: `Timed out waiting for a matching ${wait.waitKind}.`,
5219
+ },
5220
+ };
5221
+ }
5222
+ function queryRecentNetworkActivity(db, options) {
5223
+ const where = ['session_id = ?', 'ts_start >= ?'];
5224
+ const params = [options.sessionId, options.sinceTs];
5225
+ if (options.urlContains) {
5226
+ where.push('url LIKE ?');
5227
+ params.push(`%${options.urlContains}%`);
5228
+ }
5229
+ if (options.method) {
5230
+ where.push('method = ?');
5231
+ params.push(options.method);
5232
+ }
5233
+ if (options.tabId !== undefined) {
5234
+ where.push('tab_id = ?');
5235
+ params.push(options.tabId);
5236
+ }
5237
+ return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
5238
+ FROM network
5239
+ WHERE ${where.join(' AND ')}
5240
+ ORDER BY ts_start DESC
5241
+ LIMIT 10`).all(...params);
5242
+ }
5243
+ async function waitForNetworkQuietCondition(sessionId, wait, db) {
5244
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
5245
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
5246
+ const quietMs = resolveDurationMs(wait.quietMs, 500, 10_000);
5247
+ const urlContains = normalizeOptionalString(wait.urlContains);
5248
+ const method = normalizeHttpMethod(wait.method);
5249
+ const tabId = resolveOptionalTabId(wait.tabId);
5250
+ const startedAt = Date.now();
5251
+ const deadline = startedAt + timeoutMs;
5252
+ let attempts = 0;
5253
+ let lastActivityAt = startedAt;
5254
+ let lastCalls = [];
5255
+ while (Date.now() <= deadline) {
5256
+ attempts += 1;
5257
+ const rows = queryRecentNetworkActivity(db, {
5258
+ sessionId,
5259
+ sinceTs: lastActivityAt + 1,
5260
+ urlContains,
5261
+ method,
5262
+ tabId,
5263
+ });
5264
+ if (rows.length > 0) {
5265
+ lastCalls = rows;
5266
+ lastActivityAt = Math.max(...rows.map((row) => row.ts_start), Date.now());
5267
+ }
5268
+ if (Date.now() - lastActivityAt >= quietMs) {
5269
+ return {
5270
+ waitKind: 'network_quiet',
5271
+ matched: true,
5272
+ waitedMs: Date.now() - startedAt,
5273
+ attempts,
5274
+ timeoutMs,
5275
+ pollIntervalMs,
5276
+ evidence: {
5277
+ quietMs,
5278
+ filters: { urlContains, method, tabId },
5279
+ lastActivityAt,
5280
+ sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
5281
+ },
5282
+ };
5283
+ }
5284
+ await sleep(pollIntervalMs);
5285
+ }
5286
+ return {
5287
+ waitKind: 'network_quiet',
5288
+ matched: false,
5289
+ waitedMs: Date.now() - startedAt,
5290
+ attempts,
5291
+ timeoutMs,
5292
+ pollIntervalMs,
5293
+ evidence: {
5294
+ quietMs,
5295
+ filters: { urlContains, method, tabId },
5296
+ lastActivityAt,
5297
+ sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
5298
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
5299
+ waitKind: 'network_quiet',
5300
+ timeoutMs,
5301
+ waitedMs: Date.now() - startedAt,
5302
+ attempts,
5303
+ pollIntervalMs,
5304
+ matcherSummary: { quietMs, urlContains, method, tabId },
5305
+ lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[0], false) : undefined,
5306
+ candidateCount: lastCalls.length,
5307
+ sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
5308
+ }),
5309
+ },
5310
+ error: {
5311
+ code: 'network_quiet_timeout',
5312
+ message: `Timed out waiting for ${quietMs}ms of quiet network activity.`,
5313
+ },
5314
+ };
5315
+ }
5316
+ async function runAutomationWait(options) {
5317
+ switch (options.wait.waitKind) {
5318
+ case 'url':
5319
+ return waitForUrlCondition(options.sessionId, options.wait, options.capturePageState);
5320
+ case 'navigation': {
5321
+ const db = options.getDb?.();
5322
+ if (!db) {
5323
+ throw new Error('navigation waits require database access');
5324
+ }
5325
+ return waitForNavigationCondition(options.sessionId, options.wait, db);
5326
+ }
5327
+ case 'navigation_lifecycle':
5328
+ return waitForNavigationLifecycleCondition(options.sessionId, options.wait, options.captureClient);
5329
+ case 'load_state':
5330
+ return waitForLoadStateCondition(options.sessionId, options.wait, options.capturePageState);
5331
+ case 'selector_state':
5332
+ return waitForSelectorStateCondition(options.sessionId, options.wait, options.captureClient);
5333
+ case 'console':
5334
+ return waitForConsoleCondition(options.sessionId, options.wait, options.captureClient);
5335
+ case 'dialog':
5336
+ return waitForDialogCondition(options.sessionId, options.wait, options.captureClient);
5337
+ case 'stable_layout':
5338
+ return waitForStableLayoutCondition(options.sessionId, options.wait, options.captureClient);
5339
+ case 'download':
5340
+ return waitForDownloadCondition(options.sessionId, options.wait, options.captureClient);
5341
+ case 'popup':
5342
+ return waitForPopupCondition(options.sessionId, options.wait, options.captureClient);
5343
+ case 'network_quiet': {
5344
+ const db = options.getDb?.();
5345
+ if (!db) {
5346
+ throw new Error('network_quiet waits require database access');
5347
+ }
5348
+ return waitForNetworkQuietCondition(options.sessionId, options.wait, db);
5349
+ }
5350
+ case 'request':
5351
+ case 'response': {
5352
+ const db = options.getDb?.();
5353
+ if (!db) {
5354
+ throw new Error(`${options.wait.waitKind} waits require database access`);
5355
+ }
5356
+ return waitForNetworkMatchCondition(options.sessionId, options.wait, db);
5357
+ }
5358
+ }
5359
+ }
5360
+ function getSessionRow(db, sessionId) {
5361
+ return db.prepare(`
5362
+ SELECT
5363
+ session_id,
5364
+ created_at,
5365
+ last_seen_at,
5366
+ paused_at,
5367
+ ended_at,
5368
+ tab_id,
5369
+ window_id,
5370
+ url_start,
5371
+ url_last,
5372
+ user_agent,
5373
+ viewport_w,
5374
+ viewport_h,
5375
+ dpr,
5376
+ safe_mode,
5377
+ pinned
5378
+ FROM sessions
5379
+ WHERE session_id = ?
5380
+ LIMIT 1
5381
+ `).get(sessionId);
5382
+ }
5383
+ function looksSensitiveText(value) {
5384
+ return typeof value === 'string'
5385
+ && /(password|passwd|pwd|token|secret|auth|session|email|card|cvv|cvc|ssn|iban|payment|billing)/i.test(value);
5386
+ }
5387
+ function isSensitivePageInput(input) {
5388
+ const type = typeof input.type === 'string' ? input.type.toLowerCase() : '';
5389
+ return type === 'password'
5390
+ || looksSensitiveText(input.selector)
5391
+ || looksSensitiveText(input.label)
5392
+ || looksSensitiveText(input.name)
5393
+ || looksSensitiveText(input.placeholder)
5394
+ || looksSensitiveText(input.testId);
5395
+ }
5396
+ function collectAutomationPageRisks(payload) {
5397
+ if (!payload) {
5398
+ return {
5399
+ sensitiveInputs: [],
5400
+ frameCount: 0,
5401
+ crossOriginFrameCount: 0,
5402
+ };
5403
+ }
5404
+ const inputs = asRecordArray(payload.inputs);
5405
+ const frames = asRecordArray(payload.frames);
5406
+ const sensitiveInputs = inputs
5407
+ .filter(isSensitivePageInput)
5408
+ .slice(0, 8)
5409
+ .map((input) => ({
5410
+ selector: input.selector,
5411
+ type: input.type,
5412
+ label: input.label,
5413
+ name: input.name,
5414
+ placeholder: input.placeholder,
5415
+ frameId: input.frameId,
5416
+ frameUrl: input.frameUrl,
5417
+ }));
5418
+ const crossOriginFrames = frames
5419
+ .filter((frame) => frame.sameOrigin === false || frame.accessible === false || frame.crossOrigin === true)
5420
+ .slice(0, 8)
5421
+ .map((frame) => ({
5422
+ frameId: frame.frameId,
5423
+ url: frame.url ?? frame.frameUrl,
5424
+ title: frame.title ?? frame.frameTitle,
5425
+ sameOrigin: frame.sameOrigin,
5426
+ accessible: frame.accessible,
5427
+ }));
5428
+ return {
5429
+ sensitiveInputs,
5430
+ sensitiveInputCount: sensitiveInputs.length,
5431
+ frameCount: frames.length,
5432
+ crossOriginFrameCount: crossOriginFrames.length,
5433
+ crossOriginFrames,
5434
+ };
5435
+ }
5436
+ async function buildAutomationFlowPreflight(options) {
5437
+ const blockers = [];
5438
+ const warnings = [];
5439
+ const includePageState = options.input.includePageState !== false;
5440
+ const expectedUrlContains = normalizeOptionalString(options.input.expectedUrlContains);
5441
+ const requireSensitiveAutomation = options.input.requireSensitiveAutomation === true;
5442
+ const plannedActions = Array.isArray(options.input.plannedActions)
5443
+ ? options.input.plannedActions.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
5444
+ : [];
5445
+ const db = options.getDb?.();
5446
+ const session = db ? getSessionRow(db, options.sessionId) : undefined;
5447
+ const sessionState = options.getSessionConnectionState?.(options.sessionId);
5448
+ const hasLiveConnectionLookup = typeof options.getSessionConnectionState === 'function';
5449
+ const scope = classifySessionUrl(session?.url_last ?? undefined);
5450
+ const liveConnection = session
5451
+ ? buildLiveConnectionRecord(session, scope, sessionState)
5452
+ : {
5453
+ connected: sessionState?.connected === true,
5454
+ status: sessionState?.connected === true ? 'connected' : 'unknown',
5455
+ recommendedForLiveCapture: false,
5456
+ };
5457
+ if (!db) {
5458
+ warnings.push({
5459
+ code: 'DB_UNAVAILABLE',
5460
+ severity: 'warning',
5461
+ source: 'server',
5462
+ message: 'Database access was not available; session history checks were skipped.',
5463
+ });
5464
+ }
5465
+ if (!session) {
5466
+ blockers.push({
5467
+ code: 'SESSION_NOT_FOUND',
5468
+ severity: 'error',
5469
+ source: 'session',
5470
+ message: `Session not found: ${options.sessionId}`,
5471
+ });
5472
+ }
5473
+ else {
5474
+ const status = getSessionStatus(session);
5475
+ if (status === 'paused') {
5476
+ blockers.push({
5477
+ code: 'SESSION_PAUSED',
5478
+ severity: 'error',
5479
+ source: 'session',
5480
+ message: 'Resume the session before running an automation flow.',
5481
+ });
5482
+ }
5483
+ if (status === 'ended') {
5484
+ blockers.push({
5485
+ code: 'SESSION_ENDED',
5486
+ severity: 'error',
5487
+ source: 'session',
5488
+ message: 'Start a new session before running an automation flow.',
5489
+ });
5490
+ }
5491
+ if (scope.kind === 'likely_iframe_noise') {
5492
+ blockers.push({
5493
+ code: 'SESSION_SCOPE_NOISE',
5494
+ severity: 'error',
5495
+ source: 'session',
5496
+ message: 'The selected session appears to be bound to iframe/ad traffic rather than the app surface.',
5497
+ });
5498
+ }
5499
+ if (scope.kind === 'top_level_page' && scope.isLocalhost !== true) {
5500
+ warnings.push({
5501
+ code: 'PRODUCTION_OR_REMOTE_ORIGIN',
5502
+ severity: 'warning',
5503
+ source: 'session',
5504
+ message: 'The current session URL is remote/production-like. Keep the flow scoped and avoid destructive actions.',
5505
+ origin: scope.origin,
5506
+ });
5507
+ }
5508
+ if (expectedUrlContains && !String(session.url_last ?? '').includes(expectedUrlContains)) {
5509
+ blockers.push({
5510
+ code: 'EXPECTED_URL_MISMATCH',
5511
+ severity: 'error',
5512
+ source: 'session',
5513
+ message: `Current session URL does not include "${expectedUrlContains}".`,
5514
+ currentUrl: session.url_last,
5515
+ });
5516
+ }
5517
+ }
5518
+ if (hasLiveConnectionLookup && (!sessionState || sessionState.connected !== true)) {
5519
+ blockers.push({
5520
+ code: LIVE_SESSION_DISCONNECTED_CODE,
5521
+ severity: 'error',
5522
+ source: 'connection',
5523
+ message: 'The session is not currently connected to a live extension target.',
5524
+ disconnectedAt: sessionState?.disconnectedAt,
5525
+ disconnectReason: sessionState?.disconnectReason,
5526
+ });
5527
+ }
5528
+ let pageCapture;
5529
+ if (includePageState && blockers.length === 0) {
5530
+ try {
5531
+ pageCapture = await options.capturePageState(options.sessionId, {
5532
+ includeButtons: true,
5533
+ includeLinks: true,
5534
+ includeInputs: true,
5535
+ includeModals: true,
5536
+ maxItems: resolveLimit(options.input.maxItems, 40),
5537
+ maxTextLength: resolveDurationMs(options.input.maxTextLength, 80, 200),
5538
+ });
5539
+ }
5540
+ catch (error) {
5541
+ blockers.push({
5542
+ code: isLiveSessionDisconnectedError(error) ? LIVE_SESSION_DISCONNECTED_CODE : 'PAGE_STATE_CAPTURE_FAILED',
5543
+ severity: 'error',
5544
+ source: 'page-state',
5545
+ message: error instanceof Error ? error.message : String(error),
5546
+ });
5547
+ }
5548
+ }
5549
+ const pageRisks = collectAutomationPageRisks(pageCapture?.payload);
5550
+ const sensitiveInputs = Array.isArray(pageRisks.sensitiveInputs) ? pageRisks.sensitiveInputs : [];
5551
+ const hasInputLikeAction = plannedActions.some((action) => ['input', 'type', 'clear', 'select_option', 'press_key'].includes(action));
5552
+ if (sensitiveInputs.length > 0 && (requireSensitiveAutomation || hasInputLikeAction)) {
5553
+ warnings.push({
5554
+ code: 'SENSITIVE_FIELD_AUTOMATION_RISK',
5555
+ severity: 'warning',
5556
+ source: 'page-state',
5557
+ message: 'Sensitive-looking fields are present. The extension sensitive-field opt-in may be required before input-like actions.',
5558
+ count: sensitiveInputs.length,
5559
+ sampledInputs: sensitiveInputs,
5560
+ });
5561
+ }
5562
+ if (typeof pageRisks.crossOriginFrameCount === 'number' && pageRisks.crossOriginFrameCount > 0) {
5563
+ warnings.push({
5564
+ code: 'CROSS_ORIGIN_FRAME_PRESENT',
5565
+ severity: 'warning',
5566
+ source: 'page-state',
5567
+ message: 'Cross-origin or inaccessible frames are present. Automation inside those frames may be diagnostic-only.',
5568
+ count: pageRisks.crossOriginFrameCount,
5569
+ frames: pageRisks.crossOriginFrames,
5570
+ });
5571
+ }
5572
+ const ready = blockers.length === 0;
5573
+ return {
5574
+ ready,
5575
+ blockers,
5576
+ warnings,
5577
+ checks: {
5578
+ sessionFound: Boolean(session),
5579
+ liveConnected: sessionState?.connected === true || (hasLiveConnectionLookup ? false : undefined),
5580
+ recommendedForLiveCapture: liveConnection.recommendedForLiveCapture,
5581
+ expectedUrlMatched: expectedUrlContains ? blockers.every((blocker) => blocker.code !== 'EXPECTED_URL_MISMATCH') : undefined,
5582
+ pageStateCaptured: pageCapture !== undefined,
5583
+ remoteOrProductionLike: scope.kind === 'top_level_page' && scope.isLocalhost !== true,
5584
+ sensitiveInputCount: sensitiveInputs.length,
5585
+ crossOriginFrameCount: pageRisks.crossOriginFrameCount,
5586
+ },
5587
+ session: session
5588
+ ? {
5589
+ sessionId: session.session_id,
5590
+ status: getSessionStatus(session),
5591
+ tabId: session.tab_id ?? undefined,
5592
+ windowId: session.window_id ?? undefined,
5593
+ urlStart: session.url_start ?? undefined,
5594
+ urlLast: session.url_last ?? undefined,
5595
+ lastSeenAt: resolveSessionLastSeenAt(session, sessionState),
5596
+ safeMode: session.safe_mode === 1,
5597
+ }
5598
+ : undefined,
5599
+ scope,
5600
+ liveConnection,
5601
+ page: pageCapture
5602
+ ? {
5603
+ url: pageCapture.payload.url,
5604
+ title: pageCapture.payload.title,
5605
+ language: pageCapture.payload.language,
5606
+ viewport: pageCapture.payload.viewport,
5607
+ summary: pageCapture.payload.summary,
5608
+ }
5609
+ : undefined,
5610
+ detectedRisks: pageRisks,
5611
+ nextActions: ready
5612
+ ? [{ code: 'RUN_FLOW', message: 'Run the automation flow with bounded waits and failure capture enabled.' }]
5613
+ : blockers.map((blocker) => ({
5614
+ code: String(blocker.code ?? 'FIX_BLOCKER'),
5615
+ message: String(blocker.message ?? 'Resolve this preflight blocker before running the flow.'),
5616
+ })),
3165
5617
  };
3166
5618
  }
3167
5619
  function createWorkflowStepId(step, index) {
@@ -3172,6 +5624,7 @@ async function captureWorkflowPageState(sessionId, capturePageState, mode) {
3172
5624
  const maxTextLength = mode === 'fast' ? 60 : 80;
3173
5625
  return capturePageState(sessionId, {
3174
5626
  includeButtons: true,
5627
+ includeLinks: true,
3175
5628
  includeInputs: true,
3176
5629
  includeModals: true,
3177
5630
  maxItems,
@@ -3226,6 +5679,20 @@ function resolveWorkflowRecommendedAction(error) {
3226
5679
  if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
3227
5680
  return 'inspect_page_state';
3228
5681
  }
5682
+ if (error.code === 'url_wait_timeout' || error.code === 'navigation_wait_timeout') {
5683
+ return 'inspect_navigation_state';
5684
+ }
5685
+ if (error.code === 'selector_state_wait_timeout') {
5686
+ return 'inspect_selector_state';
5687
+ }
5688
+ if (error.code === 'console_wait_timeout') {
5689
+ return 'inspect_live_console_logs';
5690
+ }
5691
+ if (error.code === 'network_quiet_timeout'
5692
+ || error.code === 'request_wait_timeout'
5693
+ || error.code === 'response_wait_timeout') {
5694
+ return 'inspect_network_calls';
5695
+ }
3229
5696
  return undefined;
3230
5697
  }
3231
5698
  function resolveWorkflowFailureSelector(step, stepResultTarget) {
@@ -3970,15 +6437,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
3970
6437
  recommendedAction,
3971
6438
  };
3972
6439
  },
3973
- list_override_profiles: async () => {
6440
+ list_override_profiles: async (input) => {
3974
6441
  const profiles = buildOverrideProfileRecords();
6442
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
3975
6443
  return {
3976
6444
  ...createBaseResponse(),
3977
6445
  limitsApplied: {
3978
6446
  maxResults: profiles.length,
3979
6447
  truncated: false,
3980
6448
  },
3981
- profiles,
6449
+ responseProfile,
6450
+ profiles: profiles.map((profile) => serializeOverrideProfile(profile, responseProfile)),
3982
6451
  nextActions: profiles.length > 0
3983
6452
  ? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
3984
6453
  : [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
@@ -4016,6 +6485,8 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
4016
6485
  });
4017
6486
  const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
4018
6487
  const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
6488
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
6489
+ const includeConfigJson = input.includeConfigJson === true || responseProfile === 'full';
4019
6490
  const write = {
4020
6491
  written: false,
4021
6492
  path: generated.suggestedConfigPath,
@@ -4064,21 +6535,31 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
4064
6535
  warnings: generated.warnings,
4065
6536
  nextActions,
4066
6537
  write,
4067
- profile: generated.profile,
4068
- config: generated.config,
4069
- configJson: generated.configJson,
6538
+ responseProfile,
6539
+ profile: responseProfile === 'full' ? generated.profile : compactOverrideProfile(generated.profile),
6540
+ config: responseProfile === 'full'
6541
+ ? generated.config
6542
+ : {
6543
+ enabled: generated.config.enabled,
6544
+ activeProfileId: generated.config.activeProfileId,
6545
+ profileCount: generated.config.profiles.length,
6546
+ },
6547
+ configJson: includeConfigJson ? generated.configJson : undefined,
6548
+ configJsonOmitted: !includeConfigJson,
4070
6549
  };
4071
6550
  },
4072
6551
  validate_override_profile: async (input) => {
4073
6552
  const profile = resolveOverrideProfileRecord(input.profileId);
4074
6553
  const issues = buildOverrideProfileIssues(profile);
6554
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
4075
6555
  return {
4076
6556
  ...createBaseResponse(),
4077
6557
  profileId: profile.profileId,
4078
6558
  valid: !issues.some((issue) => issue.severity === 'error'),
4079
6559
  issues,
4080
6560
  nextActions: buildOverrideProfileNextActions(profile, issues),
4081
- profile,
6561
+ responseProfile,
6562
+ profile: serializeOverrideProfile(profile, responseProfile),
4082
6563
  };
4083
6564
  },
4084
6565
  preflight_overrides: async (input) => {
@@ -4105,16 +6586,18 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
4105
6586
  }
4106
6587
  const assets = listObservedOverrideAssets(getDb(), {
4107
6588
  sessionId,
4108
- limit: typeof input.limit === 'number' ? input.limit : undefined,
6589
+ limit: typeof input.limit === 'number' ? input.limit : 50,
4109
6590
  sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
4110
6591
  });
6592
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
4111
6593
  return {
4112
6594
  ...createBaseResponse(sessionId),
4113
6595
  limitsApplied: {
4114
6596
  maxResults: assets.length,
4115
6597
  truncated: false,
4116
6598
  },
4117
- assets,
6599
+ responseProfile,
6600
+ assets: responseProfile === 'full' ? assets : assets.map(compactObservedOverrideAsset),
4118
6601
  };
4119
6602
  },
4120
6603
  plan_override_response_patch: async (input) => {
@@ -4356,6 +6839,373 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
4356
6839
  : [{ code: 'NO_DIAGNOSIS_ISSUES', message: 'No diagnosis issues were found for the selected override run.' }],
4357
6840
  };
4358
6841
  },
6842
+ discover_ssr_mockability: async (input) => {
6843
+ const db = getDb();
6844
+ const projectRoot = normalizeOptionalString(input.projectRoot);
6845
+ if (!projectRoot) {
6846
+ throw new Error('projectRoot is required');
6847
+ }
6848
+ const discovery = discoverSsrMockability({
6849
+ projectRoot,
6850
+ targetUrl: normalizeOptionalString(input.targetUrl),
6851
+ apiHost: normalizeOptionalString(input.apiHost),
6852
+ maxFiles: typeof input.maxFiles === 'number' ? input.maxFiles : undefined,
6853
+ });
6854
+ const auditRecord = createSsrMockAuditRecord({
6855
+ action: 'discover',
6856
+ status: discovery.mockable ? 'succeeded' : 'not_mockable',
6857
+ projectRoot: discovery.projectRoot,
6858
+ targetUrl: discovery.targetUrl,
6859
+ apiHost: discovery.apiHost,
6860
+ envVarName: discovery.preferredEnvVarName,
6861
+ envFilePath: discovery.preferredEnvFilePath,
6862
+ summary: {
6863
+ classification: discovery.classification,
6864
+ scannedFileCount: discovery.scannedFileCount,
6865
+ candidateCount: discovery.candidates.length,
6866
+ },
6867
+ result: discovery,
6868
+ });
6869
+ insertSsrMockAudit(db, auditRecord);
6870
+ return {
6871
+ ...createBaseResponse(),
6872
+ limitsApplied: {
6873
+ maxResults: discovery.candidates.length,
6874
+ truncated: false,
6875
+ },
6876
+ audit: auditRecord,
6877
+ ...discovery,
6878
+ };
6879
+ },
6880
+ apply_ssr_mock_config: async (input) => {
6881
+ const db = getDb();
6882
+ const projectRoot = normalizeOptionalString(input.projectRoot);
6883
+ if (!projectRoot) {
6884
+ throw new Error('projectRoot is required');
6885
+ }
6886
+ const envVarName = normalizeOptionalString(input.envVarName);
6887
+ if (!envVarName) {
6888
+ throw new Error('envVarName is required');
6889
+ }
6890
+ const mockBaseUrl = normalizeOptionalString(input.mockBaseUrl);
6891
+ if (!mockBaseUrl) {
6892
+ throw new Error('mockBaseUrl is required');
6893
+ }
6894
+ const applied = applySsrMockConfig({
6895
+ projectRoot,
6896
+ envVarName,
6897
+ mockBaseUrl,
6898
+ envFilePath: normalizeOptionalString(input.envFilePath),
6899
+ rollbackId: normalizeOptionalString(input.rollbackId),
6900
+ });
6901
+ const auditRecord = createSsrMockAuditRecord({
6902
+ action: 'apply-config',
6903
+ status: applied.changed ? 'succeeded' : 'no_change',
6904
+ projectRoot,
6905
+ envVarName,
6906
+ envFilePath: applied.envFilePath,
6907
+ mockBaseUrl: applied.mockBaseUrl,
6908
+ rollbackId: applied.rollbackId,
6909
+ summary: {
6910
+ mode: applied.mode,
6911
+ createdFile: applied.createdFile,
6912
+ changed: applied.changed,
6913
+ },
6914
+ result: applied,
6915
+ });
6916
+ insertSsrMockAudit(db, auditRecord);
6917
+ return {
6918
+ ...createBaseResponse(),
6919
+ limitsApplied: {
6920
+ maxResults: 1,
6921
+ truncated: false,
6922
+ },
6923
+ audit: auditRecord,
6924
+ ...applied,
6925
+ nextActions: [
6926
+ { code: 'RESTART_APP_SERVER', message: 'Restart the SSR app server so the env change takes effect.' },
6927
+ { code: 'REMOVE_SSR_MOCK_CONFIG', message: 'Run remove_ssr_mock_config when the mock session is finished.' },
6928
+ ],
6929
+ };
6930
+ },
6931
+ remove_ssr_mock_config: async (input) => {
6932
+ const db = getDb();
6933
+ const envFilePath = normalizeOptionalString(input.envFilePath);
6934
+ if (!envFilePath) {
6935
+ throw new Error('envFilePath is required');
6936
+ }
6937
+ const envVarName = normalizeOptionalString(input.envVarName);
6938
+ if (!envVarName) {
6939
+ throw new Error('envVarName is required');
6940
+ }
6941
+ const removed = removeSsrMockConfig({
6942
+ envFilePath,
6943
+ envVarName,
6944
+ rollbackId: normalizeOptionalString(input.rollbackId),
6945
+ });
6946
+ const auditRecord = createSsrMockAuditRecord({
6947
+ action: 'remove-config',
6948
+ status: removed.restored ? 'succeeded' : 'not_found',
6949
+ projectRoot: envFilePath,
6950
+ envVarName,
6951
+ envFilePath: removed.envFilePath,
6952
+ rollbackId: removed.rollbackId,
6953
+ summary: {
6954
+ mode: removed.mode,
6955
+ restored: removed.restored,
6956
+ },
6957
+ result: removed,
6958
+ });
6959
+ insertSsrMockAudit(db, auditRecord);
6960
+ return {
6961
+ ...createBaseResponse(),
6962
+ limitsApplied: {
6963
+ maxResults: 1,
6964
+ truncated: false,
6965
+ },
6966
+ audit: auditRecord,
6967
+ ...removed,
6968
+ nextActions: removed.restored
6969
+ ? [{ code: 'RESTART_APP_SERVER', message: 'Restart the SSR app server so the restored env value takes effect.' }]
6970
+ : [{ code: 'INSPECT_ENV_FILE', message: 'No managed SSR mock patch was found for this env var.' }],
6971
+ };
6972
+ },
6973
+ get_ssr_mock_audit_log: async (input) => {
6974
+ const db = getDb();
6975
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
6976
+ const offset = resolveOffset(input.offset);
6977
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
6978
+ const projectRoot = normalizeOptionalString(input.projectRoot);
6979
+ const rollbackId = normalizeOptionalString(input.rollbackId);
6980
+ const envVarName = normalizeOptionalString(input.envVarName);
6981
+ const result = listSsrMockAudits(db, {
6982
+ projectRoot,
6983
+ rollbackId,
6984
+ envVarName,
6985
+ limit,
6986
+ offset,
6987
+ });
6988
+ const bytePage = applyByteBudget(result.audits, maxResponseBytes);
6989
+ const truncated = result.hasMore || bytePage.truncatedByBytes;
6990
+ return {
6991
+ ...createBaseResponse(),
6992
+ limitsApplied: {
6993
+ maxResults: limit,
6994
+ truncated,
6995
+ },
6996
+ projectRoot: projectRoot ?? null,
6997
+ rollbackId: rollbackId ?? null,
6998
+ envVarName: envVarName ?? null,
6999
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
7000
+ responseBytes: bytePage.responseBytes,
7001
+ audits: bytePage.items,
7002
+ nextActions: bytePage.items.length === 0
7003
+ ? [{ code: 'DISCOVER_SSR_MOCKABILITY', message: 'Run discover_ssr_mockability or apply_ssr_mock_config to create SSR mock audit rows.' }]
7004
+ : [{ code: 'REVIEW_SSR_MOCK_CLEANUP', message: 'Review the latest audit rows to confirm rollback ids and env file cleanup state.' }],
7005
+ };
7006
+ },
7007
+ create_mock_route: async (input) => {
7008
+ const db = getDb();
7009
+ const record = createMockRouteRecord(input);
7010
+ upsertMockRoute(db, record);
7011
+ const ssrScope = record.sessionScope ?? record.routeId;
7012
+ const ssrMockBasePath = record.mode === 'ssr' || record.mode === 'both'
7013
+ ? `/mock/ssr/${encodeURIComponent(ssrScope)}`
7014
+ : undefined;
7015
+ return {
7016
+ ...createBaseResponse(),
7017
+ limitsApplied: {
7018
+ maxResults: 1,
7019
+ truncated: false,
7020
+ },
7021
+ route: record,
7022
+ ssrMockBasePath,
7023
+ nextActions: [record.enabled
7024
+ ? {
7025
+ code: record.mode === 'browser' ? 'ENABLE_BROWSER_MOCKS' : 'APPLY_SSR_MOCK_CONFIG',
7026
+ message: record.mode === 'browser'
7027
+ ? 'Enable browser overrides so matching requests are fulfilled.'
7028
+ : record.mode === 'ssr'
7029
+ ? `Point the discovered SSR env var at ${ssrMockBasePath ?? '/mock/ssr/<scope>'}.`
7030
+ : `Use browser overrides or point the discovered SSR env var at ${ssrMockBasePath ?? '/mock/ssr/<scope>'}.`,
7031
+ }
7032
+ : {
7033
+ code: 'ENABLE_MOCK_ROUTE',
7034
+ message: 'Set enabled=true when the route should start affecting browser or SSR traffic.',
7035
+ }],
7036
+ };
7037
+ },
7038
+ update_mock_route: async (input) => {
7039
+ const db = getDb();
7040
+ const routeId = normalizeOptionalString(input.routeId);
7041
+ if (!routeId) {
7042
+ throw new Error('routeId is required');
7043
+ }
7044
+ const existing = getMockRoute(db, routeId);
7045
+ if (!existing) {
7046
+ throw new Error(`Unknown mock route: ${routeId}`);
7047
+ }
7048
+ const record = createMockRouteRecord(input, existing);
7049
+ upsertMockRoute(db, record);
7050
+ return {
7051
+ ...createBaseResponse(),
7052
+ limitsApplied: {
7053
+ maxResults: 1,
7054
+ truncated: false,
7055
+ },
7056
+ route: record,
7057
+ nextActions: [{ code: 'REVIEW_MOCK_STATUS', message: 'Review route mode, target URL, and body payload before running the mock.' }],
7058
+ };
7059
+ },
7060
+ delete_mock_route: async (input) => {
7061
+ const db = getDb();
7062
+ const routeId = normalizeOptionalString(input.routeId);
7063
+ if (!routeId) {
7064
+ throw new Error('routeId is required');
7065
+ }
7066
+ const deleted = deleteMockRoute(db, routeId);
7067
+ return {
7068
+ ...createBaseResponse(),
7069
+ limitsApplied: {
7070
+ maxResults: 1,
7071
+ truncated: false,
7072
+ },
7073
+ routeId,
7074
+ deleted,
7075
+ nextActions: deleted
7076
+ ? [{ code: 'CONFIRM_CLEANUP', message: 'If a runtime was using this route, confirm no active mock run still references it.' }]
7077
+ : [{ code: 'LIST_MOCK_ROUTES', message: 'The route was not found; list persisted mock routes to confirm the intended routeId.' }],
7078
+ };
7079
+ },
7080
+ list_mock_routes: async (input) => {
7081
+ const db = getDb();
7082
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
7083
+ const offset = resolveOffset(input.offset);
7084
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
7085
+ const result = listMockRoutes(db, {
7086
+ projectRoot: normalizeOptionalString(input.projectRoot),
7087
+ mode: normalizeOptionalString(input.mode) === 'ssr' || normalizeOptionalString(input.mode) === 'both'
7088
+ ? normalizeOptionalString(input.mode)
7089
+ : normalizeOptionalString(input.mode) === 'browser'
7090
+ ? 'browser'
7091
+ : undefined,
7092
+ enabled: typeof input.enabled === 'boolean' ? input.enabled : undefined,
7093
+ limit,
7094
+ offset,
7095
+ });
7096
+ const bytePage = applyByteBudget(result.routes, maxResponseBytes);
7097
+ const truncated = result.hasMore || bytePage.truncatedByBytes;
7098
+ return {
7099
+ ...createBaseResponse(),
7100
+ limitsApplied: {
7101
+ maxResults: limit,
7102
+ truncated,
7103
+ },
7104
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
7105
+ responseBytes: bytePage.responseBytes,
7106
+ routes: bytePage.items,
7107
+ nextActions: bytePage.items.length === 0
7108
+ ? [{ code: 'CREATE_MOCK_ROUTE', message: 'Create a persisted mock route before enabling browser or SSR mocking.' }]
7109
+ : [{ code: 'GET_MOCK_STATUS', message: 'Inspect mock status and recent run or hit records for one route.' }],
7110
+ };
7111
+ },
7112
+ get_mock_route: async (input) => {
7113
+ const db = getDb();
7114
+ const routeId = normalizeOptionalString(input.routeId);
7115
+ if (!routeId) {
7116
+ throw new Error('routeId is required');
7117
+ }
7118
+ const route = getMockRoute(db, routeId);
7119
+ if (!route) {
7120
+ throw new Error(`Unknown mock route: ${routeId}`);
7121
+ }
7122
+ return {
7123
+ ...createBaseResponse(),
7124
+ limitsApplied: {
7125
+ maxResults: 1,
7126
+ truncated: false,
7127
+ },
7128
+ route,
7129
+ nextActions: [{ code: 'GET_MOCK_STATUS', message: 'Inspect latest runs and hits before enabling or updating this route.' }],
7130
+ };
7131
+ },
7132
+ get_mock_run_log: async (input) => {
7133
+ const db = getDb();
7134
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
7135
+ const offset = resolveOffset(input.offset);
7136
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
7137
+ const result = listMockRuns(db, {
7138
+ routeId: normalizeOptionalString(input.routeId),
7139
+ sessionId: normalizeOptionalString(input.sessionId),
7140
+ limit,
7141
+ offset,
7142
+ });
7143
+ const bytePage = applyByteBudget(result.runs, maxResponseBytes);
7144
+ const truncated = result.hasMore || bytePage.truncatedByBytes;
7145
+ return {
7146
+ ...createBaseResponse(),
7147
+ limitsApplied: {
7148
+ maxResults: limit,
7149
+ truncated,
7150
+ },
7151
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
7152
+ responseBytes: bytePage.responseBytes,
7153
+ runs: bytePage.items,
7154
+ nextActions: bytePage.items.length === 0
7155
+ ? [{ code: 'ENABLE_BROWSER_MOCKS', message: 'No mock runs exist yet; wire a runtime flow that can create execution records.' }]
7156
+ : [{ code: 'GET_MOCK_HIT_LOG', message: 'Inspect matching hit records for the selected run or route.' }],
7157
+ };
7158
+ },
7159
+ get_mock_hit_log: async (input) => {
7160
+ const db = getDb();
7161
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
7162
+ const offset = resolveOffset(input.offset);
7163
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
7164
+ const result = listMockHits(db, {
7165
+ routeId: normalizeOptionalString(input.routeId),
7166
+ runId: normalizeOptionalString(input.runId),
7167
+ limit,
7168
+ offset,
7169
+ });
7170
+ const bytePage = applyByteBudget(result.hits, maxResponseBytes);
7171
+ const truncated = result.hasMore || bytePage.truncatedByBytes;
7172
+ return {
7173
+ ...createBaseResponse(),
7174
+ limitsApplied: {
7175
+ maxResults: limit,
7176
+ truncated,
7177
+ },
7178
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
7179
+ responseBytes: bytePage.responseBytes,
7180
+ hits: bytePage.items,
7181
+ nextActions: bytePage.items.length === 0
7182
+ ? [{ code: 'RUN_MOCK_ROUTE', message: 'No hit records exist yet; execute the route through a browser or SSR runtime.' }]
7183
+ : [{ code: 'DIAGNOSE_MOCK_ROUTE', message: 'Use hit results to verify whether the route matched and fulfilled as expected.' }],
7184
+ };
7185
+ },
7186
+ get_mock_status: async (input) => {
7187
+ const db = getDb();
7188
+ const routeId = normalizeOptionalString(input.routeId);
7189
+ const projectRoot = normalizeOptionalString(input.projectRoot);
7190
+ const route = routeId ? getMockRoute(db, routeId) : null;
7191
+ const recentRoutes = listMockRoutes(db, { projectRoot, limit: 5, offset: 0 }).routes;
7192
+ const recentRuns = listMockRuns(db, { routeId, limit: 5, offset: 0 }).runs;
7193
+ const recentHits = listMockHits(db, { routeId, limit: 5, offset: 0 }).hits;
7194
+ return {
7195
+ ...createBaseResponse(),
7196
+ limitsApplied: {
7197
+ maxResults: 5,
7198
+ truncated: false,
7199
+ },
7200
+ route,
7201
+ recentRoutes,
7202
+ recentRuns,
7203
+ recentHits,
7204
+ nextActions: route
7205
+ ? [{ code: 'EXECUTE_MOCK_ROUTE', message: 'Bind the persisted route to browser or SSR execution so runs and hits begin to accumulate.' }]
7206
+ : [{ code: 'CREATE_MOCK_ROUTE', message: 'Persist a mock route first, then inspect status again.' }],
7207
+ };
7208
+ },
4359
7209
  get_recent_events: async (input) => {
4360
7210
  const db = getDb();
4361
7211
  const sessionId = getSessionId(input);
@@ -5526,6 +8376,113 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5526
8376
  chunkBase64: encoding === 'base64' ? chunkBuffer.toString('base64') : undefined,
5527
8377
  };
5528
8378
  },
8379
+ run_lighthouse_report: async (input) => {
8380
+ const db = getDb();
8381
+ const sessionId = getSessionId(input);
8382
+ const report = await runLighthouseReport(db, {
8383
+ sessionId,
8384
+ url: typeof input.url === 'string' ? input.url : undefined,
8385
+ formFactor: input.formFactor === 'desktop' ? 'desktop' : 'mobile',
8386
+ categories: Array.isArray(input.categories)
8387
+ ? input.categories.filter((entry) => typeof entry === 'string')
8388
+ : undefined,
8389
+ maxWaitForLoadMs: typeof input.maxWaitForLoadMs === 'number' ? input.maxWaitForLoadMs : undefined,
8390
+ chromeFlags: Array.isArray(input.chromeFlags)
8391
+ ? input.chromeFlags.filter((entry) => typeof entry === 'string')
8392
+ : undefined,
8393
+ });
8394
+ return {
8395
+ ...createBaseResponse(sessionId),
8396
+ limitsApplied: {
8397
+ maxResults: 1,
8398
+ truncated: false,
8399
+ },
8400
+ report,
8401
+ };
8402
+ },
8403
+ list_lighthouse_reports: async (input) => {
8404
+ const db = getDb();
8405
+ const result = listLighthouseReports(db, {
8406
+ sessionId: getSessionId(input),
8407
+ urlContains: typeof input.urlContains === 'string' ? input.urlContains : undefined,
8408
+ status: typeof input.status === 'string' ? input.status : undefined,
8409
+ limit: typeof input.limit === 'number' ? input.limit : undefined,
8410
+ offset: typeof input.offset === 'number' ? input.offset : undefined,
8411
+ });
8412
+ return {
8413
+ ...createBaseResponse(getSessionId(input)),
8414
+ limitsApplied: {
8415
+ maxResults: result.pagination.limit,
8416
+ truncated: result.pagination.hasMore,
8417
+ },
8418
+ pagination: result.pagination,
8419
+ reports: result.reports,
8420
+ };
8421
+ },
8422
+ get_lighthouse_report: async (input) => {
8423
+ const db = getDb();
8424
+ const reportId = typeof input.reportId === 'string' ? input.reportId : '';
8425
+ if (!reportId) {
8426
+ throw new Error('reportId is required');
8427
+ }
8428
+ const report = getLighthouseReport(db, reportId);
8429
+ return {
8430
+ ...createBaseResponse(report.sessionId),
8431
+ limitsApplied: {
8432
+ maxResults: 1,
8433
+ truncated: false,
8434
+ },
8435
+ report,
8436
+ };
8437
+ },
8438
+ get_lighthouse_report_asset: async (input) => {
8439
+ const db = getDb();
8440
+ const reportId = typeof input.reportId === 'string' ? input.reportId : '';
8441
+ if (!reportId) {
8442
+ throw new Error('reportId is required');
8443
+ }
8444
+ const asset = normalizeLighthouseAsset(input.asset);
8445
+ const chunk = getLighthouseReportAsset(db, {
8446
+ reportId,
8447
+ asset,
8448
+ offset: typeof input.offset === 'number' ? input.offset : undefined,
8449
+ maxBytes: typeof input.maxBytes === 'number' ? input.maxBytes : undefined,
8450
+ encoding: input.encoding === 'raw' ? 'raw' : 'base64',
8451
+ });
8452
+ return {
8453
+ ...createBaseResponse(),
8454
+ limitsApplied: {
8455
+ maxResults: typeof chunk.bytesReturned === 'number' ? chunk.bytesReturned : 0,
8456
+ truncated: chunk.hasMore === true,
8457
+ },
8458
+ ...chunk,
8459
+ };
8460
+ },
8461
+ plan_lighthouse_fixes: async (input) => {
8462
+ const db = getDb();
8463
+ const reportId = typeof input.reportId === 'string' ? input.reportId : '';
8464
+ if (!reportId) {
8465
+ throw new Error('reportId is required');
8466
+ }
8467
+ const plan = planLighthouseFixes(db, {
8468
+ reportId,
8469
+ minPriority: input.minPriority === 'critical' || input.minPriority === 'high' || input.minPriority === 'medium' || input.minPriority === 'low'
8470
+ ? input.minPriority
8471
+ : undefined,
8472
+ limit: typeof input.limit === 'number' ? input.limit : undefined,
8473
+ projectRoot: typeof input.projectRoot === 'string' ? input.projectRoot : undefined,
8474
+ routePath: typeof input.routePath === 'string' ? input.routePath : undefined,
8475
+ sourceCandidateLimit: typeof input.sourceCandidateLimit === 'number' ? input.sourceCandidateLimit : undefined,
8476
+ });
8477
+ return {
8478
+ ...createBaseResponse(plan.sessionId),
8479
+ limitsApplied: {
8480
+ maxResults: plan.itemCount,
8481
+ truncated: false,
8482
+ },
8483
+ plan,
8484
+ };
8485
+ },
5529
8486
  list_automation_runs: async (input) => {
5530
8487
  const db = getDb();
5531
8488
  const sessionId = getSessionId(input);
@@ -5562,9 +8519,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5562
8519
  r.status,
5563
8520
  r.started_at,
5564
8521
  r.completed_at,
5565
- r.stop_reason,
5566
- r.target_summary_json,
5567
- r.failure_json,
8522
+ r.stop_reason,
8523
+ r.target_summary_json,
8524
+ r.diagnostics_json,
8525
+ r.failure_json,
5568
8526
  r.redaction_json,
5569
8527
  r.created_at,
5570
8528
  r.updated_at,
@@ -5626,9 +8584,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5626
8584
  r.status,
5627
8585
  r.started_at,
5628
8586
  r.completed_at,
5629
- r.stop_reason,
5630
- r.target_summary_json,
5631
- r.failure_json,
8587
+ r.stop_reason,
8588
+ r.target_summary_json,
8589
+ r.diagnostics_json,
8590
+ r.failure_json,
5632
8591
  r.redaction_json,
5633
8592
  r.created_at,
5634
8593
  r.updated_at,
@@ -5660,9 +8619,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5660
8619
  started_at,
5661
8620
  finished_at,
5662
8621
  duration_ms,
5663
- tab_id,
5664
- target_summary_json,
5665
- redaction_json,
8622
+ tab_id,
8623
+ target_summary_json,
8624
+ diagnostics_json,
8625
+ redaction_json,
5666
8626
  failure_json,
5667
8627
  input_metadata_json,
5668
8628
  event_type,
@@ -5696,12 +8656,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5696
8656
  const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
5697
8657
  const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
5698
8658
  const includeButtons = input.includeButtons !== false;
8659
+ const includeLinks = input.includeLinks !== false;
5699
8660
  const includeInputs = input.includeInputs !== false;
5700
8661
  const includeModals = input.includeModals !== false;
5701
8662
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
5702
8663
  maxItems,
5703
8664
  maxTextLength,
5704
8665
  includeButtons,
8666
+ includeLinks,
5705
8667
  includeInputs,
5706
8668
  includeModals,
5707
8669
  }, 4_000);
@@ -6376,7 +9338,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6376
9338
  throw new Error('selector is required');
6377
9339
  }
6378
9340
  const properties = asStringArray(input.properties, 64);
6379
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, properties }, 3_000);
9341
+ const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
9342
+ ? Math.max(0, Math.floor(input.frameId))
9343
+ : 0;
9344
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties }, 3_000);
6380
9345
  return {
6381
9346
  ...createBaseResponse(sessionId),
6382
9347
  limitsApplied: {
@@ -6392,7 +9357,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6392
9357
  throw new Error('sessionId is required');
6393
9358
  }
6394
9359
  const selector = typeof input.selector === 'string' ? input.selector : undefined;
6395
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector }, 3_000);
9360
+ const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
9361
+ ? Math.max(0, Math.floor(input.frameId))
9362
+ : 0;
9363
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000);
6396
9364
  return {
6397
9365
  ...createBaseResponse(sessionId),
6398
9366
  limitsApplied: {
@@ -6423,6 +9391,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6423
9391
  const normalizedInput = {
6424
9392
  ...input,
6425
9393
  includeButtons: kinds.includes('buttons'),
9394
+ includeLinks: kinds.includes('links'),
6426
9395
  includeInputs: kinds.includes('inputs'),
6427
9396
  includeModals: kinds.includes('modals'),
6428
9397
  };
@@ -6503,6 +9472,247 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6503
9472
  ...waited,
6504
9473
  };
6505
9474
  },
9475
+ preflight_automation_flow: async (input) => {
9476
+ const sessionId = getSessionId(input);
9477
+ if (!sessionId) {
9478
+ throw new Error('sessionId is required');
9479
+ }
9480
+ const preflight = await buildAutomationFlowPreflight({
9481
+ sessionId,
9482
+ input,
9483
+ capturePageState,
9484
+ getDb,
9485
+ getSessionConnectionState,
9486
+ });
9487
+ return {
9488
+ ...createBaseResponse(sessionId),
9489
+ limitsApplied: {
9490
+ maxResults: 1,
9491
+ truncated: false,
9492
+ },
9493
+ ...preflight,
9494
+ };
9495
+ },
9496
+ wait_for_url: async (input) => {
9497
+ const sessionId = getSessionId(input);
9498
+ if (!sessionId) {
9499
+ throw new Error('sessionId is required');
9500
+ }
9501
+ const wait = AutomationWaitUrlSchema.parse({ ...input, waitKind: 'url' });
9502
+ const waited = await waitForUrlCondition(sessionId, wait, capturePageState);
9503
+ return {
9504
+ ...createBaseResponse(sessionId),
9505
+ limitsApplied: {
9506
+ maxResults: 1,
9507
+ truncated: false,
9508
+ },
9509
+ ...waited,
9510
+ };
9511
+ },
9512
+ wait_for_navigation: async (input) => {
9513
+ const sessionId = getSessionId(input);
9514
+ if (!sessionId) {
9515
+ throw new Error('sessionId is required');
9516
+ }
9517
+ if (!getDb) {
9518
+ throw new Error('wait_for_navigation requires database access');
9519
+ }
9520
+ const wait = AutomationWaitNavigationSchema.parse({ ...input, waitKind: 'navigation' });
9521
+ const waited = await waitForNavigationCondition(sessionId, wait, getDb());
9522
+ return {
9523
+ ...createBaseResponse(sessionId),
9524
+ limitsApplied: {
9525
+ maxResults: 10,
9526
+ truncated: false,
9527
+ },
9528
+ ...waited,
9529
+ };
9530
+ },
9531
+ wait_for_navigation_lifecycle: async (input) => {
9532
+ const sessionId = getSessionId(input);
9533
+ if (!sessionId) {
9534
+ throw new Error('sessionId is required');
9535
+ }
9536
+ const wait = AutomationWaitNavigationLifecycleSchema.parse({ ...input, waitKind: 'navigation_lifecycle' });
9537
+ const waited = await waitForNavigationLifecycleCondition(sessionId, wait, captureClient);
9538
+ return {
9539
+ ...createBaseResponse(sessionId),
9540
+ limitsApplied: {
9541
+ maxResults: 1,
9542
+ truncated: false,
9543
+ },
9544
+ ...waited,
9545
+ };
9546
+ },
9547
+ wait_for_load_state: async (input) => {
9548
+ const sessionId = getSessionId(input);
9549
+ if (!sessionId) {
9550
+ throw new Error('sessionId is required');
9551
+ }
9552
+ const wait = AutomationWaitLoadStateSchema.parse({ ...input, waitKind: 'load_state' });
9553
+ const waited = await waitForLoadStateCondition(sessionId, wait, capturePageState);
9554
+ return {
9555
+ ...createBaseResponse(sessionId),
9556
+ limitsApplied: {
9557
+ maxResults: 1,
9558
+ truncated: false,
9559
+ },
9560
+ ...waited,
9561
+ };
9562
+ },
9563
+ wait_for_selector_state: async (input) => {
9564
+ const sessionId = getSessionId(input);
9565
+ if (!sessionId) {
9566
+ throw new Error('sessionId is required');
9567
+ }
9568
+ const wait = AutomationWaitSelectorStateSchema.parse({ ...input, waitKind: 'selector_state' });
9569
+ const waited = await waitForSelectorStateCondition(sessionId, wait, captureClient);
9570
+ return {
9571
+ ...createBaseResponse(sessionId),
9572
+ limitsApplied: {
9573
+ maxResults: 1,
9574
+ truncated: false,
9575
+ },
9576
+ ...waited,
9577
+ };
9578
+ },
9579
+ wait_for_request: async (input) => {
9580
+ const sessionId = getSessionId(input);
9581
+ if (!sessionId) {
9582
+ throw new Error('sessionId is required');
9583
+ }
9584
+ if (!getDb) {
9585
+ throw new Error('wait_for_request requires database access');
9586
+ }
9587
+ const wait = AutomationWaitRequestSchema.parse({ ...input, waitKind: 'request' });
9588
+ const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
9589
+ return {
9590
+ ...createBaseResponse(sessionId),
9591
+ limitsApplied: {
9592
+ maxResults: 10,
9593
+ truncated: false,
9594
+ },
9595
+ ...waited,
9596
+ };
9597
+ },
9598
+ wait_for_response: async (input) => {
9599
+ const sessionId = getSessionId(input);
9600
+ if (!sessionId) {
9601
+ throw new Error('sessionId is required');
9602
+ }
9603
+ if (!getDb) {
9604
+ throw new Error('wait_for_response requires database access');
9605
+ }
9606
+ const wait = AutomationWaitResponseSchema.parse({ ...input, waitKind: 'response' });
9607
+ const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
9608
+ return {
9609
+ ...createBaseResponse(sessionId),
9610
+ limitsApplied: {
9611
+ maxResults: 10,
9612
+ truncated: false,
9613
+ },
9614
+ ...waited,
9615
+ };
9616
+ },
9617
+ wait_for_console: async (input) => {
9618
+ const sessionId = getSessionId(input);
9619
+ if (!sessionId) {
9620
+ throw new Error('sessionId is required');
9621
+ }
9622
+ const wait = AutomationWaitConsoleSchema.parse({ ...input, waitKind: 'console' });
9623
+ const waited = await waitForConsoleCondition(sessionId, wait, captureClient);
9624
+ return {
9625
+ ...createBaseResponse(sessionId),
9626
+ limitsApplied: {
9627
+ maxResults: 10,
9628
+ truncated: false,
9629
+ },
9630
+ ...waited,
9631
+ };
9632
+ },
9633
+ wait_for_dialog: async (input) => {
9634
+ const sessionId = getSessionId(input);
9635
+ if (!sessionId) {
9636
+ throw new Error('sessionId is required');
9637
+ }
9638
+ const wait = AutomationWaitDialogSchema.parse({ ...input, waitKind: 'dialog' });
9639
+ const waited = await waitForDialogCondition(sessionId, wait, captureClient);
9640
+ return {
9641
+ ...createBaseResponse(sessionId),
9642
+ limitsApplied: {
9643
+ maxResults: 1,
9644
+ truncated: false,
9645
+ },
9646
+ ...waited,
9647
+ };
9648
+ },
9649
+ wait_for_stable_layout: async (input) => {
9650
+ const sessionId = getSessionId(input);
9651
+ if (!sessionId) {
9652
+ throw new Error('sessionId is required');
9653
+ }
9654
+ const wait = AutomationWaitStableLayoutSchema.parse({ ...input, waitKind: 'stable_layout' });
9655
+ const waited = await waitForStableLayoutCondition(sessionId, wait, captureClient);
9656
+ return {
9657
+ ...createBaseResponse(sessionId),
9658
+ limitsApplied: {
9659
+ maxResults: 1,
9660
+ truncated: false,
9661
+ },
9662
+ ...waited,
9663
+ };
9664
+ },
9665
+ wait_for_download: async (input) => {
9666
+ const sessionId = getSessionId(input);
9667
+ if (!sessionId) {
9668
+ throw new Error('sessionId is required');
9669
+ }
9670
+ const wait = AutomationWaitDownloadSchema.parse({ ...input, waitKind: 'download' });
9671
+ const waited = await waitForDownloadCondition(sessionId, wait, captureClient);
9672
+ return {
9673
+ ...createBaseResponse(sessionId),
9674
+ limitsApplied: {
9675
+ maxResults: 1,
9676
+ truncated: false,
9677
+ },
9678
+ ...waited,
9679
+ };
9680
+ },
9681
+ wait_for_popup: async (input) => {
9682
+ const sessionId = getSessionId(input);
9683
+ if (!sessionId) {
9684
+ throw new Error('sessionId is required');
9685
+ }
9686
+ const wait = AutomationWaitPopupSchema.parse({ ...input, waitKind: 'popup' });
9687
+ const waited = await waitForPopupCondition(sessionId, wait, captureClient);
9688
+ return {
9689
+ ...createBaseResponse(sessionId),
9690
+ limitsApplied: {
9691
+ maxResults: 1,
9692
+ truncated: false,
9693
+ },
9694
+ ...waited,
9695
+ };
9696
+ },
9697
+ wait_for_network_quiet: async (input) => {
9698
+ const sessionId = getSessionId(input);
9699
+ if (!sessionId) {
9700
+ throw new Error('sessionId is required');
9701
+ }
9702
+ if (!getDb) {
9703
+ throw new Error('wait_for_network_quiet requires database access');
9704
+ }
9705
+ const wait = AutomationWaitNetworkQuietSchema.parse({ ...input, waitKind: 'network_quiet' });
9706
+ const waited = await waitForNetworkQuietCondition(sessionId, wait, getDb());
9707
+ return {
9708
+ ...createBaseResponse(sessionId),
9709
+ limitsApplied: {
9710
+ maxResults: 10,
9711
+ truncated: false,
9712
+ },
9713
+ ...waited,
9714
+ };
9715
+ },
6506
9716
  run_ui_steps: async (input) => {
6507
9717
  const request = RunUIStepsSchema.parse(input);
6508
9718
  const workflowTraceId = createUIWorkflowTraceId();
@@ -6530,7 +9740,16 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6530
9740
  const previousCapture = lastPageCapture;
6531
9741
  try {
6532
9742
  if (step.kind === 'action') {
6533
- const resolvedTarget = await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
9743
+ const resolvedTarget = step.target?.locator
9744
+ ? {
9745
+ target: step.target,
9746
+ resolution: {
9747
+ strategy: 'native_locator_pending',
9748
+ matcher: summarizeWorkflowTargetMatcher(step.target),
9749
+ },
9750
+ pageCapture: undefined,
9751
+ }
9752
+ : await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
6534
9753
  const liveRequest = LiveUIActionRequestSchema.parse({
6535
9754
  action: step.action,
6536
9755
  target: resolvedTarget.target,
@@ -6541,6 +9760,12 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6541
9760
  const payload = ensureCaptureSuccess(capture, request.sessionId);
6542
9761
  const actionResult = payload;
6543
9762
  const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
9763
+ const actionResultPayload = typeof actionResult.result === 'object' && actionResult.result !== null
9764
+ ? actionResult.result
9765
+ : undefined;
9766
+ const nativeLocatorResolution = typeof actionResultPayload?.locatorResolution === 'object' && actionResultPayload.locatorResolution !== null
9767
+ ? actionResultPayload.locatorResolution
9768
+ : undefined;
6544
9769
  let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
6545
9770
  if (!failed && request.mode === 'fast') {
6546
9771
  await sleep(75);
@@ -6555,7 +9780,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6555
9780
  action: step.action,
6556
9781
  traceId: actionResult.traceId,
6557
9782
  target: {
6558
- resolution: resolvedTarget.resolution,
9783
+ resolution: nativeLocatorResolution ?? resolvedTarget.resolution,
6559
9784
  actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
6560
9785
  ? actionResult.target
6561
9786
  : undefined,
@@ -6568,6 +9793,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6568
9793
  : undefined,
6569
9794
  pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
6570
9795
  };
9796
+ if (failed && getDb && finalStepResult.traceId) {
9797
+ mergeAutomationDiagnosticsEvidence(getDb(), {
9798
+ sessionId: request.sessionId,
9799
+ traceId: finalStepResult.traceId,
9800
+ failureEvidence: finalStepResult.failureEvidence,
9801
+ cdpFailure: actionResult.failureReason,
9802
+ });
9803
+ }
6571
9804
  }
6572
9805
  else if (step.kind === 'waitFor') {
6573
9806
  const waitInput = {
@@ -6595,6 +9828,48 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6595
9828
  pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
6596
9829
  };
6597
9830
  }
9831
+ else if (step.kind === 'wait') {
9832
+ const waitSpec = AutomationWaitSpecSchema.parse({
9833
+ ...step.wait,
9834
+ timeoutMs: step.wait.timeoutMs ?? request.defaultTimeoutMs,
9835
+ pollIntervalMs: step.wait.pollIntervalMs ?? request.defaultPollIntervalMs,
9836
+ });
9837
+ const waited = await runAutomationWait({
9838
+ sessionId: request.sessionId,
9839
+ wait: waitSpec,
9840
+ capturePageState: workflowCapturePageState,
9841
+ captureClient,
9842
+ getDb,
9843
+ });
9844
+ if (waited.waitKind === 'url' || waited.waitKind === 'navigation' || waited.waitKind === 'load_state') {
9845
+ lastPageCapture = await workflowCapturePageState(request.sessionId, {
9846
+ includeButtons: true,
9847
+ includeLinks: true,
9848
+ includeInputs: true,
9849
+ includeModals: true,
9850
+ maxItems: request.mode === 'fast' ? 12 : 20,
9851
+ maxTextLength: request.mode === 'fast' ? 60 : 80,
9852
+ }).catch(() => lastPageCapture);
9853
+ }
9854
+ finalStepResult = {
9855
+ id: stepId,
9856
+ kind: step.kind,
9857
+ status: waited.matched ? 'succeeded' : 'failed',
9858
+ durationMs: Math.max(0, Date.now() - startedAt),
9859
+ wait: {
9860
+ ...waitSpec,
9861
+ waitKind: waited.waitKind,
9862
+ matched: waited.matched,
9863
+ timeoutMs: waited.timeoutMs,
9864
+ pollIntervalMs: waited.pollIntervalMs,
9865
+ },
9866
+ waitedMs: waited.waitedMs,
9867
+ attempts: waited.attempts,
9868
+ error: waited.error,
9869
+ pageChangeSummary: createPageChangeSummary(previousCapture, lastPageCapture),
9870
+ target: waited.evidence,
9871
+ };
9872
+ }
6598
9873
  else {
6599
9874
  const capture = request.mode === 'fast' && lastPageCapture
6600
9875
  ? lastPageCapture
@@ -6629,7 +9904,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6629
9904
  target: step.kind === 'action' && workflowError
6630
9905
  ? workflowError.details
6631
9906
  : undefined,
6632
- matcher: step.kind === 'action' ? undefined : step.matcher,
9907
+ matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
9908
+ wait: step.kind === 'wait' ? step.wait : undefined,
6633
9909
  error: normalizeWorkflowError(error),
6634
9910
  };
6635
9911
  }
@@ -6652,6 +9928,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6652
9928
  if (evidence) {
6653
9929
  failureCaptureCount += 1;
6654
9930
  finalStepResult.failureEvidence = evidence;
9931
+ if (getDb && finalStepResult.traceId) {
9932
+ mergeAutomationDiagnosticsEvidence(getDb(), {
9933
+ sessionId: request.sessionId,
9934
+ traceId: finalStepResult.traceId,
9935
+ failureEvidence: evidence,
9936
+ cdpFailure: finalStepResult.error,
9937
+ });
9938
+ }
6655
9939
  }
6656
9940
  }
6657
9941
  stepResults.push(finalStepResult);
@@ -6671,7 +9955,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6671
9955
  status: 'skipped',
6672
9956
  durationMs: 0,
6673
9957
  action: step.kind === 'action' ? step.action : undefined,
6674
- matcher: step.kind === 'action' ? undefined : step.matcher,
9958
+ matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
9959
+ wait: step.kind === 'wait' ? step.wait : undefined,
6675
9960
  pageChangeSummary: undefined,
6676
9961
  error: {
6677
9962
  code: 'workflow_stopped_early',
@@ -6871,7 +10156,62 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6871
10156
  const actionInput = { ...input };
6872
10157
  delete actionInput.sessionId;
6873
10158
  delete actionInput.captureOnFailure;
6874
- const request = LiveUIActionRequestSchema.parse(actionInput);
10159
+ let request = LiveUIActionRequestSchema.parse(actionInput);
10160
+ let targetResolution;
10161
+ try {
10162
+ if (request.target?.locator) {
10163
+ targetResolution = {
10164
+ strategy: 'native_locator_pending',
10165
+ matcher: summarizeWorkflowTargetMatcher(request.target),
10166
+ };
10167
+ }
10168
+ else if (hasSemanticActionTargetMatcher(request.target)) {
10169
+ const resolvedTarget = await resolveWorkflowActionTarget(sessionId, request.target, capturePageState);
10170
+ targetResolution = resolvedTarget.resolution;
10171
+ request = LiveUIActionRequestSchema.parse({
10172
+ ...request,
10173
+ target: resolvedTarget.target,
10174
+ });
10175
+ }
10176
+ }
10177
+ catch (error) {
10178
+ if (error instanceof WorkflowTargetResolutionError) {
10179
+ const traceId = request.traceId ?? `uiaction-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
10180
+ return {
10181
+ ...createBaseResponse(sessionId),
10182
+ limitsApplied: {
10183
+ maxResults: 1,
10184
+ truncated: false,
10185
+ },
10186
+ action: request.action,
10187
+ status: 'rejected',
10188
+ traceId,
10189
+ startedAt: Date.now(),
10190
+ finishedAt: Date.now(),
10191
+ durationMs: 0,
10192
+ target: {
10193
+ matched: false,
10194
+ },
10195
+ tabContext: {
10196
+ frameId: 0,
10197
+ },
10198
+ failureDetails: {
10199
+ code: error.code,
10200
+ message: error.message,
10201
+ },
10202
+ targetResolution: {
10203
+ ...error.details,
10204
+ strategy: 'semantic_failed',
10205
+ },
10206
+ supportedScopes: {
10207
+ executionScope: 'top-document-v1',
10208
+ topDocumentOnly: false,
10209
+ opensNewBrowserSession: false,
10210
+ },
10211
+ };
10212
+ }
10213
+ throw error;
10214
+ }
6875
10215
  const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
6876
10216
  const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
6877
10217
  const payload = ensureCaptureSuccess(capture, sessionId);
@@ -6896,6 +10236,24 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6896
10236
  const target = typeof actionResult.target === 'object' && actionResult.target !== null
6897
10237
  ? actionResult.target
6898
10238
  : {};
10239
+ const actionResultRecord = actionResult;
10240
+ const nativeResult = typeof actionResultRecord.result === 'object' && actionResultRecord.result !== null
10241
+ ? actionResultRecord.result
10242
+ : undefined;
10243
+ const nativeLocatorResolution = typeof nativeResult?.locatorResolution === 'object' && nativeResult.locatorResolution !== null
10244
+ ? nativeResult.locatorResolution
10245
+ : undefined;
10246
+ if (nativeLocatorResolution) {
10247
+ targetResolution = nativeLocatorResolution;
10248
+ }
10249
+ if (failed && getDb && actionResult.traceId) {
10250
+ mergeAutomationDiagnosticsEvidence(getDb(), {
10251
+ sessionId,
10252
+ traceId: actionResult.traceId,
10253
+ failureEvidence,
10254
+ cdpFailure: actionResult.failureReason,
10255
+ });
10256
+ }
6899
10257
  return {
6900
10258
  ...createBaseResponse(sessionId),
6901
10259
  limitsApplied: {
@@ -6914,6 +10272,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6914
10272
  : undefined,
6915
10273
  actionResult,
6916
10274
  target,
10275
+ targetResolution,
6917
10276
  tabContext: {
6918
10277
  tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
6919
10278
  frameId: typeof target.frameId === 'number' ? target.frameId : 0,
@@ -6924,7 +10283,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6924
10283
  postActionState,
6925
10284
  supportedScopes: {
6926
10285
  executionScope: actionResult.executionScope,
6927
- topDocumentOnly: true,
10286
+ topDocumentOnly: false,
6928
10287
  opensNewBrowserSession: false,
6929
10288
  },
6930
10289
  };
@@ -6976,13 +10335,37 @@ export function createToolRegistry(overrides = {}) {
6976
10335
  };
6977
10336
  });
6978
10337
  }
6979
- export async function routeToolCall(tools, toolName, input) {
10338
+ export async function routeToolCall(tools, toolName, input, options = {}) {
6980
10339
  const tool = tools.find((candidate) => candidate.name === toolName);
6981
10340
  if (!tool) {
6982
10341
  throw new Error(`Unknown tool: ${toolName}`);
6983
10342
  }
6984
- const response = await tool.handler(isRecord(input) ? input : {});
6985
- return attachResponseBytes(response);
10343
+ const normalizedInput = isRecord(input) ? input : {};
10344
+ const guardCall = options.loopGuard?.prepareCall(toolName, normalizedInput);
10345
+ const beforeCall = guardCall ? await options.loopGuard?.beforeCall(guardCall) : undefined;
10346
+ if (beforeCall?.blocked) {
10347
+ return attachResponseBytes(beforeCall.response);
10348
+ }
10349
+ const startedAt = Date.now();
10350
+ try {
10351
+ const response = await tool.handler(normalizedInput);
10352
+ const guarded = guardCall
10353
+ ? await options.loopGuard?.afterCall(guardCall, {
10354
+ response,
10355
+ durationMs: Date.now() - startedAt,
10356
+ })
10357
+ : undefined;
10358
+ return attachResponseBytes((guarded?.response ?? response));
10359
+ }
10360
+ catch (error) {
10361
+ if (guardCall) {
10362
+ await options.loopGuard?.afterCall(guardCall, {
10363
+ error,
10364
+ durationMs: Date.now() - startedAt,
10365
+ });
10366
+ }
10367
+ throw error;
10368
+ }
6986
10369
  }
6987
10370
  export function createMCPServer(overrides = {}, options = {}) {
6988
10371
  const logger = options.logger ?? createDefaultMcpLogger();
@@ -6994,6 +10377,17 @@ export function createMCPServer(overrides = {}, options = {}) {
6994
10377
  ...v2Handlers,
6995
10378
  ...overrides,
6996
10379
  });
10380
+ const loopGuard = options.loopGuard === false
10381
+ ? undefined
10382
+ : options.loopGuard ?? createToolLoopGuard({
10383
+ getDb: () => getConnection().db,
10384
+ onEvent: (event) => {
10385
+ logger.info({
10386
+ component: 'mcp',
10387
+ ...event,
10388
+ }, `[MCPServer][MCP] ${event.event}`);
10389
+ },
10390
+ });
6997
10391
  const server = new Server({
6998
10392
  name: 'browser-debug-mcp-bridge',
6999
10393
  version: '1.0.0',
@@ -7017,7 +10411,7 @@ export function createMCPServer(overrides = {}, options = {}) {
7017
10411
  const startedAt = Date.now();
7018
10412
  logger.info({ component: 'mcp', event: 'tool_call_started', toolName }, '[MCPServer][MCP] Tool call started');
7019
10413
  try {
7020
- const response = await routeToolCall(tools, toolName, request.params.arguments);
10414
+ const response = await routeToolCall(tools, toolName, request.params.arguments, { loopGuard });
7021
10415
  logger.info({
7022
10416
  component: 'mcp',
7023
10417
  event: 'tool_call_completed',