browser-debug-mcp-bridge 1.9.0 → 1.10.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.
@@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { existsSync, readFileSync } from 'fs';
5
5
  import { dirname, resolve } from 'path';
6
+ import { z } from 'zod';
6
7
  import { getConnection } from '../db/connection.js';
7
8
  function createDefaultMcpLogger() {
8
9
  const write = (level, message, payload) => {
@@ -20,6 +21,238 @@ function createDefaultMcpLogger() {
20
21
  },
21
22
  };
22
23
  }
24
+ const LiveUIActionTargetSchema = z.object({
25
+ selector: z.string().min(1).optional(),
26
+ elementRef: z.string().min(1).optional(),
27
+ tabId: z.number().int().min(0).optional(),
28
+ frameId: z.number().int().min(0).optional(),
29
+ url: z.string().url().optional(),
30
+ });
31
+ const LiveUIActionBaseSchema = z.object({
32
+ traceId: z.string().min(1).optional(),
33
+ target: LiveUIActionTargetSchema.optional(),
34
+ });
35
+ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
36
+ LiveUIActionBaseSchema.extend({
37
+ action: z.literal('click'),
38
+ input: z.object({
39
+ button: z.enum(['left', 'middle', 'right']).optional(),
40
+ clickCount: z.number().int().min(1).max(3).optional(),
41
+ }).optional(),
42
+ }),
43
+ LiveUIActionBaseSchema.extend({
44
+ action: z.literal('input'),
45
+ input: z.object({
46
+ value: z.string(),
47
+ }),
48
+ }),
49
+ LiveUIActionBaseSchema.extend({
50
+ action: z.literal('focus'),
51
+ input: z.object({}).optional(),
52
+ }),
53
+ LiveUIActionBaseSchema.extend({
54
+ action: z.literal('blur'),
55
+ input: z.object({}).optional(),
56
+ }),
57
+ LiveUIActionBaseSchema.extend({
58
+ action: z.literal('scroll'),
59
+ input: z.object({
60
+ x: z.number().optional(),
61
+ y: z.number().optional(),
62
+ behavior: z.enum(['auto', 'smooth']).optional(),
63
+ }).optional(),
64
+ }),
65
+ LiveUIActionBaseSchema.extend({
66
+ action: z.literal('press_key'),
67
+ input: z.object({
68
+ key: z.string().min(1),
69
+ altKey: z.boolean().optional(),
70
+ ctrlKey: z.boolean().optional(),
71
+ metaKey: z.boolean().optional(),
72
+ shiftKey: z.boolean().optional(),
73
+ }),
74
+ }),
75
+ LiveUIActionBaseSchema.extend({
76
+ action: z.literal('submit'),
77
+ input: z.object({}).optional(),
78
+ }),
79
+ LiveUIActionBaseSchema.extend({
80
+ action: z.literal('reload'),
81
+ input: z.object({
82
+ ignoreCache: z.boolean().optional(),
83
+ }).optional(),
84
+ }),
85
+ ]);
86
+ const UIWorkflowModeSchema = z.enum(['safe', 'fast']);
87
+ const UIWorkflowFailureStrategySchema = z.enum(['stop', 'continue', 'retry_once']);
88
+ const UIWorkflowActionTargetScopeSchema = z.enum(['buttons', 'inputs', 'modals', 'focused']);
89
+ const UIWorkflowActionTargetSchema = z.object({
90
+ selector: z.string().min(1).optional(),
91
+ elementRef: z.string().min(1).optional(),
92
+ tabId: z.number().int().min(0).optional(),
93
+ frameId: z.number().int().min(0).optional(),
94
+ url: z.string().url().optional(),
95
+ testId: z.string().min(1).optional(),
96
+ scope: UIWorkflowActionTargetScopeSchema.optional(),
97
+ textContains: z.string().min(1).optional(),
98
+ labelContains: z.string().min(1).optional(),
99
+ titleContains: z.string().min(1).optional(),
100
+ tagName: z.string().min(1).optional(),
101
+ type: z.string().min(1).optional(),
102
+ disabled: z.boolean().optional(),
103
+ selected: z.boolean().optional(),
104
+ pressed: z.boolean().optional(),
105
+ expanded: z.boolean().optional(),
106
+ readOnly: z.boolean().optional(),
107
+ requiredField: z.boolean().optional(),
108
+ }).superRefine((value, ctx) => {
109
+ if (!value.selector
110
+ && !value.elementRef
111
+ && !value.testId
112
+ && !value.textContains
113
+ && !value.labelContains
114
+ && !value.titleContains) {
115
+ ctx.addIssue({
116
+ code: z.ZodIssueCode.custom,
117
+ message: 'target requires selector, elementRef, testId, textContains, labelContains, or titleContains',
118
+ path: ['target'],
119
+ });
120
+ }
121
+ });
122
+ const UIWorkflowFailureCaptureSchema = z.object({
123
+ enabled: z.boolean().optional(),
124
+ selector: z.string().min(1).optional(),
125
+ mode: z.enum(['dom', 'png', 'both']).optional(),
126
+ styleMode: z.enum(['computed-lite', 'computed-full']).optional(),
127
+ maxDepth: z.number().int().min(1).max(10).optional(),
128
+ maxBytes: z.number().int().min(1_000).max(200_000).optional(),
129
+ maxAncestors: z.number().int().min(0).max(10).optional(),
130
+ includeDom: z.boolean().optional(),
131
+ includeStyles: z.boolean().optional(),
132
+ includePngDataUrl: z.boolean().optional(),
133
+ });
134
+ const UIWorkflowFailurePolicySchema = z.object({
135
+ strategy: UIWorkflowFailureStrategySchema.optional(),
136
+ capture: UIWorkflowFailureCaptureSchema.optional(),
137
+ });
138
+ const UIWorkflowStepBaseSchema = z.object({
139
+ id: z.string().min(1).optional(),
140
+ note: z.string().min(1).optional(),
141
+ onFailure: UIWorkflowFailurePolicySchema.optional(),
142
+ });
143
+ const UIWorkflowActionBaseSchema = UIWorkflowStepBaseSchema.extend({
144
+ kind: z.literal('action'),
145
+ traceId: z.string().min(1).optional(),
146
+ target: UIWorkflowActionTargetSchema.optional(),
147
+ });
148
+ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
149
+ UIWorkflowActionBaseSchema.extend({
150
+ action: z.literal('click'),
151
+ input: z.object({
152
+ button: z.enum(['left', 'middle', 'right']).optional(),
153
+ clickCount: z.number().int().min(1).max(3).optional(),
154
+ }).optional(),
155
+ }),
156
+ UIWorkflowActionBaseSchema.extend({
157
+ action: z.literal('input'),
158
+ input: z.object({
159
+ value: z.string(),
160
+ }),
161
+ }),
162
+ UIWorkflowActionBaseSchema.extend({
163
+ action: z.literal('focus'),
164
+ input: z.object({}).optional(),
165
+ }),
166
+ UIWorkflowActionBaseSchema.extend({
167
+ action: z.literal('blur'),
168
+ input: z.object({}).optional(),
169
+ }),
170
+ UIWorkflowActionBaseSchema.extend({
171
+ action: z.literal('scroll'),
172
+ input: z.object({
173
+ x: z.number().optional(),
174
+ y: z.number().optional(),
175
+ behavior: z.enum(['auto', 'smooth']).optional(),
176
+ }).optional(),
177
+ }),
178
+ UIWorkflowActionBaseSchema.extend({
179
+ action: z.literal('press_key'),
180
+ input: z.object({
181
+ key: z.string().min(1),
182
+ altKey: z.boolean().optional(),
183
+ ctrlKey: z.boolean().optional(),
184
+ metaKey: z.boolean().optional(),
185
+ shiftKey: z.boolean().optional(),
186
+ }),
187
+ }),
188
+ UIWorkflowActionBaseSchema.extend({
189
+ action: z.literal('submit'),
190
+ input: z.object({}).optional(),
191
+ }),
192
+ UIWorkflowActionBaseSchema.extend({
193
+ action: z.literal('reload'),
194
+ input: z.object({
195
+ ignoreCache: z.boolean().optional(),
196
+ }).optional(),
197
+ }),
198
+ ]);
199
+ const UIWorkflowPageStateMatcherSchema = z.object({
200
+ scope: z.enum(['buttons', 'inputs', 'modals', 'focused', 'page']),
201
+ selector: z.string().optional(),
202
+ testId: z.string().optional(),
203
+ textContains: z.string().optional(),
204
+ labelContains: z.string().optional(),
205
+ titleContains: z.string().optional(),
206
+ urlContains: z.string().optional(),
207
+ language: z.string().optional(),
208
+ disabled: z.boolean().optional(),
209
+ selected: z.boolean().optional(),
210
+ pressed: z.boolean().optional(),
211
+ expanded: z.boolean().optional(),
212
+ readOnly: z.boolean().optional(),
213
+ requiredField: z.boolean().optional(),
214
+ tagName: z.string().optional(),
215
+ type: z.string().optional(),
216
+ countExactly: z.number().int().min(0).optional(),
217
+ countAtLeast: z.number().int().min(0).optional(),
218
+ maxItems: z.number().int().min(1).max(100).optional(),
219
+ maxTextLength: z.number().int().min(8).max(200).optional(),
220
+ }).superRefine((value, ctx) => {
221
+ if (value.countExactly !== undefined && value.countAtLeast !== undefined) {
222
+ ctx.addIssue({
223
+ code: z.ZodIssueCode.custom,
224
+ message: 'countExactly and countAtLeast cannot both be set',
225
+ path: ['countExactly'],
226
+ });
227
+ }
228
+ });
229
+ const UIWorkflowWaitForStepSchema = UIWorkflowStepBaseSchema.extend({
230
+ kind: z.literal('waitFor'),
231
+ matcher: UIWorkflowPageStateMatcherSchema.extend({
232
+ timeoutMs: z.number().int().min(100).max(30000).optional(),
233
+ pollIntervalMs: z.number().int().min(50).max(2000).optional(),
234
+ }),
235
+ });
236
+ const UIWorkflowAssertStepSchema = UIWorkflowStepBaseSchema.extend({
237
+ kind: z.literal('assert'),
238
+ matcher: UIWorkflowPageStateMatcherSchema,
239
+ });
240
+ const UIWorkflowStepSchema = z.discriminatedUnion('kind', [
241
+ UIWorkflowActionStepSchema,
242
+ UIWorkflowWaitForStepSchema,
243
+ UIWorkflowAssertStepSchema,
244
+ ]);
245
+ const RunUIStepsSchema = z.object({
246
+ sessionId: z.string().min(1),
247
+ mode: UIWorkflowModeSchema.default('safe'),
248
+ stopOnFailure: z.boolean().default(true),
249
+ defaultTimeoutMs: z.number().int().min(100).max(30000).optional(),
250
+ defaultPollIntervalMs: z.number().int().min(50).max(2000).optional(),
251
+ steps: z.array(UIWorkflowStepSchema).min(1).max(50),
252
+ });
253
+ function createUIWorkflowTraceId() {
254
+ return `uiworkflow-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
255
+ }
23
256
  const TOOL_SCHEMAS = {
24
257
  list_sessions: {
25
258
  type: 'object',
@@ -212,6 +445,103 @@ const TOOL_SCHEMAS = {
212
445
  selector: { type: 'string' },
213
446
  },
214
447
  },
448
+ get_page_state: {
449
+ type: 'object',
450
+ required: ['sessionId'],
451
+ properties: {
452
+ sessionId: { type: 'string' },
453
+ maxItems: { type: 'number' },
454
+ maxTextLength: { type: 'number' },
455
+ includeButtons: { type: 'boolean' },
456
+ includeInputs: { type: 'boolean' },
457
+ includeModals: { type: 'boolean' },
458
+ },
459
+ },
460
+ get_interactive_elements: {
461
+ type: 'object',
462
+ required: ['sessionId'],
463
+ properties: {
464
+ sessionId: { type: 'string' },
465
+ kinds: {
466
+ type: 'array',
467
+ items: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
468
+ },
469
+ maxItems: { type: 'number' },
470
+ maxTextLength: { type: 'number' },
471
+ },
472
+ },
473
+ get_live_session_health: {
474
+ type: 'object',
475
+ required: ['sessionId'],
476
+ properties: {
477
+ sessionId: { type: 'string' },
478
+ },
479
+ },
480
+ set_viewport: {
481
+ type: 'object',
482
+ required: ['sessionId', 'width', 'height'],
483
+ properties: {
484
+ sessionId: { type: 'string' },
485
+ width: { type: 'number' },
486
+ height: { type: 'number' },
487
+ },
488
+ },
489
+ assert_page_state: {
490
+ type: 'object',
491
+ required: ['sessionId', 'scope'],
492
+ properties: {
493
+ sessionId: { type: 'string' },
494
+ scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
495
+ selector: { type: 'string' },
496
+ testId: { type: 'string' },
497
+ textContains: { type: 'string' },
498
+ labelContains: { type: 'string' },
499
+ titleContains: { type: 'string' },
500
+ urlContains: { type: 'string' },
501
+ language: { type: 'string' },
502
+ disabled: { type: 'boolean' },
503
+ selected: { type: 'boolean' },
504
+ pressed: { type: 'boolean' },
505
+ expanded: { type: 'boolean' },
506
+ readOnly: { type: 'boolean' },
507
+ requiredField: { type: 'boolean' },
508
+ tagName: { type: 'string' },
509
+ type: { type: 'string' },
510
+ countExactly: { type: 'number' },
511
+ countAtLeast: { type: 'number' },
512
+ maxItems: { type: 'number' },
513
+ maxTextLength: { type: 'number' },
514
+ },
515
+ },
516
+ wait_for_page_state: {
517
+ type: 'object',
518
+ required: ['sessionId', 'scope'],
519
+ properties: {
520
+ sessionId: { type: 'string' },
521
+ scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
522
+ selector: { type: 'string' },
523
+ testId: { type: 'string' },
524
+ textContains: { type: 'string' },
525
+ labelContains: { type: 'string' },
526
+ titleContains: { type: 'string' },
527
+ urlContains: { type: 'string' },
528
+ language: { type: 'string' },
529
+ disabled: { type: 'boolean' },
530
+ selected: { type: 'boolean' },
531
+ pressed: { type: 'boolean' },
532
+ expanded: { type: 'boolean' },
533
+ readOnly: { type: 'boolean' },
534
+ requiredField: { type: 'boolean' },
535
+ tagName: { type: 'string' },
536
+ type: { type: 'string' },
537
+ countExactly: { type: 'number' },
538
+ countAtLeast: { type: 'number' },
539
+ maxItems: { type: 'number' },
540
+ maxTextLength: { type: 'number' },
541
+ timeoutMs: { type: 'number' },
542
+ pollIntervalMs: { type: 'number' },
543
+ },
544
+ },
215
545
  capture_ui_snapshot: {
216
546
  type: 'object',
217
547
  required: ['sessionId'],
@@ -298,6 +628,191 @@ const TOOL_SCHEMAS = {
298
628
  encoding: { type: 'string' },
299
629
  },
300
630
  },
631
+ list_automation_runs: {
632
+ type: 'object',
633
+ required: ['sessionId'],
634
+ properties: {
635
+ sessionId: { type: 'string' },
636
+ status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
637
+ action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
638
+ traceId: { type: 'string' },
639
+ limit: { type: 'number' },
640
+ offset: { type: 'number' },
641
+ maxResponseBytes: { type: 'number' },
642
+ },
643
+ },
644
+ get_automation_run: {
645
+ type: 'object',
646
+ required: ['sessionId', 'runId'],
647
+ properties: {
648
+ sessionId: { type: 'string' },
649
+ runId: { type: 'string' },
650
+ stepLimit: { type: 'number' },
651
+ stepOffset: { type: 'number' },
652
+ maxResponseBytes: { type: 'number' },
653
+ },
654
+ },
655
+ execute_ui_action: {
656
+ type: 'object',
657
+ required: ['sessionId', 'action'],
658
+ properties: {
659
+ sessionId: { type: 'string' },
660
+ action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
661
+ traceId: { type: 'string' },
662
+ target: {
663
+ type: 'object',
664
+ properties: {
665
+ selector: { type: 'string' },
666
+ elementRef: { type: 'string' },
667
+ tabId: { type: 'number' },
668
+ frameId: { type: 'number' },
669
+ url: { type: 'string' },
670
+ },
671
+ },
672
+ input: { type: 'object' },
673
+ captureOnFailure: {
674
+ type: 'object',
675
+ properties: {
676
+ enabled: { type: 'boolean' },
677
+ selector: { type: 'string' },
678
+ mode: { type: 'string', enum: ['dom', 'png', 'both'] },
679
+ styleMode: { type: 'string', enum: ['computed-lite', 'computed-full'] },
680
+ maxDepth: { type: 'number' },
681
+ maxBytes: { type: 'number' },
682
+ maxAncestors: { type: 'number' },
683
+ includeDom: { type: 'boolean' },
684
+ includeStyles: { type: 'boolean' },
685
+ includePngDataUrl: { type: 'boolean' },
686
+ },
687
+ },
688
+ waitForPageState: {
689
+ type: 'object',
690
+ required: ['scope'],
691
+ properties: {
692
+ scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
693
+ selector: { type: 'string' },
694
+ testId: { type: 'string' },
695
+ textContains: { type: 'string' },
696
+ labelContains: { type: 'string' },
697
+ titleContains: { type: 'string' },
698
+ urlContains: { type: 'string' },
699
+ language: { type: 'string' },
700
+ disabled: { type: 'boolean' },
701
+ selected: { type: 'boolean' },
702
+ pressed: { type: 'boolean' },
703
+ expanded: { type: 'boolean' },
704
+ readOnly: { type: 'boolean' },
705
+ requiredField: { type: 'boolean' },
706
+ tagName: { type: 'string' },
707
+ type: { type: 'string' },
708
+ countExactly: { type: 'number' },
709
+ countAtLeast: { type: 'number' },
710
+ maxItems: { type: 'number' },
711
+ maxTextLength: { type: 'number' },
712
+ timeoutMs: { type: 'number' },
713
+ pollIntervalMs: { type: 'number' },
714
+ },
715
+ },
716
+ },
717
+ },
718
+ run_ui_steps: {
719
+ type: 'object',
720
+ required: ['sessionId', 'steps'],
721
+ properties: {
722
+ sessionId: { type: 'string' },
723
+ mode: { type: 'string', enum: ['safe', 'fast'] },
724
+ stopOnFailure: { type: 'boolean' },
725
+ defaultTimeoutMs: { type: 'number' },
726
+ defaultPollIntervalMs: { type: 'number' },
727
+ steps: {
728
+ type: 'array',
729
+ minItems: 1,
730
+ items: {
731
+ type: 'object',
732
+ required: ['kind'],
733
+ properties: {
734
+ id: { type: 'string' },
735
+ note: { type: 'string' },
736
+ kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
737
+ action: { type: 'string' },
738
+ traceId: { type: 'string' },
739
+ target: {
740
+ type: 'object',
741
+ properties: {
742
+ selector: { type: 'string' },
743
+ elementRef: { type: 'string' },
744
+ tabId: { type: 'number' },
745
+ frameId: { type: 'number' },
746
+ url: { type: 'string' },
747
+ testId: { type: 'string' },
748
+ scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
749
+ textContains: { type: 'string' },
750
+ labelContains: { type: 'string' },
751
+ titleContains: { type: 'string' },
752
+ tagName: { type: 'string' },
753
+ type: { type: 'string' },
754
+ disabled: { type: 'boolean' },
755
+ selected: { type: 'boolean' },
756
+ pressed: { type: 'boolean' },
757
+ expanded: { type: 'boolean' },
758
+ readOnly: { type: 'boolean' },
759
+ requiredField: { type: 'boolean' },
760
+ },
761
+ },
762
+ input: { type: 'object' },
763
+ onFailure: {
764
+ type: 'object',
765
+ properties: {
766
+ strategy: { type: 'string', enum: ['stop', 'continue', 'retry_once'] },
767
+ capture: {
768
+ type: 'object',
769
+ properties: {
770
+ enabled: { type: 'boolean' },
771
+ selector: { type: 'string' },
772
+ mode: { type: 'string', enum: ['dom', 'png', 'both'] },
773
+ styleMode: { type: 'string', enum: ['computed-lite', 'computed-full'] },
774
+ maxDepth: { type: 'number' },
775
+ maxBytes: { type: 'number' },
776
+ maxAncestors: { type: 'number' },
777
+ includeDom: { type: 'boolean' },
778
+ includeStyles: { type: 'boolean' },
779
+ includePngDataUrl: { type: 'boolean' },
780
+ },
781
+ },
782
+ },
783
+ },
784
+ matcher: {
785
+ type: 'object',
786
+ properties: {
787
+ scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
788
+ selector: { type: 'string' },
789
+ testId: { type: 'string' },
790
+ textContains: { type: 'string' },
791
+ labelContains: { type: 'string' },
792
+ titleContains: { type: 'string' },
793
+ urlContains: { type: 'string' },
794
+ language: { type: 'string' },
795
+ disabled: { type: 'boolean' },
796
+ selected: { type: 'boolean' },
797
+ pressed: { type: 'boolean' },
798
+ expanded: { type: 'boolean' },
799
+ readOnly: { type: 'boolean' },
800
+ requiredField: { type: 'boolean' },
801
+ tagName: { type: 'string' },
802
+ type: { type: 'string' },
803
+ countExactly: { type: 'number' },
804
+ countAtLeast: { type: 'number' },
805
+ maxItems: { type: 'number' },
806
+ maxTextLength: { type: 'number' },
807
+ timeoutMs: { type: 'number' },
808
+ pollIntervalMs: { type: 'number' },
809
+ },
810
+ },
811
+ },
812
+ },
813
+ },
814
+ },
815
+ },
301
816
  };
302
817
  const TOOL_DESCRIPTIONS = {
303
818
  list_sessions: 'List captured debugging sessions',
@@ -318,6 +833,12 @@ const TOOL_DESCRIPTIONS = {
318
833
  get_dom_document: 'Capture full document as outline or html',
319
834
  get_computed_styles: 'Read computed CSS styles for an element',
320
835
  get_layout_metrics: 'Read viewport and element layout metrics',
836
+ get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
837
+ get_interactive_elements: 'Read compact live element references for buttons, inputs, modals, and focused elements',
838
+ get_live_session_health: 'Read live transport health and session binding details for one session',
839
+ set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
840
+ assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
841
+ wait_for_page_state: 'Poll compact page state until a structured assertion becomes true',
321
842
  capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
322
843
  get_live_console_logs: 'Read in-memory live console logs for a connected session',
323
844
  explain_last_failure: 'Explain the latest failure timeline',
@@ -325,6 +846,10 @@ const TOOL_DESCRIPTIONS = {
325
846
  list_snapshots: 'List snapshot metadata by session/time/trigger',
326
847
  get_snapshot_for_event: 'Find snapshot most related to an event',
327
848
  get_snapshot_asset: 'Read bounded binary chunks for snapshot assets',
849
+ list_automation_runs: 'List first-class automation runs from dedicated automation tables',
850
+ get_automation_run: 'Inspect one automation run with bounded step details',
851
+ execute_ui_action: 'Execute one live UI action in the current bound extension session',
852
+ run_ui_steps: 'Run a small generic UI workflow locally in the bridge using actions, waits, and assertions',
328
853
  };
329
854
  const ALL_TOOLS = Object.keys(TOOL_SCHEMAS);
330
855
  const DEFAULT_REDACTION_SUMMARY = {
@@ -831,6 +1356,56 @@ function mapSnapshotMetadata(row) {
831
1356
  createdAt: row.created_at,
832
1357
  };
833
1358
  }
1359
+ function mapAutomationRunRecord(row) {
1360
+ return {
1361
+ runId: row.run_id,
1362
+ sessionId: row.session_id,
1363
+ traceId: row.trace_id ?? undefined,
1364
+ action: row.action ?? undefined,
1365
+ tabId: row.tab_id ?? undefined,
1366
+ selector: row.selector ?? undefined,
1367
+ status: row.status,
1368
+ startedAt: row.started_at,
1369
+ completedAt: row.completed_at ?? undefined,
1370
+ durationMs: typeof row.completed_at === 'number'
1371
+ ? Math.max(0, row.completed_at - row.started_at)
1372
+ : undefined,
1373
+ stopReason: row.stop_reason ?? undefined,
1374
+ target: parseJsonOrUndefined(row.target_summary_json),
1375
+ failure: parseJsonOrUndefined(row.failure_json),
1376
+ redaction: parseJsonOrUndefined(row.redaction_json),
1377
+ stepCount: row.step_count,
1378
+ lastStepAt: row.last_step_at ?? undefined,
1379
+ createdAt: row.created_at,
1380
+ updatedAt: row.updated_at,
1381
+ source: 'automation_runs',
1382
+ };
1383
+ }
1384
+ function mapAutomationStepRecord(row) {
1385
+ return {
1386
+ stepId: row.step_id,
1387
+ runId: row.run_id,
1388
+ sessionId: row.session_id,
1389
+ stepOrder: row.step_order,
1390
+ traceId: row.trace_id ?? undefined,
1391
+ action: row.action,
1392
+ selector: row.selector ?? undefined,
1393
+ status: row.status,
1394
+ startedAt: row.started_at ?? undefined,
1395
+ finishedAt: row.finished_at ?? undefined,
1396
+ durationMs: row.duration_ms ?? undefined,
1397
+ tabId: row.tab_id ?? undefined,
1398
+ target: parseJsonOrUndefined(row.target_summary_json),
1399
+ redaction: parseJsonOrUndefined(row.redaction_json),
1400
+ failure: parseJsonOrUndefined(row.failure_json),
1401
+ inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
1402
+ eventType: row.event_type,
1403
+ eventId: row.event_id ?? undefined,
1404
+ createdAt: row.created_at,
1405
+ updatedAt: row.updated_at,
1406
+ source: 'automation_steps',
1407
+ };
1408
+ }
834
1409
  function formatUrlPath(url) {
835
1410
  try {
836
1411
  const parsed = new URL(url);
@@ -931,6 +1506,559 @@ function resolveCaptureAncestors(value, fallback) {
931
1506
  }
932
1507
  return Math.min(floored, 8);
933
1508
  }
1509
+ function resolveStructuredMaxItems(value, fallback) {
1510
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1511
+ return fallback;
1512
+ }
1513
+ const floored = Math.floor(value);
1514
+ if (floored < 1) {
1515
+ return fallback;
1516
+ }
1517
+ return Math.min(floored, 100);
1518
+ }
1519
+ function resolveStructuredTextLength(value, fallback) {
1520
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1521
+ return fallback;
1522
+ }
1523
+ const floored = Math.floor(value);
1524
+ if (floored < 8) {
1525
+ return fallback;
1526
+ }
1527
+ return Math.min(floored, 200);
1528
+ }
1529
+ function resolveViewportDimension(value, axis) {
1530
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1531
+ throw new Error(`${axis} must be a finite number`);
1532
+ }
1533
+ const floored = Math.floor(value);
1534
+ const min = axis === 'width' ? 320 : 200;
1535
+ const max = axis === 'width' ? 5120 : 4320;
1536
+ if (floored < min || floored > max) {
1537
+ throw new Error(`${axis} must be between ${min} and ${max}`);
1538
+ }
1539
+ return floored;
1540
+ }
1541
+ class WorkflowTargetResolutionError extends Error {
1542
+ code;
1543
+ details;
1544
+ constructor(code, message, details) {
1545
+ super(message);
1546
+ this.name = 'WorkflowTargetResolutionError';
1547
+ this.code = code;
1548
+ this.details = details;
1549
+ }
1550
+ }
1551
+ function resolveOptionalMatcherString(value) {
1552
+ if (typeof value !== 'string') {
1553
+ return undefined;
1554
+ }
1555
+ const normalized = value.trim();
1556
+ return normalized.length > 0 ? normalized : undefined;
1557
+ }
1558
+ function resolveOptionalMatcherBoolean(value) {
1559
+ return typeof value === 'boolean' ? value : undefined;
1560
+ }
1561
+ function resolveOptionalMatcherCount(value, field) {
1562
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1563
+ return undefined;
1564
+ }
1565
+ const floored = Math.floor(value);
1566
+ if (floored < 0) {
1567
+ throw new Error(`${field} must be greater than or equal to 0`);
1568
+ }
1569
+ return floored;
1570
+ }
1571
+ function resolvePageStateScope(value) {
1572
+ if (value === 'buttons' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
1573
+ return value;
1574
+ }
1575
+ throw new Error('scope must be one of buttons, inputs, modals, focused, or page');
1576
+ }
1577
+ function resolvePageStateMatcher(input) {
1578
+ const matcher = {
1579
+ scope: resolvePageStateScope(input.scope),
1580
+ selector: resolveOptionalMatcherString(input.selector),
1581
+ testId: resolveOptionalMatcherString(input.testId),
1582
+ textContains: resolveOptionalMatcherString(input.textContains),
1583
+ labelContains: resolveOptionalMatcherString(input.labelContains),
1584
+ titleContains: resolveOptionalMatcherString(input.titleContains),
1585
+ urlContains: resolveOptionalMatcherString(input.urlContains),
1586
+ language: resolveOptionalMatcherString(input.language),
1587
+ disabled: resolveOptionalMatcherBoolean(input.disabled),
1588
+ selected: resolveOptionalMatcherBoolean(input.selected),
1589
+ pressed: resolveOptionalMatcherBoolean(input.pressed),
1590
+ expanded: resolveOptionalMatcherBoolean(input.expanded),
1591
+ readOnly: resolveOptionalMatcherBoolean(input.readOnly),
1592
+ requiredField: resolveOptionalMatcherBoolean(input.requiredField),
1593
+ tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
1594
+ type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
1595
+ countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
1596
+ countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
1597
+ };
1598
+ if (matcher.countExactly !== undefined && matcher.countAtLeast !== undefined) {
1599
+ throw new Error('countExactly and countAtLeast cannot both be set');
1600
+ }
1601
+ return matcher;
1602
+ }
1603
+ function includesNormalized(value, needle) {
1604
+ if (!needle) {
1605
+ return true;
1606
+ }
1607
+ return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
1608
+ }
1609
+ function equalsNormalized(value, expected) {
1610
+ if (!expected) {
1611
+ return true;
1612
+ }
1613
+ return typeof value === 'string' && value.toLowerCase() === expected.toLowerCase();
1614
+ }
1615
+ function equalsOptionalBoolean(value, expected) {
1616
+ if (expected === undefined) {
1617
+ return true;
1618
+ }
1619
+ return value === expected;
1620
+ }
1621
+ function pickPageStateScopeItems(payload, scope) {
1622
+ if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
1623
+ const value = payload[scope];
1624
+ return asRecordArray(value);
1625
+ }
1626
+ if (scope === 'focused') {
1627
+ const focused = payload.focused;
1628
+ return typeof focused === 'object' && focused !== null ? [focused] : [];
1629
+ }
1630
+ return [payload];
1631
+ }
1632
+ function matchesPageStateItem(item, matcher) {
1633
+ return (includesNormalized(item.selector, matcher.selector)
1634
+ && equalsNormalized(item.testId, matcher.testId)
1635
+ && includesNormalized(item.text, matcher.textContains)
1636
+ && includesNormalized(item.label, matcher.labelContains)
1637
+ && includesNormalized(item.title, matcher.titleContains)
1638
+ && includesNormalized(item.url, matcher.urlContains)
1639
+ && equalsNormalized(item.language, matcher.language)
1640
+ && equalsNormalized(item.tagName, matcher.tagName)
1641
+ && equalsNormalized(item.type, matcher.type)
1642
+ && equalsOptionalBoolean(item.disabled, matcher.disabled)
1643
+ && equalsOptionalBoolean(item.selected, matcher.selected)
1644
+ && equalsOptionalBoolean(item.pressed, matcher.pressed)
1645
+ && equalsOptionalBoolean(item.expanded, matcher.expanded)
1646
+ && equalsOptionalBoolean(item.readOnly, matcher.readOnly)
1647
+ && equalsOptionalBoolean(item.required, matcher.requiredField));
1648
+ }
1649
+ function evaluatePageStateAssertion(payload, matcher) {
1650
+ const scopeItems = pickPageStateScopeItems(payload, matcher.scope);
1651
+ const matchingItems = scopeItems.filter((item) => matchesPageStateItem(item, matcher));
1652
+ const matchCount = matchingItems.length;
1653
+ const matched = matcher.countExactly !== undefined
1654
+ ? matchCount === matcher.countExactly
1655
+ : matcher.countAtLeast !== undefined
1656
+ ? matchCount >= matcher.countAtLeast
1657
+ : matchCount >= 1;
1658
+ return {
1659
+ matched,
1660
+ matchCount,
1661
+ sampledMatches: matchingItems.slice(0, 5),
1662
+ expectedCount: {
1663
+ countExactly: matcher.countExactly,
1664
+ countAtLeast: matcher.countAtLeast,
1665
+ },
1666
+ summary: typeof payload.summary === 'object' && payload.summary !== null
1667
+ ? payload.summary
1668
+ : undefined,
1669
+ };
1670
+ }
1671
+ function extractPageSummarySnapshot(capture) {
1672
+ if (!capture) {
1673
+ return undefined;
1674
+ }
1675
+ const summary = typeof capture.payload.summary === 'object' && capture.payload.summary !== null
1676
+ ? capture.payload.summary
1677
+ : undefined;
1678
+ const focused = typeof capture.payload.focused === 'object' && capture.payload.focused !== null
1679
+ ? capture.payload.focused
1680
+ : undefined;
1681
+ return {
1682
+ url: typeof capture.payload.url === 'string' ? capture.payload.url : undefined,
1683
+ language: typeof capture.payload.language === 'string' ? capture.payload.language : undefined,
1684
+ summary,
1685
+ focusedText: typeof focused?.text === 'string' ? focused.text : undefined,
1686
+ };
1687
+ }
1688
+ function createPageChangeSummary(previousCapture, currentCapture) {
1689
+ const previous = extractPageSummarySnapshot(previousCapture);
1690
+ const current = extractPageSummarySnapshot(currentCapture);
1691
+ if (!current) {
1692
+ return undefined;
1693
+ }
1694
+ const changes = [];
1695
+ const previousSummary = previous?.summary;
1696
+ const currentSummary = current.summary;
1697
+ const summaryDelta = {};
1698
+ for (const key of ['buttons', 'inputs', 'modals']) {
1699
+ const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
1700
+ const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
1701
+ if (previousValue !== currentValue && currentValue !== undefined) {
1702
+ summaryDelta[key] = {
1703
+ previous: previousValue,
1704
+ current: currentValue,
1705
+ };
1706
+ changes.push(`${key} ${previousValue ?? 0} -> ${currentValue}`);
1707
+ }
1708
+ }
1709
+ if (previous?.url && current.url && previous.url !== current.url) {
1710
+ changes.push(`url changed`);
1711
+ }
1712
+ if (previous?.language && current.language && previous.language !== current.language) {
1713
+ changes.push(`language ${previous.language} -> ${current.language}`);
1714
+ }
1715
+ if ((previous?.focusedText ?? '') !== (current.focusedText ?? '') && current.focusedText) {
1716
+ changes.push('focused element changed');
1717
+ }
1718
+ return {
1719
+ changes,
1720
+ previous: previous ?? null,
1721
+ current,
1722
+ summaryDelta,
1723
+ };
1724
+ }
1725
+ function resolveInteractiveKinds(value) {
1726
+ if (!Array.isArray(value) || value.length === 0) {
1727
+ return ['buttons', 'inputs', 'modals', 'focused'];
1728
+ }
1729
+ const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
1730
+ const kinds = value
1731
+ .filter((entry) => typeof entry === 'string' && allowed.has(entry))
1732
+ .map((entry) => entry);
1733
+ return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'inputs', 'modals', 'focused'];
1734
+ }
1735
+ function collectInteractiveElementRefs(payload, kinds, maxItems) {
1736
+ const refs = [];
1737
+ for (const kind of kinds) {
1738
+ if (kind === 'focused') {
1739
+ const focused = typeof payload.focused === 'object' && payload.focused !== null
1740
+ ? payload.focused
1741
+ : undefined;
1742
+ if (focused?.elementRef) {
1743
+ refs.push({
1744
+ kind,
1745
+ ...focused,
1746
+ });
1747
+ }
1748
+ continue;
1749
+ }
1750
+ for (const item of asRecordArray(payload[kind])) {
1751
+ refs.push({
1752
+ kind,
1753
+ ...item,
1754
+ });
1755
+ if (refs.length >= maxItems) {
1756
+ return refs.slice(0, maxItems);
1757
+ }
1758
+ }
1759
+ }
1760
+ return refs.slice(0, maxItems);
1761
+ }
1762
+ async function waitForPageStateConditionDetailed(sessionId, input, capturePageState, initialCapture) {
1763
+ const matcher = resolvePageStateMatcher(input);
1764
+ const timeoutMs = resolveTimeoutMs(input.timeoutMs, 5_000, 30_000);
1765
+ const pollIntervalMs = resolveDurationMs(input.pollIntervalMs, 50, 2_000) ?? 200;
1766
+ const startedAt = Date.now();
1767
+ const deadline = startedAt + timeoutMs;
1768
+ let attempts = 0;
1769
+ let lastCapture = initialCapture;
1770
+ let lastAssertion;
1771
+ if (lastCapture) {
1772
+ lastAssertion = evaluatePageStateAssertion(lastCapture.payload, matcher);
1773
+ if (lastAssertion.matched) {
1774
+ return {
1775
+ limitsApplied: lastCapture.limitsApplied,
1776
+ matcher,
1777
+ matched: true,
1778
+ matchCount: lastAssertion.matchCount,
1779
+ expectedCount: lastAssertion.expectedCount,
1780
+ sampledMatches: lastAssertion.sampledMatches,
1781
+ pageSummary: lastAssertion.summary,
1782
+ page: {
1783
+ url: lastCapture.payload.url,
1784
+ title: lastCapture.payload.title,
1785
+ language: lastCapture.payload.language,
1786
+ viewport: lastCapture.payload.viewport,
1787
+ },
1788
+ waitedMs: 0,
1789
+ attempts,
1790
+ pollIntervalMs,
1791
+ lastCapture,
1792
+ };
1793
+ }
1794
+ }
1795
+ while (Date.now() <= deadline) {
1796
+ attempts += 1;
1797
+ lastCapture = await capturePageState(sessionId, input);
1798
+ lastAssertion = evaluatePageStateAssertion(lastCapture.payload, matcher);
1799
+ if (lastAssertion.matched) {
1800
+ return {
1801
+ limitsApplied: lastCapture.limitsApplied,
1802
+ matcher,
1803
+ matched: true,
1804
+ matchCount: lastAssertion.matchCount,
1805
+ expectedCount: lastAssertion.expectedCount,
1806
+ sampledMatches: lastAssertion.sampledMatches,
1807
+ pageSummary: lastAssertion.summary,
1808
+ page: {
1809
+ url: lastCapture.payload.url,
1810
+ title: lastCapture.payload.title,
1811
+ language: lastCapture.payload.language,
1812
+ viewport: lastCapture.payload.viewport,
1813
+ },
1814
+ waitedMs: Date.now() - startedAt,
1815
+ attempts,
1816
+ pollIntervalMs,
1817
+ lastCapture,
1818
+ };
1819
+ }
1820
+ await sleep(pollIntervalMs);
1821
+ }
1822
+ return {
1823
+ limitsApplied: lastCapture?.limitsApplied ?? { maxResults: 0, truncated: false },
1824
+ matcher,
1825
+ matched: false,
1826
+ matchCount: lastAssertion?.matchCount ?? 0,
1827
+ expectedCount: lastAssertion?.expectedCount ?? {
1828
+ countExactly: matcher.countExactly,
1829
+ countAtLeast: matcher.countAtLeast,
1830
+ },
1831
+ sampledMatches: lastAssertion?.sampledMatches ?? [],
1832
+ pageSummary: lastAssertion?.summary,
1833
+ page: lastCapture
1834
+ ? {
1835
+ url: lastCapture.payload.url,
1836
+ title: lastCapture.payload.title,
1837
+ language: lastCapture.payload.language,
1838
+ viewport: lastCapture.payload.viewport,
1839
+ }
1840
+ : undefined,
1841
+ waitedMs: Date.now() - startedAt,
1842
+ attempts,
1843
+ pollIntervalMs,
1844
+ timeoutMs,
1845
+ lastCapture,
1846
+ };
1847
+ }
1848
+ async function waitForPageStateCondition(sessionId, input, capturePageState) {
1849
+ const detailed = await waitForPageStateConditionDetailed(sessionId, input, capturePageState);
1850
+ const { lastCapture: _lastCapture, ...waited } = detailed;
1851
+ return waited;
1852
+ }
1853
+ function candidateTextForWorkflowTarget(item) {
1854
+ return [item.text, item.label, item.title]
1855
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
1856
+ .join(' ')
1857
+ .trim();
1858
+ }
1859
+ function describeWorkflowTargetCandidate(item) {
1860
+ return {
1861
+ text: candidateTextForWorkflowTarget(item) || undefined,
1862
+ testId: typeof item.testId === 'string' ? item.testId : undefined,
1863
+ selector: typeof item.selector === 'string' ? item.selector : undefined,
1864
+ tagName: typeof item.tagName === 'string' ? item.tagName : undefined,
1865
+ type: typeof item.type === 'string' ? item.type : undefined,
1866
+ disabled: typeof item.disabled === 'boolean' ? item.disabled : undefined,
1867
+ selected: typeof item.selected === 'boolean' ? item.selected : undefined,
1868
+ };
1869
+ }
1870
+ function pickWorkflowTargetItems(payload, scope) {
1871
+ if (scope) {
1872
+ return pickPageStateScopeItems(payload, scope);
1873
+ }
1874
+ return [
1875
+ ...pickPageStateScopeItems(payload, 'buttons'),
1876
+ ...pickPageStateScopeItems(payload, 'inputs'),
1877
+ ...pickPageStateScopeItems(payload, 'modals'),
1878
+ ...pickPageStateScopeItems(payload, 'focused'),
1879
+ ];
1880
+ }
1881
+ function matchesWorkflowActionTarget(item, target) {
1882
+ return (equalsNormalized(item.testId, target.testId)
1883
+ && includesNormalized(item.text, target.textContains)
1884
+ && includesNormalized(item.label, target.labelContains)
1885
+ && includesNormalized(item.title, target.titleContains)
1886
+ && equalsNormalized(item.tagName, target.tagName)
1887
+ && equalsNormalized(item.type, target.type)
1888
+ && equalsOptionalBoolean(item.disabled, target.disabled)
1889
+ && equalsOptionalBoolean(item.selected, target.selected)
1890
+ && equalsOptionalBoolean(item.pressed, target.pressed)
1891
+ && equalsOptionalBoolean(item.expanded, target.expanded)
1892
+ && equalsOptionalBoolean(item.readOnly, target.readOnly)
1893
+ && equalsOptionalBoolean(item.required, target.requiredField)
1894
+ && (typeof item.elementRef === 'string' || typeof item.selector === 'string'));
1895
+ }
1896
+ function summarizeWorkflowTargetMatcher(target) {
1897
+ return {
1898
+ scope: target.scope,
1899
+ selector: target.selector,
1900
+ elementRef: target.elementRef,
1901
+ testId: target.testId,
1902
+ textContains: target.textContains,
1903
+ labelContains: target.labelContains,
1904
+ titleContains: target.titleContains,
1905
+ tagName: target.tagName,
1906
+ type: target.type,
1907
+ disabled: target.disabled,
1908
+ selected: target.selected,
1909
+ pressed: target.pressed,
1910
+ expanded: target.expanded,
1911
+ readOnly: target.readOnly,
1912
+ requiredField: target.requiredField,
1913
+ };
1914
+ }
1915
+ async function resolveWorkflowActionTarget(sessionId, target, capturePageState, existingCapture) {
1916
+ if (!target) {
1917
+ return {
1918
+ resolution: {
1919
+ strategy: 'none',
1920
+ },
1921
+ };
1922
+ }
1923
+ if (target.elementRef || target.selector) {
1924
+ return {
1925
+ target: {
1926
+ elementRef: target.elementRef,
1927
+ selector: target.selector,
1928
+ tabId: target.tabId,
1929
+ frameId: target.frameId,
1930
+ url: target.url,
1931
+ },
1932
+ resolution: {
1933
+ strategy: target.elementRef ? 'elementRef' : 'selector',
1934
+ matcher: summarizeWorkflowTargetMatcher(target),
1935
+ },
1936
+ };
1937
+ }
1938
+ const capture = existingCapture ?? await capturePageState(sessionId, {
1939
+ includeButtons: target.scope ? target.scope === 'buttons' : true,
1940
+ includeInputs: target.scope ? target.scope === 'inputs' : true,
1941
+ includeModals: target.scope ? target.scope === 'modals' : true,
1942
+ maxItems: 100,
1943
+ maxTextLength: 120,
1944
+ });
1945
+ const candidates = pickWorkflowTargetItems(capture.payload, target.scope)
1946
+ .filter((item) => matchesWorkflowActionTarget(item, target));
1947
+ if (candidates.length === 0) {
1948
+ throw new WorkflowTargetResolutionError('workflow_target_not_found', 'No interactive element matched the workflow target.', {
1949
+ matcher: summarizeWorkflowTargetMatcher(target),
1950
+ searchedScope: target.scope ?? 'all-interactive',
1951
+ sampledCandidates: pickWorkflowTargetItems(capture.payload, target.scope)
1952
+ .slice(0, 5)
1953
+ .map((item) => describeWorkflowTargetCandidate(item)),
1954
+ });
1955
+ }
1956
+ if (candidates.length > 1) {
1957
+ throw new WorkflowTargetResolutionError('workflow_target_ambiguous', `Workflow target matched ${candidates.length} elements; refine the matcher.`, {
1958
+ matcher: summarizeWorkflowTargetMatcher(target),
1959
+ matchedCandidateCount: candidates.length,
1960
+ sampledCandidates: candidates.slice(0, 5).map((item) => describeWorkflowTargetCandidate(item)),
1961
+ });
1962
+ }
1963
+ const candidate = candidates[0];
1964
+ return {
1965
+ target: {
1966
+ elementRef: typeof candidate.elementRef === 'string' ? candidate.elementRef : undefined,
1967
+ selector: typeof candidate.selector === 'string' ? candidate.selector : undefined,
1968
+ tabId: target.tabId,
1969
+ frameId: target.frameId,
1970
+ url: target.url,
1971
+ },
1972
+ resolution: {
1973
+ strategy: typeof candidate.elementRef === 'string' ? 'semantic_elementRef' : 'semantic_selector',
1974
+ matcher: summarizeWorkflowTargetMatcher(target),
1975
+ matchedCandidateCount: candidates.length,
1976
+ matched: describeWorkflowTargetCandidate(candidate),
1977
+ },
1978
+ pageCapture: capture,
1979
+ };
1980
+ }
1981
+ function createWorkflowStepId(step, index) {
1982
+ return step.id ?? `step_${index + 1}`;
1983
+ }
1984
+ async function captureWorkflowPageState(sessionId, capturePageState, mode) {
1985
+ const maxItems = mode === 'fast' ? 12 : 20;
1986
+ const maxTextLength = mode === 'fast' ? 60 : 80;
1987
+ return capturePageState(sessionId, {
1988
+ includeButtons: true,
1989
+ includeInputs: true,
1990
+ includeModals: true,
1991
+ maxItems,
1992
+ maxTextLength,
1993
+ });
1994
+ }
1995
+ function normalizeWorkflowError(error) {
1996
+ if (error instanceof WorkflowTargetResolutionError) {
1997
+ return {
1998
+ code: error.code,
1999
+ message: `${error.message} ${JSON.stringify(error.details)}`,
2000
+ };
2001
+ }
2002
+ if (error instanceof z.ZodError) {
2003
+ return {
2004
+ code: 'invalid_workflow_step',
2005
+ message: error.issues.map((issue) => issue.message).join('; '),
2006
+ };
2007
+ }
2008
+ if (error instanceof Error) {
2009
+ return {
2010
+ code: 'workflow_step_failed',
2011
+ message: error.message,
2012
+ };
2013
+ }
2014
+ return {
2015
+ code: 'workflow_step_failed',
2016
+ message: 'Unknown workflow step failure',
2017
+ };
2018
+ }
2019
+ function resolveWorkflowRecommendedAction(error) {
2020
+ if (!error) {
2021
+ return undefined;
2022
+ }
2023
+ if (error.code === LIVE_SESSION_DISCONNECTED_CODE
2024
+ || error.message.includes(LIVE_SESSION_DISCONNECTED_CODE)
2025
+ || error.message.toLowerCase().includes('transport closed')) {
2026
+ return 'reconnect_session';
2027
+ }
2028
+ if (error.code === 'target_not_found') {
2029
+ return 'inspect_page_state';
2030
+ }
2031
+ if (error.code === 'click_intercepted') {
2032
+ return 'retry_step';
2033
+ }
2034
+ if (error.code === 'workflow_target_ambiguous') {
2035
+ return 'refine_target';
2036
+ }
2037
+ if (error.code === 'workflow_target_not_found') {
2038
+ return 'inspect_page_state';
2039
+ }
2040
+ if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
2041
+ return 'inspect_page_state';
2042
+ }
2043
+ return undefined;
2044
+ }
2045
+ function resolveWorkflowFailureSelector(step, stepResultTarget) {
2046
+ if (step.kind === 'action') {
2047
+ if (typeof step.target?.selector === 'string' && step.target.selector.trim().length > 0) {
2048
+ return step.target.selector.trim();
2049
+ }
2050
+ const actionTarget = isRecord(stepResultTarget?.actionTarget) ? stepResultTarget?.actionTarget : undefined;
2051
+ if (typeof actionTarget?.selector === 'string' && actionTarget.selector.trim().length > 0) {
2052
+ return actionTarget.selector.trim();
2053
+ }
2054
+ const resolution = isRecord(stepResultTarget?.resolution) ? stepResultTarget?.resolution : undefined;
2055
+ const matched = isRecord(resolution?.matched) ? resolution.matched : undefined;
2056
+ if (typeof matched?.selector === 'string' && matched.selector.trim().length > 0) {
2057
+ return matched.selector.trim();
2058
+ }
2059
+ }
2060
+ return undefined;
2061
+ }
934
2062
  function asStringArray(value, maxItems) {
935
2063
  if (!Array.isArray(value)) {
936
2064
  return [];
@@ -1027,6 +2155,105 @@ function ensureCaptureSuccess(result, sessionId) {
1027
2155
  }
1028
2156
  return result.payload ?? {};
1029
2157
  }
2158
+ function normalizeSnapshotResponsePayload(payload, options) {
2159
+ const snapshotRecord = structuredClone(payload);
2160
+ const snapshotRoot = snapshotRecord.snapshot;
2161
+ if (typeof snapshotRoot === 'object' && snapshotRoot !== null) {
2162
+ const snapshotObject = snapshotRoot;
2163
+ if (!options.includeDom) {
2164
+ delete snapshotObject.dom;
2165
+ }
2166
+ if (!options.includeStyles) {
2167
+ delete snapshotObject.styles;
2168
+ }
2169
+ }
2170
+ const png = snapshotRecord.png;
2171
+ if (!options.includePngDataUrl && typeof png === 'object' && png !== null) {
2172
+ delete png.dataUrl;
2173
+ }
2174
+ return snapshotRecord;
2175
+ }
2176
+ function resolveFailureEvidenceCaptureOptions(input) {
2177
+ const raw = isRecord(input.captureOnFailure) ? input.captureOnFailure : undefined;
2178
+ const enabled = raw !== undefined ? raw.enabled !== false : false;
2179
+ const mode = raw?.mode === 'png' || raw?.mode === 'both' || raw?.mode === 'dom' ? raw.mode : 'dom';
2180
+ const styleMode = raw?.styleMode === 'computed-full' || raw?.styleMode === 'computed-lite'
2181
+ ? raw.styleMode
2182
+ : 'computed-lite';
2183
+ return {
2184
+ enabled,
2185
+ selector: typeof raw?.selector === 'string' && raw.selector.trim().length > 0 ? raw.selector.trim() : undefined,
2186
+ mode,
2187
+ styleMode,
2188
+ explicitStyleMode: raw?.styleMode === 'computed-full' || raw?.styleMode === 'computed-lite',
2189
+ maxDepth: resolveCaptureDepth(raw?.maxDepth, 3),
2190
+ maxBytes: resolveCaptureBytes(raw?.maxBytes, 50_000),
2191
+ maxAncestors: resolveCaptureAncestors(raw?.maxAncestors, 4),
2192
+ includeDom: typeof raw?.includeDom === 'boolean' ? raw.includeDom : mode !== 'png',
2193
+ includeStyles: typeof raw?.includeStyles === 'boolean' ? raw.includeStyles : mode !== 'png',
2194
+ includePngDataUrl: typeof raw?.includePngDataUrl === 'boolean' ? raw.includePngDataUrl : mode !== 'png',
2195
+ };
2196
+ }
2197
+ function resolveWorkflowFailurePolicy(step, stopOnFailure) {
2198
+ const raw = isRecord(step.onFailure) ? step.onFailure : undefined;
2199
+ const strategy = raw?.strategy === 'continue' || raw?.strategy === 'retry_once' || raw?.strategy === 'stop'
2200
+ ? raw.strategy
2201
+ : stopOnFailure === false
2202
+ ? 'continue'
2203
+ : 'stop';
2204
+ const captureRaw = raw && isRecord(raw.capture)
2205
+ ? {
2206
+ captureOnFailure: {
2207
+ ...raw.capture,
2208
+ enabled: raw.capture.enabled ?? true,
2209
+ },
2210
+ }
2211
+ : undefined;
2212
+ return {
2213
+ strategy,
2214
+ captureOptions: captureRaw ? resolveFailureEvidenceCaptureOptions(captureRaw) : undefined,
2215
+ };
2216
+ }
2217
+ async function captureFailureSnapshot(captureClient, sessionId, selector, options) {
2218
+ if (!options.enabled) {
2219
+ return undefined;
2220
+ }
2221
+ try {
2222
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
2223
+ selector: options.selector ?? selector,
2224
+ trigger: 'error',
2225
+ mode: options.mode,
2226
+ styleMode: options.styleMode,
2227
+ explicitStyleMode: options.explicitStyleMode,
2228
+ maxDepth: options.maxDepth,
2229
+ maxBytes: options.maxBytes,
2230
+ maxAncestors: options.maxAncestors,
2231
+ includeDom: options.includeDom,
2232
+ includeStyles: options.includeStyles,
2233
+ includePngDataUrl: options.includePngDataUrl,
2234
+ llmRequested: true,
2235
+ }, 5_000);
2236
+ const payload = ensureCaptureSuccess(capture, sessionId);
2237
+ return {
2238
+ captured: true,
2239
+ limitsApplied: {
2240
+ maxBytes: options.maxBytes,
2241
+ truncated: capture.truncated ?? false,
2242
+ },
2243
+ snapshot: normalizeSnapshotResponsePayload(payload, options),
2244
+ };
2245
+ }
2246
+ catch (error) {
2247
+ const normalized = normalizeCaptureError(sessionId, error);
2248
+ return {
2249
+ captured: false,
2250
+ error: normalized.message,
2251
+ };
2252
+ }
2253
+ }
2254
+ async function captureFailureEvidence(captureClient, sessionId, request, options) {
2255
+ return captureFailureSnapshot(captureClient, sessionId, request.target?.selector, options);
2256
+ }
1030
2257
  export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1031
2258
  return {
1032
2259
  list_sessions: async (input) => {
@@ -1116,6 +2343,88 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1116
2343
  sessions: bytePage.items,
1117
2344
  };
1118
2345
  },
2346
+ get_live_session_health: async (input) => {
2347
+ const db = getDb();
2348
+ const sessionId = getSessionId(input);
2349
+ if (!sessionId) {
2350
+ throw new Error('sessionId is required');
2351
+ }
2352
+ const session = db
2353
+ .prepare(`
2354
+ SELECT
2355
+ session_id,
2356
+ created_at,
2357
+ paused_at,
2358
+ ended_at,
2359
+ tab_id,
2360
+ window_id,
2361
+ url_start,
2362
+ url_last,
2363
+ viewport_w,
2364
+ viewport_h,
2365
+ dpr,
2366
+ safe_mode,
2367
+ pinned
2368
+ FROM sessions
2369
+ WHERE session_id = ?
2370
+ LIMIT 1
2371
+ `)
2372
+ .get(sessionId);
2373
+ if (!session) {
2374
+ throw new Error(`Session not found: ${sessionId}`);
2375
+ }
2376
+ const connection = getSessionConnectionState?.(sessionId);
2377
+ const now = Date.now();
2378
+ const lastSeenAt = connection?.connected
2379
+ ? connection.lastHeartbeatAt
2380
+ : connection?.disconnectedAt ?? session.ended_at ?? session.paused_at ?? session.created_at;
2381
+ const staleForMs = lastSeenAt ? Math.max(0, now - lastSeenAt) : undefined;
2382
+ return {
2383
+ ...createBaseResponse(sessionId),
2384
+ limitsApplied: {
2385
+ maxResults: 1,
2386
+ truncated: false,
2387
+ },
2388
+ session: {
2389
+ sessionId: session.session_id,
2390
+ createdAt: session.created_at,
2391
+ pausedAt: session.paused_at ?? undefined,
2392
+ endedAt: session.ended_at ?? undefined,
2393
+ status: session.ended_at ? 'ended' : session.paused_at ? 'paused' : 'active',
2394
+ tabId: session.tab_id ?? undefined,
2395
+ windowId: session.window_id ?? undefined,
2396
+ urlStart: session.url_start ?? undefined,
2397
+ urlLast: session.url_last ?? undefined,
2398
+ viewport: session.viewport_w !== null && session.viewport_h !== null
2399
+ ? {
2400
+ width: session.viewport_w,
2401
+ height: session.viewport_h,
2402
+ }
2403
+ : undefined,
2404
+ dpr: session.dpr ?? undefined,
2405
+ safeMode: session.safe_mode === 1,
2406
+ pinned: session.pinned === 1,
2407
+ },
2408
+ liveConnection: connection
2409
+ ? {
2410
+ connected: connection.connected,
2411
+ connectedAt: connection.connectedAt,
2412
+ lastHeartbeatAt: connection.lastHeartbeatAt,
2413
+ disconnectedAt: connection.disconnectedAt,
2414
+ disconnectReason: connection.disconnectReason,
2415
+ staleForMs,
2416
+ }
2417
+ : {
2418
+ connected: false,
2419
+ staleForMs,
2420
+ },
2421
+ recommendedAction: connection?.connected
2422
+ ? 'ready'
2423
+ : session.ended_at
2424
+ ? 'start_new_session'
2425
+ : 'reconnect_extension',
2426
+ };
2427
+ },
1119
2428
  get_session_summary: async (input) => {
1120
2429
  const db = getDb();
1121
2430
  const sessionId = getSessionId(input);
@@ -2348,9 +3657,193 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
2348
3657
  chunkBase64: encoding === 'base64' ? chunkBuffer.toString('base64') : undefined,
2349
3658
  };
2350
3659
  },
3660
+ list_automation_runs: async (input) => {
3661
+ const db = getDb();
3662
+ const sessionId = getSessionId(input);
3663
+ if (!sessionId) {
3664
+ throw new Error('sessionId is required');
3665
+ }
3666
+ const status = normalizeOptionalString(input.status);
3667
+ const action = normalizeOptionalString(input.action);
3668
+ const traceId = normalizeOptionalString(input.traceId);
3669
+ const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
3670
+ const offset = resolveOffset(input.offset);
3671
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
3672
+ const where = ['r.session_id = ?'];
3673
+ const params = [sessionId];
3674
+ if (status) {
3675
+ where.push('r.status = ?');
3676
+ params.push(status);
3677
+ }
3678
+ if (action) {
3679
+ where.push('r.action = ?');
3680
+ params.push(action);
3681
+ }
3682
+ if (traceId) {
3683
+ where.push('r.trace_id = ?');
3684
+ params.push(traceId);
3685
+ }
3686
+ const rows = db.prepare(`SELECT
3687
+ r.run_id,
3688
+ r.session_id,
3689
+ r.trace_id,
3690
+ r.action,
3691
+ r.tab_id,
3692
+ r.selector,
3693
+ r.status,
3694
+ r.started_at,
3695
+ r.completed_at,
3696
+ r.stop_reason,
3697
+ r.target_summary_json,
3698
+ r.failure_json,
3699
+ r.redaction_json,
3700
+ r.created_at,
3701
+ r.updated_at,
3702
+ COALESCE(step_stats.step_count, 0) AS step_count,
3703
+ step_stats.last_step_at
3704
+ FROM automation_runs r
3705
+ LEFT JOIN (
3706
+ SELECT
3707
+ run_id,
3708
+ COUNT(*) AS step_count,
3709
+ MAX(COALESCE(finished_at, started_at, created_at)) AS last_step_at
3710
+ FROM automation_steps
3711
+ GROUP BY run_id
3712
+ ) step_stats ON step_stats.run_id = r.run_id
3713
+ WHERE ${where.join(' AND ')}
3714
+ ORDER BY r.started_at DESC, r.run_id DESC
3715
+ LIMIT ? OFFSET ?`).all(...params, limit + 1, offset);
3716
+ const truncatedByLimit = rows.length > limit;
3717
+ const runs = rows.slice(0, limit).map((row) => mapAutomationRunRecord(row));
3718
+ const bytePage = applyByteBudget(runs, maxResponseBytes);
3719
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
3720
+ return {
3721
+ ...createBaseResponse(sessionId),
3722
+ limitsApplied: {
3723
+ maxResults: limit,
3724
+ truncated,
3725
+ },
3726
+ filtersApplied: {
3727
+ sessionId,
3728
+ status,
3729
+ action,
3730
+ traceId,
3731
+ },
3732
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
3733
+ responseBytes: bytePage.responseBytes,
3734
+ runs: bytePage.items,
3735
+ };
3736
+ },
3737
+ get_automation_run: async (input) => {
3738
+ const db = getDb();
3739
+ const sessionId = getSessionId(input);
3740
+ if (!sessionId) {
3741
+ throw new Error('sessionId is required');
3742
+ }
3743
+ const runId = normalizeOptionalString(input.runId);
3744
+ if (!runId) {
3745
+ throw new Error('runId is required');
3746
+ }
3747
+ const stepLimit = resolveLimit(input.stepLimit, DEFAULT_LIST_LIMIT);
3748
+ const stepOffset = resolveOffset(input.stepOffset);
3749
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
3750
+ const run = db.prepare(`SELECT
3751
+ r.run_id,
3752
+ r.session_id,
3753
+ r.trace_id,
3754
+ r.action,
3755
+ r.tab_id,
3756
+ r.selector,
3757
+ r.status,
3758
+ r.started_at,
3759
+ r.completed_at,
3760
+ r.stop_reason,
3761
+ r.target_summary_json,
3762
+ r.failure_json,
3763
+ r.redaction_json,
3764
+ r.created_at,
3765
+ r.updated_at,
3766
+ COALESCE(step_stats.step_count, 0) AS step_count,
3767
+ step_stats.last_step_at
3768
+ FROM automation_runs r
3769
+ LEFT JOIN (
3770
+ SELECT
3771
+ run_id,
3772
+ COUNT(*) AS step_count,
3773
+ MAX(COALESCE(finished_at, started_at, created_at)) AS last_step_at
3774
+ FROM automation_steps
3775
+ GROUP BY run_id
3776
+ ) step_stats ON step_stats.run_id = r.run_id
3777
+ WHERE r.session_id = ? AND r.run_id = ?
3778
+ LIMIT 1`).get(sessionId, runId);
3779
+ if (!run) {
3780
+ throw new Error(`Automation run not found: ${runId}`);
3781
+ }
3782
+ const stepRows = db.prepare(`SELECT
3783
+ step_id,
3784
+ run_id,
3785
+ session_id,
3786
+ step_order,
3787
+ trace_id,
3788
+ action,
3789
+ selector,
3790
+ status,
3791
+ started_at,
3792
+ finished_at,
3793
+ duration_ms,
3794
+ tab_id,
3795
+ target_summary_json,
3796
+ redaction_json,
3797
+ failure_json,
3798
+ input_metadata_json,
3799
+ event_type,
3800
+ event_id,
3801
+ created_at,
3802
+ updated_at
3803
+ FROM automation_steps
3804
+ WHERE session_id = ? AND run_id = ?
3805
+ ORDER BY step_order ASC, created_at ASC
3806
+ LIMIT ? OFFSET ?`).all(sessionId, runId, stepLimit + 1, stepOffset);
3807
+ const truncatedByLimit = stepRows.length > stepLimit;
3808
+ const steps = stepRows.slice(0, stepLimit).map((row) => mapAutomationStepRecord(row));
3809
+ const bytePage = applyByteBudget(steps, maxResponseBytes);
3810
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
3811
+ return {
3812
+ ...createBaseResponse(sessionId),
3813
+ limitsApplied: {
3814
+ maxResults: stepLimit,
3815
+ truncated,
3816
+ },
3817
+ run: mapAutomationRunRecord(run),
3818
+ steps: bytePage.items,
3819
+ pagination: buildOffsetPagination(stepOffset, bytePage.items.length, truncated, maxResponseBytes),
3820
+ responseBytes: bytePage.responseBytes,
3821
+ };
3822
+ },
2351
3823
  };
2352
3824
  }
2353
3825
  export function createV2ToolHandlers(captureClient) {
3826
+ const capturePageState = async (sessionId, input) => {
3827
+ const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
3828
+ const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
3829
+ const includeButtons = input.includeButtons !== false;
3830
+ const includeInputs = input.includeInputs !== false;
3831
+ const includeModals = input.includeModals !== false;
3832
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
3833
+ maxItems,
3834
+ maxTextLength,
3835
+ includeButtons,
3836
+ includeInputs,
3837
+ includeModals,
3838
+ }, 4_000);
3839
+ return {
3840
+ limitsApplied: {
3841
+ maxResults: maxItems,
3842
+ truncated: capture.truncated ?? false,
3843
+ },
3844
+ payload: ensureCaptureSuccess(capture, sessionId),
3845
+ };
3846
+ };
2354
3847
  return {
2355
3848
  get_dom_subtree: async (input) => {
2356
3849
  const sessionId = getSessionId(input);
@@ -2445,6 +3938,345 @@ export function createV2ToolHandlers(captureClient) {
2445
3938
  ...ensureCaptureSuccess(capture, sessionId),
2446
3939
  };
2447
3940
  },
3941
+ get_page_state: async (input) => {
3942
+ const sessionId = getSessionId(input);
3943
+ if (!sessionId) {
3944
+ throw new Error('sessionId is required');
3945
+ }
3946
+ const capture = await capturePageState(sessionId, input);
3947
+ return {
3948
+ ...createBaseResponse(sessionId),
3949
+ limitsApplied: capture.limitsApplied,
3950
+ ...capture.payload,
3951
+ };
3952
+ },
3953
+ get_interactive_elements: async (input) => {
3954
+ const sessionId = getSessionId(input);
3955
+ if (!sessionId) {
3956
+ throw new Error('sessionId is required');
3957
+ }
3958
+ const kinds = resolveInteractiveKinds(input.kinds);
3959
+ const normalizedInput = {
3960
+ ...input,
3961
+ includeButtons: kinds.includes('buttons'),
3962
+ includeInputs: kinds.includes('inputs'),
3963
+ includeModals: kinds.includes('modals'),
3964
+ };
3965
+ const capture = await capturePageState(sessionId, normalizedInput);
3966
+ const refs = collectInteractiveElementRefs(capture.payload, kinds, capture.limitsApplied.maxResults);
3967
+ return {
3968
+ ...createBaseResponse(sessionId),
3969
+ limitsApplied: {
3970
+ maxResults: capture.limitsApplied.maxResults,
3971
+ truncated: capture.limitsApplied.truncated || refs.length >= capture.limitsApplied.maxResults,
3972
+ },
3973
+ kinds,
3974
+ refs,
3975
+ page: {
3976
+ url: capture.payload.url,
3977
+ title: capture.payload.title,
3978
+ language: capture.payload.language,
3979
+ viewport: capture.payload.viewport,
3980
+ },
3981
+ pageSummary: typeof capture.payload.summary === 'object' && capture.payload.summary !== null
3982
+ ? capture.payload.summary
3983
+ : undefined,
3984
+ };
3985
+ },
3986
+ set_viewport: async (input) => {
3987
+ const sessionId = getSessionId(input);
3988
+ if (!sessionId) {
3989
+ throw new Error('sessionId is required');
3990
+ }
3991
+ const width = resolveViewportDimension(input.width, 'width');
3992
+ const height = resolveViewportDimension(input.height, 'height');
3993
+ const capture = await executeLiveCapture(captureClient, sessionId, 'SET_VIEWPORT', {
3994
+ width,
3995
+ height,
3996
+ }, 5_000);
3997
+ return {
3998
+ ...createBaseResponse(sessionId),
3999
+ limitsApplied: {
4000
+ maxResults: 1,
4001
+ truncated: capture.truncated ?? false,
4002
+ },
4003
+ ...ensureCaptureSuccess(capture, sessionId),
4004
+ };
4005
+ },
4006
+ assert_page_state: async (input) => {
4007
+ const sessionId = getSessionId(input);
4008
+ if (!sessionId) {
4009
+ throw new Error('sessionId is required');
4010
+ }
4011
+ const matcher = resolvePageStateMatcher(input);
4012
+ const capture = await capturePageState(sessionId, input);
4013
+ const assertion = evaluatePageStateAssertion(capture.payload, matcher);
4014
+ return {
4015
+ ...createBaseResponse(sessionId),
4016
+ limitsApplied: capture.limitsApplied,
4017
+ matcher,
4018
+ matched: assertion.matched,
4019
+ matchCount: assertion.matchCount,
4020
+ expectedCount: assertion.expectedCount,
4021
+ sampledMatches: assertion.sampledMatches,
4022
+ pageSummary: assertion.summary,
4023
+ page: {
4024
+ url: capture.payload.url,
4025
+ title: capture.payload.title,
4026
+ language: capture.payload.language,
4027
+ viewport: capture.payload.viewport,
4028
+ },
4029
+ };
4030
+ },
4031
+ wait_for_page_state: async (input) => {
4032
+ const sessionId = getSessionId(input);
4033
+ if (!sessionId) {
4034
+ throw new Error('sessionId is required');
4035
+ }
4036
+ const waited = await waitForPageStateCondition(sessionId, input, capturePageState);
4037
+ return {
4038
+ ...createBaseResponse(sessionId),
4039
+ ...waited,
4040
+ };
4041
+ },
4042
+ run_ui_steps: async (input) => {
4043
+ const request = RunUIStepsSchema.parse(input);
4044
+ const workflowTraceId = createUIWorkflowTraceId();
4045
+ const workflowStartedAt = Date.now();
4046
+ const stepResults = [];
4047
+ let lastPageCapture;
4048
+ let failedStepId;
4049
+ let stoppedAtIndex = request.steps.length;
4050
+ let stateCaptureCount = 0;
4051
+ let failureCaptureCount = 0;
4052
+ let retryCount = 0;
4053
+ const workflowCapturePageState = async (sessionId, toolInput) => {
4054
+ stateCaptureCount += 1;
4055
+ return capturePageState(sessionId, toolInput);
4056
+ };
4057
+ for (const [index, step] of request.steps.entries()) {
4058
+ const stepId = createWorkflowStepId(step, index);
4059
+ const failurePolicy = resolveWorkflowFailurePolicy(step, request.stopOnFailure);
4060
+ let executionAttempts = 0;
4061
+ let finalStepResult;
4062
+ let stepFailed = false;
4063
+ while (true) {
4064
+ executionAttempts += 1;
4065
+ const startedAt = Date.now();
4066
+ const previousCapture = lastPageCapture;
4067
+ try {
4068
+ if (step.kind === 'action') {
4069
+ const resolvedTarget = await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
4070
+ const liveRequest = LiveUIActionRequestSchema.parse({
4071
+ action: step.action,
4072
+ target: resolvedTarget.target,
4073
+ traceId: step.traceId ?? `${workflowTraceId}:${stepId}`,
4074
+ ...(step.input ? { input: step.input } : {}),
4075
+ });
4076
+ const capture = await executeLiveCapture(captureClient, request.sessionId, 'EXECUTE_UI_ACTION', liveRequest, 5_000);
4077
+ const payload = ensureCaptureSuccess(capture, request.sessionId);
4078
+ const actionResult = payload;
4079
+ const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
4080
+ let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
4081
+ if (!failed && request.mode === 'fast') {
4082
+ await sleep(75);
4083
+ currentCapture = await captureWorkflowPageState(request.sessionId, workflowCapturePageState, request.mode);
4084
+ }
4085
+ lastPageCapture = currentCapture;
4086
+ finalStepResult = {
4087
+ id: stepId,
4088
+ kind: step.kind,
4089
+ status: failed ? 'failed' : 'succeeded',
4090
+ durationMs: Math.max(0, Date.now() - startedAt),
4091
+ action: step.action,
4092
+ traceId: actionResult.traceId,
4093
+ target: {
4094
+ resolution: resolvedTarget.resolution,
4095
+ actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
4096
+ ? actionResult.target
4097
+ : undefined,
4098
+ },
4099
+ error: failed && actionResult.failureReason
4100
+ ? {
4101
+ code: actionResult.failureReason.code,
4102
+ message: actionResult.failureReason.message,
4103
+ }
4104
+ : undefined,
4105
+ pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
4106
+ };
4107
+ }
4108
+ else if (step.kind === 'waitFor') {
4109
+ const waitInput = {
4110
+ ...step.matcher,
4111
+ timeoutMs: step.matcher.timeoutMs ?? request.defaultTimeoutMs,
4112
+ pollIntervalMs: step.matcher.pollIntervalMs ?? request.defaultPollIntervalMs,
4113
+ };
4114
+ const waited = await waitForPageStateConditionDetailed(request.sessionId, waitInput, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
4115
+ lastPageCapture = waited.lastCapture ?? lastPageCapture;
4116
+ finalStepResult = {
4117
+ id: stepId,
4118
+ kind: step.kind,
4119
+ status: waited.matched ? 'succeeded' : 'failed',
4120
+ durationMs: Math.max(0, Date.now() - startedAt),
4121
+ matcher: waited.matcher,
4122
+ matchCount: waited.matchCount,
4123
+ waitedMs: waited.waitedMs,
4124
+ attempts: waited.attempts,
4125
+ error: waited.matched
4126
+ ? undefined
4127
+ : {
4128
+ code: 'page_state_not_matched',
4129
+ message: 'Workflow wait step timed out before the requested page state appeared.',
4130
+ },
4131
+ pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
4132
+ };
4133
+ }
4134
+ else {
4135
+ const capture = request.mode === 'fast' && lastPageCapture
4136
+ ? lastPageCapture
4137
+ : await workflowCapturePageState(request.sessionId, step.matcher);
4138
+ const assertion = evaluatePageStateAssertion(capture.payload, resolvePageStateMatcher(step.matcher));
4139
+ lastPageCapture = capture;
4140
+ finalStepResult = {
4141
+ id: stepId,
4142
+ kind: step.kind,
4143
+ status: assertion.matched ? 'succeeded' : 'failed',
4144
+ durationMs: Math.max(0, Date.now() - startedAt),
4145
+ matcher: step.matcher,
4146
+ matchCount: assertion.matchCount,
4147
+ error: assertion.matched
4148
+ ? undefined
4149
+ : {
4150
+ code: 'page_state_assertion_failed',
4151
+ message: 'Workflow assert step did not match the requested page state.',
4152
+ },
4153
+ pageChangeSummary: createPageChangeSummary(previousCapture, capture),
4154
+ };
4155
+ }
4156
+ }
4157
+ catch (error) {
4158
+ const workflowError = error instanceof WorkflowTargetResolutionError ? error : undefined;
4159
+ finalStepResult = {
4160
+ id: stepId,
4161
+ kind: step.kind,
4162
+ status: 'failed',
4163
+ durationMs: Math.max(0, Date.now() - startedAt),
4164
+ action: step.kind === 'action' ? step.action : undefined,
4165
+ target: step.kind === 'action' && workflowError
4166
+ ? workflowError.details
4167
+ : undefined,
4168
+ matcher: step.kind === 'action' ? undefined : step.matcher,
4169
+ error: normalizeWorkflowError(error),
4170
+ };
4171
+ }
4172
+ stepFailed = finalStepResult.status === 'failed';
4173
+ if (stepFailed && failurePolicy.strategy === 'retry_once' && executionAttempts === 1) {
4174
+ retryCount += 1;
4175
+ await sleep(100);
4176
+ continue;
4177
+ }
4178
+ break;
4179
+ }
4180
+ finalStepResult.executionAttempts = executionAttempts;
4181
+ finalStepResult.failurePolicy = {
4182
+ strategy: failurePolicy.strategy,
4183
+ captureEnabled: Boolean(failurePolicy.captureOptions?.enabled),
4184
+ };
4185
+ finalStepResult.recommendedAction = resolveWorkflowRecommendedAction(finalStepResult.error);
4186
+ if (stepFailed && failurePolicy.captureOptions) {
4187
+ const evidence = await captureFailureSnapshot(captureClient, request.sessionId, resolveWorkflowFailureSelector(step, finalStepResult.target), failurePolicy.captureOptions);
4188
+ if (evidence) {
4189
+ failureCaptureCount += 1;
4190
+ finalStepResult.failureEvidence = evidence;
4191
+ }
4192
+ }
4193
+ stepResults.push(finalStepResult);
4194
+ if (stepFailed) {
4195
+ failedStepId ??= stepId;
4196
+ if (failurePolicy.strategy !== 'continue') {
4197
+ stoppedAtIndex = index + 1;
4198
+ break;
4199
+ }
4200
+ }
4201
+ }
4202
+ if (failedStepId && stoppedAtIndex < request.steps.length) {
4203
+ for (const [index, step] of request.steps.slice(stoppedAtIndex).entries()) {
4204
+ stepResults.push({
4205
+ id: createWorkflowStepId(step, stoppedAtIndex + index),
4206
+ kind: step.kind,
4207
+ status: 'skipped',
4208
+ durationMs: 0,
4209
+ action: step.kind === 'action' ? step.action : undefined,
4210
+ matcher: step.kind === 'action' ? undefined : step.matcher,
4211
+ pageChangeSummary: undefined,
4212
+ error: {
4213
+ code: 'workflow_stopped_early',
4214
+ message: `Skipped because workflow stopped after failed step "${failedStepId}".`,
4215
+ },
4216
+ });
4217
+ }
4218
+ }
4219
+ let finalPageSummary;
4220
+ let finalPage;
4221
+ let finalCaptureTruncated = false;
4222
+ try {
4223
+ const finalCapture = lastPageCapture ?? await captureWorkflowPageState(request.sessionId, workflowCapturePageState, request.mode);
4224
+ finalPageSummary =
4225
+ typeof finalCapture.payload.summary === 'object' && finalCapture.payload.summary !== null
4226
+ ? finalCapture.payload.summary
4227
+ : undefined;
4228
+ finalPage = {
4229
+ url: finalCapture.payload.url,
4230
+ title: finalCapture.payload.title,
4231
+ language: finalCapture.payload.language,
4232
+ viewport: finalCapture.payload.viewport,
4233
+ };
4234
+ finalCaptureTruncated = finalCapture.limitsApplied.truncated;
4235
+ }
4236
+ catch {
4237
+ finalPageSummary = undefined;
4238
+ finalPage = undefined;
4239
+ }
4240
+ const workflowFinishedAt = Date.now();
4241
+ const succeededSteps = stepResults.filter((step) => step.status === 'succeeded').length;
4242
+ const failedSteps = stepResults.filter((step) => step.status === 'failed').length;
4243
+ const skippedSteps = stepResults.filter((step) => step.status === 'skipped').length;
4244
+ const failedStep = failedStepId
4245
+ ? stepResults.find((step) => step.id === failedStepId && step.status === 'failed')
4246
+ : undefined;
4247
+ return {
4248
+ ...createBaseResponse(request.sessionId),
4249
+ limitsApplied: {
4250
+ maxResults: request.steps.length,
4251
+ truncated: finalCaptureTruncated,
4252
+ },
4253
+ traceId: workflowTraceId,
4254
+ mode: request.mode,
4255
+ status: failedStepId ? 'failed' : 'succeeded',
4256
+ startedAt: workflowStartedAt,
4257
+ finishedAt: workflowFinishedAt,
4258
+ durationMs: Math.max(0, workflowFinishedAt - workflowStartedAt),
4259
+ requestedStepCount: request.steps.length,
4260
+ completedStepCount: succeededSteps,
4261
+ failedStepId,
4262
+ stoppedEarly: Boolean(failedStepId && stoppedAtIndex < request.steps.length),
4263
+ recommendedAction: failedStep?.recommendedAction,
4264
+ stepCounts: {
4265
+ succeeded: succeededSteps,
4266
+ failed: failedSteps,
4267
+ skipped: skippedSteps,
4268
+ },
4269
+ workflowDiagnostics: {
4270
+ retryCount,
4271
+ stateCaptureCount,
4272
+ failureCaptureCount,
4273
+ usedCachedState: request.mode === 'fast',
4274
+ },
4275
+ steps: stepResults,
4276
+ finalPageSummary,
4277
+ finalPage,
4278
+ };
4279
+ },
2448
4280
  capture_ui_snapshot: async (input) => {
2449
4281
  const sessionId = getSessionId(input);
2450
4282
  if (!sessionId) {
@@ -2482,21 +4314,11 @@ export function createV2ToolHandlers(captureClient) {
2482
4314
  llmRequested: true,
2483
4315
  }, 5_000);
2484
4316
  const payload = ensureCaptureSuccess(capture, sessionId);
2485
- const snapshotRecord = structuredClone(payload);
2486
- const snapshotRoot = snapshotRecord.snapshot;
2487
- if (typeof snapshotRoot === 'object' && snapshotRoot !== null) {
2488
- const snapshotObject = snapshotRoot;
2489
- if (!includeDom) {
2490
- delete snapshotObject.dom;
2491
- }
2492
- if (!includeStyles) {
2493
- delete snapshotObject.styles;
2494
- }
2495
- }
2496
- const png = snapshotRecord.png;
2497
- if (!includePngDataUrl && typeof png === 'object' && png !== null) {
2498
- delete png.dataUrl;
2499
- }
4317
+ const snapshotRecord = normalizeSnapshotResponsePayload(payload, {
4318
+ includeDom,
4319
+ includeStyles,
4320
+ includePngDataUrl,
4321
+ });
2500
4322
  return {
2501
4323
  ...createBaseResponse(sessionId),
2502
4324
  limitsApplied: {
@@ -2577,6 +4399,72 @@ export function createV2ToolHandlers(captureClient) {
2577
4399
  bufferStats: payload.bufferStats,
2578
4400
  };
2579
4401
  },
4402
+ execute_ui_action: async (input) => {
4403
+ const sessionId = getSessionId(input);
4404
+ if (!sessionId) {
4405
+ throw new Error('sessionId is required');
4406
+ }
4407
+ const actionInput = { ...input };
4408
+ delete actionInput.sessionId;
4409
+ delete actionInput.captureOnFailure;
4410
+ const request = LiveUIActionRequestSchema.parse(actionInput);
4411
+ const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
4412
+ const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
4413
+ const payload = ensureCaptureSuccess(capture, sessionId);
4414
+ const actionResult = payload;
4415
+ const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
4416
+ const failureEvidence = failed
4417
+ ? await captureFailureEvidence(captureClient, sessionId, request, failureCaptureOptions)
4418
+ : undefined;
4419
+ const postActionWaitInput = typeof input.waitForPageState === 'object' && input.waitForPageState !== null
4420
+ ? {
4421
+ ...input.waitForPageState,
4422
+ }
4423
+ : undefined;
4424
+ const postActionState = actionResult.status === 'succeeded' && postActionWaitInput
4425
+ ? await waitForPageStateCondition(sessionId, postActionWaitInput, capturePageState)
4426
+ : undefined;
4427
+ const evidenceTruncated = Boolean(failureEvidence
4428
+ && typeof failureEvidence === 'object'
4429
+ && failureEvidence !== null
4430
+ && typeof failureEvidence.limitsApplied?.truncated === 'boolean'
4431
+ && failureEvidence.limitsApplied.truncated);
4432
+ const target = typeof actionResult.target === 'object' && actionResult.target !== null
4433
+ ? actionResult.target
4434
+ : {};
4435
+ return {
4436
+ ...createBaseResponse(sessionId),
4437
+ limitsApplied: {
4438
+ maxResults: 1,
4439
+ truncated: (capture.truncated ?? false)
4440
+ || evidenceTruncated
4441
+ || Boolean(postActionState?.limitsApplied.truncated),
4442
+ },
4443
+ action: actionResult.action,
4444
+ status: actionResult.status,
4445
+ traceId: actionResult.traceId,
4446
+ startedAt: actionResult.startedAt,
4447
+ finishedAt: actionResult.finishedAt,
4448
+ durationMs: typeof actionResult.startedAt === 'number' && typeof actionResult.finishedAt === 'number'
4449
+ ? Math.max(0, actionResult.finishedAt - actionResult.startedAt)
4450
+ : undefined,
4451
+ actionResult,
4452
+ target,
4453
+ tabContext: {
4454
+ tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
4455
+ frameId: typeof target.frameId === 'number' ? target.frameId : 0,
4456
+ url: typeof target.url === 'string' ? target.url : undefined,
4457
+ },
4458
+ failureDetails: actionResult.failureReason,
4459
+ postActionEvidence: failureEvidence,
4460
+ postActionState,
4461
+ supportedScopes: {
4462
+ executionScope: actionResult.executionScope,
4463
+ topDocumentOnly: true,
4464
+ opensNewBrowserSession: false,
4465
+ },
4466
+ };
4467
+ },
2580
4468
  };
2581
4469
  }
2582
4470
  function isRecord(value) {