browser-debug-mcp-bridge 1.6.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.
Files changed (30) hide show
  1. package/README.md +25 -0
  2. package/apps/mcp-server/dist/db/automation-repository.js +199 -0
  3. package/apps/mcp-server/dist/db/automation-repository.js.map +1 -0
  4. package/apps/mcp-server/dist/db/connection.js +1 -5
  5. package/apps/mcp-server/dist/db/connection.js.map +1 -1
  6. package/apps/mcp-server/dist/db/events-repository.js +263 -14
  7. package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
  8. package/apps/mcp-server/dist/db/index.js +2 -0
  9. package/apps/mcp-server/dist/db/index.js.map +1 -1
  10. package/apps/mcp-server/dist/db/migrations.js +180 -0
  11. package/apps/mcp-server/dist/db/migrations.js.map +1 -1
  12. package/apps/mcp-server/dist/db/schema.js +93 -1
  13. package/apps/mcp-server/dist/db/schema.js.map +1 -1
  14. package/apps/mcp-server/dist/main.js +54 -4
  15. package/apps/mcp-server/dist/main.js.map +1 -1
  16. package/apps/mcp-server/dist/mcp/server.js +2860 -86
  17. package/apps/mcp-server/dist/mcp/server.js.map +1 -1
  18. package/apps/mcp-server/dist/mcp-bridge.js +46 -3
  19. package/apps/mcp-server/dist/mcp-bridge.js.map +1 -1
  20. package/apps/mcp-server/dist/retention.js +67 -4
  21. package/apps/mcp-server/dist/retention.js.map +1 -1
  22. package/apps/mcp-server/dist/runtime-paths.js +33 -0
  23. package/apps/mcp-server/dist/runtime-paths.js.map +1 -0
  24. package/apps/mcp-server/dist/websocket/messages.js +30 -0
  25. package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
  26. package/apps/mcp-server/dist/websocket/websocket-server.js +18 -0
  27. package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
  28. package/apps/mcp-server/package.json +2 -2
  29. package/package.json +17 -6
  30. package/scripts/mcp-start.cjs +201 -11
@@ -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',
@@ -27,6 +260,7 @@ const TOOL_SCHEMAS = {
27
260
  sinceMinutes: { type: 'number' },
28
261
  limit: { type: 'number' },
29
262
  offset: { type: 'number' },
263
+ maxResponseBytes: { type: 'number' },
30
264
  },
31
265
  },
32
266
  get_session_summary: {
@@ -44,6 +278,9 @@ const TOOL_SCHEMAS = {
44
278
  eventTypes: { type: 'array', items: { type: 'string' } },
45
279
  limit: { type: 'number' },
46
280
  offset: { type: 'number' },
281
+ responseProfile: { type: 'string' },
282
+ includePayload: { type: 'boolean' },
283
+ maxResponseBytes: { type: 'number' },
47
284
  },
48
285
  },
49
286
  get_navigation_history: {
@@ -53,6 +290,9 @@ const TOOL_SCHEMAS = {
53
290
  url: { type: 'string' },
54
291
  limit: { type: 'number' },
55
292
  offset: { type: 'number' },
293
+ responseProfile: { type: 'string' },
294
+ includePayload: { type: 'boolean' },
295
+ maxResponseBytes: { type: 'number' },
56
296
  },
57
297
  },
58
298
  get_console_events: {
@@ -63,6 +303,29 @@ const TOOL_SCHEMAS = {
63
303
  level: { type: 'string' },
64
304
  limit: { type: 'number' },
65
305
  offset: { type: 'number' },
306
+ responseProfile: { type: 'string' },
307
+ includePayload: { type: 'boolean' },
308
+ maxResponseBytes: { type: 'number' },
309
+ },
310
+ },
311
+ get_console_summary: {
312
+ type: 'object',
313
+ properties: {
314
+ sessionId: { type: 'string' },
315
+ url: { type: 'string' },
316
+ level: { type: 'string' },
317
+ sinceMinutes: { type: 'number' },
318
+ limit: { type: 'number' },
319
+ },
320
+ },
321
+ get_event_summary: {
322
+ type: 'object',
323
+ properties: {
324
+ sessionId: { type: 'string' },
325
+ url: { type: 'string' },
326
+ eventTypes: { type: 'array', items: { type: 'string' } },
327
+ sinceMinutes: { type: 'number' },
328
+ limit: { type: 'number' },
66
329
  },
67
330
  },
68
331
  get_error_fingerprints: {
@@ -72,6 +335,7 @@ const TOOL_SCHEMAS = {
72
335
  sinceMinutes: { type: 'number' },
73
336
  limit: { type: 'number' },
74
337
  offset: { type: 'number' },
338
+ maxResponseBytes: { type: 'number' },
75
339
  },
76
340
  },
77
341
  get_network_failures: {
@@ -83,6 +347,56 @@ const TOOL_SCHEMAS = {
83
347
  groupBy: { type: 'string' },
84
348
  limit: { type: 'number' },
85
349
  offset: { type: 'number' },
350
+ maxResponseBytes: { type: 'number' },
351
+ },
352
+ },
353
+ get_network_calls: {
354
+ type: 'object',
355
+ required: ['sessionId'],
356
+ properties: {
357
+ sessionId: { type: 'string' },
358
+ urlContains: { type: 'string' },
359
+ urlRegex: { type: 'string' },
360
+ method: { type: 'string' },
361
+ statusIn: { type: 'array', items: { type: 'number' } },
362
+ tabId: { type: 'number' },
363
+ timeFrom: { type: 'number' },
364
+ timeTo: { type: 'number' },
365
+ includeBodies: { type: 'boolean' },
366
+ limit: { type: 'number' },
367
+ offset: { type: 'number' },
368
+ maxResponseBytes: { type: 'number' },
369
+ },
370
+ },
371
+ wait_for_network_call: {
372
+ type: 'object',
373
+ required: ['sessionId', 'urlPattern'],
374
+ properties: {
375
+ sessionId: { type: 'string' },
376
+ urlPattern: { type: 'string' },
377
+ method: { type: 'string' },
378
+ timeoutMs: { type: 'number' },
379
+ includeBodies: { type: 'boolean' },
380
+ },
381
+ },
382
+ get_request_trace: {
383
+ type: 'object',
384
+ properties: {
385
+ sessionId: { type: 'string' },
386
+ requestId: { type: 'string' },
387
+ traceId: { type: 'string' },
388
+ includeBodies: { type: 'boolean' },
389
+ eventLimit: { type: 'number' },
390
+ },
391
+ },
392
+ get_body_chunk: {
393
+ type: 'object',
394
+ required: ['chunkRef'],
395
+ properties: {
396
+ chunkRef: { type: 'string' },
397
+ sessionId: { type: 'string' },
398
+ offset: { type: 'number' },
399
+ limit: { type: 'number' },
86
400
  },
87
401
  },
88
402
  get_element_refs: {
@@ -93,6 +407,7 @@ const TOOL_SCHEMAS = {
93
407
  selector: { type: 'string' },
94
408
  limit: { type: 'number' },
95
409
  offset: { type: 'number' },
410
+ maxResponseBytes: { type: 'number' },
96
411
  },
97
412
  },
98
413
  get_dom_subtree: {
@@ -130,6 +445,103 @@ const TOOL_SCHEMAS = {
130
445
  selector: { type: 'string' },
131
446
  },
132
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
+ },
133
545
  capture_ui_snapshot: {
134
546
  type: 'object',
135
547
  required: ['sessionId'],
@@ -142,6 +554,9 @@ const TOOL_SCHEMAS = {
142
554
  maxDepth: { type: 'number' },
143
555
  maxBytes: { type: 'number' },
144
556
  maxAncestors: { type: 'number' },
557
+ includeDom: { type: 'boolean' },
558
+ includeStyles: { type: 'boolean' },
559
+ includePngDataUrl: { type: 'boolean' },
145
560
  },
146
561
  },
147
562
  get_live_console_logs: {
@@ -155,7 +570,11 @@ const TOOL_SCHEMAS = {
155
570
  contains: { type: 'string' },
156
571
  sinceTs: { type: 'number' },
157
572
  includeRuntimeErrors: { type: 'boolean' },
573
+ dedupeWindowMs: { type: 'number' },
158
574
  limit: { type: 'number' },
575
+ responseProfile: { type: 'string' },
576
+ includeArgs: { type: 'boolean' },
577
+ maxResponseBytes: { type: 'number' },
159
578
  },
160
579
  },
161
580
  explain_last_failure: {
@@ -185,6 +604,7 @@ const TOOL_SCHEMAS = {
185
604
  untilTimestamp: { type: 'number' },
186
605
  limit: { type: 'number' },
187
606
  offset: { type: 'number' },
607
+ maxResponseBytes: { type: 'number' },
188
608
  },
189
609
  },
190
610
  get_snapshot_for_event: {
@@ -208,6 +628,191 @@ const TOOL_SCHEMAS = {
208
628
  encoding: { type: 'string' },
209
629
  },
210
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
+ },
211
816
  };
212
817
  const TOOL_DESCRIPTIONS = {
213
818
  list_sessions: 'List captured debugging sessions',
@@ -215,13 +820,25 @@ const TOOL_DESCRIPTIONS = {
215
820
  get_recent_events: 'Read recent events from a session',
216
821
  get_navigation_history: 'Read navigation events for a session',
217
822
  get_console_events: 'Read console events for a session',
823
+ get_console_summary: 'Summarize console volume and top repeated messages',
824
+ get_event_summary: 'Summarize event volume and type distribution',
218
825
  get_error_fingerprints: 'List aggregated error fingerprints',
219
826
  get_network_failures: 'List network failures and groupings',
827
+ get_network_calls: 'Query network calls with targeted filters and optional sanitized bodies',
828
+ wait_for_network_call: 'Wait for the next matching network call and return it deterministically',
829
+ get_request_trace: 'Get correlated UI/events/network chain by requestId or traceId',
830
+ get_body_chunk: 'Retrieve a chunk from a stored large body payload',
220
831
  get_element_refs: 'Get element references by selector',
221
832
  get_dom_subtree: 'Capture a bounded DOM subtree',
222
833
  get_dom_document: 'Capture full document as outline or html',
223
834
  get_computed_styles: 'Read computed CSS styles for an element',
224
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',
225
842
  capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
226
843
  get_live_console_logs: 'Read in-memory live console logs for a connected session',
227
844
  explain_last_failure: 'Explain the latest failure timeline',
@@ -229,6 +846,10 @@ const TOOL_DESCRIPTIONS = {
229
846
  list_snapshots: 'List snapshot metadata by session/time/trigger',
230
847
  get_snapshot_for_event: 'Find snapshot most related to an event',
231
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',
232
853
  };
233
854
  const ALL_TOOLS = Object.keys(TOOL_SCHEMAS);
234
855
  const DEFAULT_REDACTION_SUMMARY = {
@@ -239,9 +860,21 @@ const DEFAULT_REDACTION_SUMMARY = {
239
860
  const DEFAULT_LIST_LIMIT = 25;
240
861
  const DEFAULT_EVENT_LIMIT = 50;
241
862
  const MAX_LIMIT = 200;
863
+ const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024;
864
+ const MAX_RESPONSE_BYTES = 512 * 1024;
242
865
  const DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES = 64 * 1024;
243
866
  const MAX_SNAPSHOT_ASSET_CHUNK_BYTES = 256 * 1024;
867
+ const DEFAULT_BODY_CHUNK_BYTES = 64 * 1024;
868
+ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
869
+ const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
870
+ const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
871
+ const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
244
872
  const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
873
+ const NETWORK_CALL_SELECT_COLUMNS = `
874
+ request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class, response_size_est,
875
+ request_content_type, request_body_text, request_body_json, request_body_bytes, request_body_truncated, request_body_chunk_ref,
876
+ response_content_type, response_body_text, response_body_json, response_body_bytes, response_body_truncated, response_body_chunk_ref
877
+ `;
245
878
  const NETWORK_DOMAIN_GROUP_SQL = `
246
879
  CASE
247
880
  WHEN instr(replace(replace(url, 'https://', ''), 'http://', ''), '/') > 0
@@ -280,6 +913,62 @@ function resolveOffset(value) {
280
913
  const floored = Math.floor(value);
281
914
  return floored < 0 ? 0 : floored;
282
915
  }
916
+ function resolveResponseProfile(value) {
917
+ return value === 'compact' ? 'compact' : 'legacy';
918
+ }
919
+ function resolveMaxResponseBytes(value) {
920
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
921
+ return DEFAULT_MAX_RESPONSE_BYTES;
922
+ }
923
+ const floored = Math.floor(value);
924
+ if (floored < 1_024) {
925
+ return DEFAULT_MAX_RESPONSE_BYTES;
926
+ }
927
+ return Math.min(floored, MAX_RESPONSE_BYTES);
928
+ }
929
+ function estimateJsonBytes(value) {
930
+ return Buffer.byteLength(JSON.stringify(value), 'utf-8');
931
+ }
932
+ function applyByteBudget(items, maxResponseBytes) {
933
+ if (items.length === 0) {
934
+ return {
935
+ items: [],
936
+ responseBytes: 2, // []
937
+ truncatedByBytes: false,
938
+ };
939
+ }
940
+ const selected = [];
941
+ let usedBytes = 2; // []
942
+ let truncatedByBytes = false;
943
+ for (const item of items) {
944
+ const itemBytes = estimateJsonBytes(item);
945
+ const separatorBytes = selected.length > 0 ? 1 : 0; // comma
946
+ const nextBytes = usedBytes + separatorBytes + itemBytes;
947
+ if (nextBytes > maxResponseBytes && selected.length > 0) {
948
+ truncatedByBytes = true;
949
+ break;
950
+ }
951
+ selected.push(item);
952
+ usedBytes = nextBytes;
953
+ }
954
+ if (!truncatedByBytes && selected.length < items.length) {
955
+ truncatedByBytes = true;
956
+ }
957
+ return {
958
+ items: selected,
959
+ responseBytes: usedBytes,
960
+ truncatedByBytes,
961
+ };
962
+ }
963
+ function buildOffsetPagination(offset, returned, hasMore, maxResponseBytes) {
964
+ return {
965
+ offset,
966
+ returned,
967
+ hasMore,
968
+ nextOffset: hasMore ? offset + returned : null,
969
+ maxResponseBytes,
970
+ };
971
+ }
283
972
  function readJsonPayload(payloadJson) {
284
973
  try {
285
974
  const parsed = JSON.parse(payloadJson);
@@ -387,8 +1076,28 @@ function resolveLastUrl(payload) {
387
1076
  }
388
1077
  return undefined;
389
1078
  }
390
- function mapEventRecord(row) {
1079
+ function mapEventRecord(row, profile = 'legacy', options = {}) {
391
1080
  const payload = readJsonPayload(row.payload_json);
1081
+ if (profile === 'compact') {
1082
+ const compact = {
1083
+ eventId: row.event_id,
1084
+ sessionId: row.session_id,
1085
+ timestamp: row.ts,
1086
+ type: row.type,
1087
+ summary: describeEvent(row.type, payload),
1088
+ };
1089
+ if (row.type === 'console') {
1090
+ compact.level = typeof payload.level === 'string' ? payload.level : undefined;
1091
+ compact.message = typeof payload.message === 'string' ? payload.message : undefined;
1092
+ }
1093
+ if (row.type === 'nav') {
1094
+ compact.url = resolveLastUrl(payload);
1095
+ }
1096
+ if (options.includePayload === true) {
1097
+ compact.payload = payload;
1098
+ }
1099
+ return compact;
1100
+ }
392
1101
  return {
393
1102
  eventId: row.event_id,
394
1103
  sessionId: row.session_id,
@@ -456,6 +1165,153 @@ function resolveDurationMs(value, fallback, maxValue) {
456
1165
  }
457
1166
  return Math.min(floored, maxValue);
458
1167
  }
1168
+ function resolveBodyChunkBytes(value) {
1169
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1170
+ return DEFAULT_BODY_CHUNK_BYTES;
1171
+ }
1172
+ const floored = Math.floor(value);
1173
+ if (floored < 1) {
1174
+ return DEFAULT_BODY_CHUNK_BYTES;
1175
+ }
1176
+ return Math.min(floored, MAX_BODY_CHUNK_BYTES);
1177
+ }
1178
+ function resolveTimeoutMs(value, fallback, maxValue) {
1179
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1180
+ return fallback;
1181
+ }
1182
+ const floored = Math.floor(value);
1183
+ if (floored < 100) {
1184
+ return fallback;
1185
+ }
1186
+ return Math.min(floored, maxValue);
1187
+ }
1188
+ function normalizeHttpMethod(value) {
1189
+ if (typeof value !== 'string') {
1190
+ return undefined;
1191
+ }
1192
+ const normalized = value.trim().toUpperCase();
1193
+ return normalized.length > 0 ? normalized : undefined;
1194
+ }
1195
+ function normalizeOptionalString(value) {
1196
+ if (typeof value !== 'string') {
1197
+ return undefined;
1198
+ }
1199
+ const trimmed = value.trim();
1200
+ return trimmed.length > 0 ? trimmed : undefined;
1201
+ }
1202
+ function normalizeStatusIn(value) {
1203
+ if (!Array.isArray(value)) {
1204
+ return [];
1205
+ }
1206
+ const statuses = value
1207
+ .filter((entry) => typeof entry === 'number' && Number.isFinite(entry))
1208
+ .map((entry) => Math.floor(entry))
1209
+ .filter((entry) => entry >= 100 && entry <= 599);
1210
+ return Array.from(new Set(statuses));
1211
+ }
1212
+ function parseJsonOrUndefined(value) {
1213
+ if (!value) {
1214
+ return undefined;
1215
+ }
1216
+ try {
1217
+ return JSON.parse(value);
1218
+ }
1219
+ catch {
1220
+ return undefined;
1221
+ }
1222
+ }
1223
+ function compileSafeRegex(value) {
1224
+ if (!value) {
1225
+ return undefined;
1226
+ }
1227
+ try {
1228
+ return new RegExp(value);
1229
+ }
1230
+ catch {
1231
+ throw new Error('urlRegex must be a valid regular expression');
1232
+ }
1233
+ }
1234
+ function mapNetworkCallRecord(row, includeBodies) {
1235
+ const requestBodyJson = parseJsonOrUndefined(row.request_body_json);
1236
+ const responseBodyJson = parseJsonOrUndefined(row.response_body_json);
1237
+ return {
1238
+ requestId: row.request_id,
1239
+ sessionId: row.session_id,
1240
+ traceId: row.trace_id ?? undefined,
1241
+ tabId: row.tab_id ?? undefined,
1242
+ timestamp: row.ts_start,
1243
+ durationMs: row.duration_ms ?? undefined,
1244
+ method: row.method,
1245
+ url: row.url,
1246
+ origin: row.origin ?? undefined,
1247
+ status: row.status ?? undefined,
1248
+ initiator: row.initiator ?? undefined,
1249
+ errorType: classifyNetworkFailure(row.status, row.error_class),
1250
+ responseSizeEst: row.response_size_est ?? undefined,
1251
+ request: {
1252
+ contentType: row.request_content_type ?? undefined,
1253
+ bodyBytes: row.request_body_bytes ?? undefined,
1254
+ truncated: row.request_body_truncated === 1,
1255
+ bodyChunkRef: row.request_body_chunk_ref ?? undefined,
1256
+ bodyJson: includeBodies ? requestBodyJson : undefined,
1257
+ bodyText: includeBodies ? row.request_body_text ?? undefined : undefined,
1258
+ },
1259
+ response: {
1260
+ contentType: row.response_content_type ?? undefined,
1261
+ bodyBytes: row.response_body_bytes ?? undefined,
1262
+ truncated: row.response_body_truncated === 1,
1263
+ bodyChunkRef: row.response_body_chunk_ref ?? undefined,
1264
+ bodyJson: includeBodies ? responseBodyJson : undefined,
1265
+ bodyText: includeBodies ? row.response_body_text ?? undefined : undefined,
1266
+ },
1267
+ };
1268
+ }
1269
+ function mapBodyChunkRecord(row, offset, limit) {
1270
+ const fullBuffer = Buffer.from(row.body_text, 'utf-8');
1271
+ if (offset >= fullBuffer.byteLength) {
1272
+ return {
1273
+ chunkRef: row.chunk_ref,
1274
+ sessionId: row.session_id,
1275
+ requestId: row.request_id ?? undefined,
1276
+ traceId: row.trace_id ?? undefined,
1277
+ bodyKind: row.body_kind,
1278
+ contentType: row.content_type ?? undefined,
1279
+ totalBytes: fullBuffer.byteLength,
1280
+ offset,
1281
+ returnedBytes: 0,
1282
+ hasMore: false,
1283
+ nextOffset: null,
1284
+ chunkText: '',
1285
+ truncated: row.truncated === 1,
1286
+ createdAt: row.created_at,
1287
+ };
1288
+ }
1289
+ const chunkBuffer = fullBuffer.subarray(offset, Math.min(offset + limit, fullBuffer.byteLength));
1290
+ const returnedBytes = chunkBuffer.byteLength;
1291
+ const nextOffset = offset + returnedBytes;
1292
+ const hasMore = nextOffset < fullBuffer.byteLength;
1293
+ return {
1294
+ chunkRef: row.chunk_ref,
1295
+ sessionId: row.session_id,
1296
+ requestId: row.request_id ?? undefined,
1297
+ traceId: row.trace_id ?? undefined,
1298
+ bodyKind: row.body_kind,
1299
+ contentType: row.content_type ?? undefined,
1300
+ totalBytes: fullBuffer.byteLength,
1301
+ offset,
1302
+ returnedBytes,
1303
+ hasMore,
1304
+ nextOffset: hasMore ? nextOffset : null,
1305
+ chunkText: chunkBuffer.toString('utf-8'),
1306
+ truncated: row.truncated === 1,
1307
+ createdAt: row.created_at,
1308
+ };
1309
+ }
1310
+ function sleep(ms) {
1311
+ return new Promise((resolvePromise) => {
1312
+ setTimeout(resolvePromise, ms);
1313
+ });
1314
+ }
459
1315
  function normalizeAssetPath(pathValue) {
460
1316
  return pathValue.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, '');
461
1317
  }
@@ -500,6 +1356,56 @@ function mapSnapshotMetadata(row) {
500
1356
  createdAt: row.created_at,
501
1357
  };
502
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
+ }
503
1409
  function formatUrlPath(url) {
504
1410
  try {
505
1411
  const parsed = new URL(url);
@@ -600,6 +1506,559 @@ function resolveCaptureAncestors(value, fallback) {
600
1506
  }
601
1507
  return Math.min(floored, 8);
602
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
+ }
603
2062
  function asStringArray(value, maxItems) {
604
2063
  if (!Array.isArray(value)) {
605
2064
  return [];
@@ -615,6 +2074,39 @@ function resolveLiveConsoleLevels(value) {
615
2074
  .filter((entry) => LIVE_CONSOLE_LEVELS.has(entry));
616
2075
  return Array.from(new Set(levels));
617
2076
  }
2077
+ function asRecordArray(value) {
2078
+ if (!Array.isArray(value)) {
2079
+ return [];
2080
+ }
2081
+ return value.filter((entry) => typeof entry === 'object' && entry !== null);
2082
+ }
2083
+ function mapLiveConsoleLogRecord(log, profile, options = {}) {
2084
+ if (profile === 'compact') {
2085
+ const compact = {
2086
+ timestamp: typeof log.timestamp === 'number'
2087
+ ? log.timestamp
2088
+ : typeof log.ts === 'number'
2089
+ ? log.ts
2090
+ : undefined,
2091
+ level: typeof log.level === 'string' ? log.level : undefined,
2092
+ message: typeof log.message === 'string' ? log.message : '',
2093
+ };
2094
+ if (typeof log.count === 'number') {
2095
+ compact.count = log.count;
2096
+ }
2097
+ if (typeof log.firstTimestamp === 'number') {
2098
+ compact.firstTimestamp = log.firstTimestamp;
2099
+ }
2100
+ if (typeof log.lastTimestamp === 'number') {
2101
+ compact.lastTimestamp = log.lastTimestamp;
2102
+ }
2103
+ if (options.includeArgs === true && Array.isArray(log.args)) {
2104
+ compact.args = log.args;
2105
+ }
2106
+ return compact;
2107
+ }
2108
+ return log;
2109
+ }
618
2110
  function resolveOptionalTabId(value) {
619
2111
  if (value === undefined || value === null || value === '') {
620
2112
  return undefined;
@@ -663,6 +2155,105 @@ function ensureCaptureSuccess(result, sessionId) {
663
2155
  }
664
2156
  return result.payload ?? {};
665
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
+ }
666
2257
  export function createV1ToolHandlers(getDb, getSessionConnectionState) {
667
2258
  return {
668
2259
  list_sessions: async (input) => {
@@ -670,6 +2261,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
670
2261
  const sinceMinutes = typeof input.sinceMinutes === 'number' ? input.sinceMinutes : undefined;
671
2262
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
672
2263
  const offset = resolveOffset(input.offset);
2264
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
673
2265
  const where = [];
674
2266
  const params = [];
675
2267
  if (sinceMinutes !== undefined && Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
@@ -681,6 +2273,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
681
2273
  SELECT
682
2274
  session_id,
683
2275
  created_at,
2276
+ paused_at,
684
2277
  ended_at,
685
2278
  tab_id,
686
2279
  window_id,
@@ -698,11 +2291,13 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
698
2291
  LIMIT ? OFFSET ?
699
2292
  `;
700
2293
  const rows = db.prepare(sql).all(...params, limit + 1, offset);
701
- const truncated = rows.length > limit;
2294
+ const truncatedByLimit = rows.length > limit;
702
2295
  const sessions = rows.slice(0, limit).map((row) => ({
703
2296
  sessionId: row.session_id,
704
2297
  createdAt: row.created_at,
2298
+ pausedAt: row.paused_at ?? undefined,
705
2299
  endedAt: row.ended_at ?? undefined,
2300
+ status: row.ended_at ? 'ended' : row.paused_at ? 'paused' : 'active',
706
2301
  tabId: row.tab_id ?? undefined,
707
2302
  windowId: row.window_id ?? undefined,
708
2303
  urlStart: row.url_start ?? undefined,
@@ -735,17 +2330,99 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
735
2330
  };
736
2331
  })(),
737
2332
  }));
2333
+ const bytePage = applyByteBudget(sessions, maxResponseBytes);
2334
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
738
2335
  return {
739
2336
  ...createBaseResponse(),
740
2337
  limitsApplied: {
741
2338
  maxResults: limit,
742
2339
  truncated,
743
2340
  },
744
- pagination: {
745
- offset,
746
- returned: sessions.length,
2341
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2342
+ responseBytes: bytePage.responseBytes,
2343
+ sessions: bytePage.items,
2344
+ };
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,
747
2407
  },
748
- sessions,
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',
749
2426
  };
750
2427
  },
751
2428
  get_session_summary: async (input) => {
@@ -817,6 +2494,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
817
2494
  ensureSessionOrOriginFilter(sessionId, origin);
818
2495
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
819
2496
  const offset = resolveOffset(input.offset);
2497
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
2498
+ const responseProfile = resolveResponseProfile(input.responseProfile);
2499
+ const includePayload = responseProfile === 'compact' && input.includePayload === true;
820
2500
  const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
821
2501
  const params = [];
822
2502
  const where = [];
@@ -839,18 +2519,22 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
839
2519
  LIMIT ? OFFSET ?
840
2520
  `)
841
2521
  .all(...params, limit + 1, offset);
842
- const truncated = rows.length > limit;
2522
+ const truncatedByLimit = rows.length > limit;
2523
+ const events = rows
2524
+ .slice(0, limit)
2525
+ .map((row) => mapEventRecord(row, responseProfile, { includePayload }));
2526
+ const bytePage = applyByteBudget(events, maxResponseBytes);
2527
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
843
2528
  return {
844
2529
  ...createBaseResponse(sessionId),
845
2530
  limitsApplied: {
846
2531
  maxResults: limit,
847
2532
  truncated,
848
2533
  },
849
- pagination: {
850
- offset,
851
- returned: Math.min(rows.length, limit),
852
- },
853
- events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
2534
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2535
+ responseProfile,
2536
+ responseBytes: bytePage.responseBytes,
2537
+ events: bytePage.items,
854
2538
  };
855
2539
  },
856
2540
  get_navigation_history: async (input) => {
@@ -860,6 +2544,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
860
2544
  ensureSessionOrOriginFilter(sessionId, origin);
861
2545
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
862
2546
  const offset = resolveOffset(input.offset);
2547
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
2548
+ const responseProfile = resolveResponseProfile(input.responseProfile);
2549
+ const includePayload = responseProfile === 'compact' && input.includePayload === true;
863
2550
  const params = [];
864
2551
  const where = ["type = 'nav'"];
865
2552
  if (sessionId) {
@@ -876,18 +2563,22 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
876
2563
  LIMIT ? OFFSET ?
877
2564
  `)
878
2565
  .all(...params, limit + 1, offset);
879
- const truncated = rows.length > limit;
2566
+ const truncatedByLimit = rows.length > limit;
2567
+ const events = rows
2568
+ .slice(0, limit)
2569
+ .map((row) => mapEventRecord(row, responseProfile, { includePayload }));
2570
+ const bytePage = applyByteBudget(events, maxResponseBytes);
2571
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
880
2572
  return {
881
2573
  ...createBaseResponse(sessionId),
882
2574
  limitsApplied: {
883
2575
  maxResults: limit,
884
2576
  truncated,
885
2577
  },
886
- pagination: {
887
- offset,
888
- returned: Math.min(rows.length, limit),
889
- },
890
- events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
2578
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2579
+ responseProfile,
2580
+ responseBytes: bytePage.responseBytes,
2581
+ events: bytePage.items,
891
2582
  };
892
2583
  },
893
2584
  get_console_events: async (input) => {
@@ -898,6 +2589,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
898
2589
  const level = typeof input.level === 'string' ? input.level : undefined;
899
2590
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
900
2591
  const offset = resolveOffset(input.offset);
2592
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
2593
+ const responseProfile = resolveResponseProfile(input.responseProfile);
2594
+ const includePayload = responseProfile === 'compact' && input.includePayload === true;
901
2595
  const params = [];
902
2596
  const where = ["type = 'console'"];
903
2597
  if (sessionId) {
@@ -918,18 +2612,170 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
918
2612
  LIMIT ? OFFSET ?
919
2613
  `)
920
2614
  .all(...params, limit + 1, offset);
921
- const truncated = rows.length > limit;
2615
+ const truncatedByLimit = rows.length > limit;
2616
+ const events = rows
2617
+ .slice(0, limit)
2618
+ .map((row) => mapEventRecord(row, responseProfile, { includePayload }));
2619
+ const bytePage = applyByteBudget(events, maxResponseBytes);
2620
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
2621
+ return {
2622
+ ...createBaseResponse(sessionId),
2623
+ limitsApplied: {
2624
+ maxResults: limit,
2625
+ truncated,
2626
+ },
2627
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2628
+ responseProfile,
2629
+ responseBytes: bytePage.responseBytes,
2630
+ events: bytePage.items,
2631
+ };
2632
+ },
2633
+ get_console_summary: async (input) => {
2634
+ const db = getDb();
2635
+ const sessionId = getSessionId(input);
2636
+ const origin = normalizeRequestedOrigin(input.url);
2637
+ ensureSessionOrOriginFilter(sessionId, origin);
2638
+ const level = typeof input.level === 'string' && input.level.length > 0 ? input.level : undefined;
2639
+ const sinceMinutes = typeof input.sinceMinutes === 'number' && Number.isFinite(input.sinceMinutes)
2640
+ ? Math.floor(input.sinceMinutes)
2641
+ : undefined;
2642
+ const limit = resolveLimit(input.limit, 10);
2643
+ const where = ["type = 'console'"];
2644
+ const params = [];
2645
+ if (sessionId) {
2646
+ where.push('session_id = ?');
2647
+ params.push(sessionId);
2648
+ }
2649
+ appendEventOriginFilter(where, params, origin);
2650
+ if (level) {
2651
+ where.push("json_extract(payload_json, '$.level') = ?");
2652
+ params.push(level);
2653
+ }
2654
+ if (sinceMinutes !== undefined && sinceMinutes > 0) {
2655
+ where.push('ts >= ?');
2656
+ params.push(Date.now() - sinceMinutes * 60_000);
2657
+ }
2658
+ const whereClause = `WHERE ${where.join(' AND ')}`;
2659
+ const totals = db
2660
+ .prepare(`
2661
+ SELECT
2662
+ COUNT(*) AS total,
2663
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'log' THEN 1 ELSE 0 END) AS log_count,
2664
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'info' THEN 1 ELSE 0 END) AS info_count,
2665
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'warn' THEN 1 ELSE 0 END) AS warn_count,
2666
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'error' THEN 1 ELSE 0 END) AS error_count,
2667
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'debug' THEN 1 ELSE 0 END) AS debug_count,
2668
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'trace' THEN 1 ELSE 0 END) AS trace_count,
2669
+ MIN(ts) AS first_ts,
2670
+ MAX(ts) AS last_ts
2671
+ FROM events
2672
+ ${whereClause}
2673
+ `)
2674
+ .get(...params);
2675
+ const topMessages = db
2676
+ .prepare(`
2677
+ SELECT
2678
+ COALESCE(json_extract(payload_json, '$.message'), 'console event') AS message,
2679
+ COALESCE(json_extract(payload_json, '$.level'), 'log') AS level,
2680
+ COUNT(*) AS count,
2681
+ MIN(ts) AS first_ts,
2682
+ MAX(ts) AS last_ts
2683
+ FROM events
2684
+ ${whereClause}
2685
+ GROUP BY message, level
2686
+ ORDER BY count DESC, last_ts DESC
2687
+ LIMIT ?
2688
+ `)
2689
+ .all(...params, limit);
2690
+ return {
2691
+ ...createBaseResponse(sessionId),
2692
+ limitsApplied: {
2693
+ maxResults: limit,
2694
+ truncated: false,
2695
+ },
2696
+ counts: {
2697
+ total: totals.total ?? 0,
2698
+ byLevel: {
2699
+ log: totals.log_count ?? 0,
2700
+ info: totals.info_count ?? 0,
2701
+ warn: totals.warn_count ?? 0,
2702
+ error: totals.error_count ?? 0,
2703
+ debug: totals.debug_count ?? 0,
2704
+ trace: totals.trace_count ?? 0,
2705
+ },
2706
+ },
2707
+ firstSeenAt: totals.first_ts ?? undefined,
2708
+ lastSeenAt: totals.last_ts ?? undefined,
2709
+ topMessages: topMessages.map((entry) => ({
2710
+ level: entry.level,
2711
+ message: entry.message,
2712
+ count: entry.count,
2713
+ firstSeenAt: entry.first_ts,
2714
+ lastSeenAt: entry.last_ts,
2715
+ })),
2716
+ };
2717
+ },
2718
+ get_event_summary: async (input) => {
2719
+ const db = getDb();
2720
+ const sessionId = getSessionId(input);
2721
+ const origin = normalizeRequestedOrigin(input.url);
2722
+ ensureSessionOrOriginFilter(sessionId, origin);
2723
+ const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
2724
+ const sinceMinutes = typeof input.sinceMinutes === 'number' && Number.isFinite(input.sinceMinutes)
2725
+ ? Math.floor(input.sinceMinutes)
2726
+ : undefined;
2727
+ const limit = resolveLimit(input.limit, 20);
2728
+ const where = [];
2729
+ const params = [];
2730
+ if (sessionId) {
2731
+ where.push('session_id = ?');
2732
+ params.push(sessionId);
2733
+ }
2734
+ appendEventOriginFilter(where, params, origin);
2735
+ if (requestedTypes.length > 0) {
2736
+ const placeholders = requestedTypes.map(() => '?').join(', ');
2737
+ where.push(`type IN (${placeholders})`);
2738
+ params.push(...requestedTypes);
2739
+ }
2740
+ if (sinceMinutes !== undefined && sinceMinutes > 0) {
2741
+ where.push('ts >= ?');
2742
+ params.push(Date.now() - sinceMinutes * 60_000);
2743
+ }
2744
+ const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
2745
+ const totals = db
2746
+ .prepare(`
2747
+ SELECT COUNT(*) AS total, MIN(ts) AS first_ts, MAX(ts) AS last_ts
2748
+ FROM events
2749
+ ${whereClause}
2750
+ `)
2751
+ .get(...params);
2752
+ const byType = db
2753
+ .prepare(`
2754
+ SELECT type, COUNT(*) AS count, MIN(ts) AS first_ts, MAX(ts) AS last_ts
2755
+ FROM events
2756
+ ${whereClause}
2757
+ GROUP BY type
2758
+ ORDER BY count DESC, last_ts DESC
2759
+ LIMIT ?
2760
+ `)
2761
+ .all(...params, limit);
922
2762
  return {
923
2763
  ...createBaseResponse(sessionId),
924
2764
  limitsApplied: {
925
2765
  maxResults: limit,
926
- truncated,
2766
+ truncated: false,
927
2767
  },
928
- pagination: {
929
- offset,
930
- returned: Math.min(rows.length, limit),
2768
+ counts: {
2769
+ total: totals.total ?? 0,
931
2770
  },
932
- events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
2771
+ firstSeenAt: totals.first_ts ?? undefined,
2772
+ lastSeenAt: totals.last_ts ?? undefined,
2773
+ byType: byType.map((entry) => ({
2774
+ type: entry.type,
2775
+ count: entry.count,
2776
+ firstSeenAt: entry.first_ts,
2777
+ lastSeenAt: entry.last_ts,
2778
+ })),
933
2779
  };
934
2780
  },
935
2781
  get_error_fingerprints: async (input) => {
@@ -940,6 +2786,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
940
2786
  : undefined;
941
2787
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
942
2788
  const offset = resolveOffset(input.offset);
2789
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
943
2790
  const params = [];
944
2791
  const where = [];
945
2792
  if (sessionId) {
@@ -960,26 +2807,27 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
960
2807
  LIMIT ? OFFSET ?
961
2808
  `)
962
2809
  .all(...params, limit + 1, offset);
963
- const truncated = rows.length > limit;
2810
+ const truncatedByLimit = rows.length > limit;
2811
+ const fingerprints = rows.slice(0, limit).map((row) => ({
2812
+ fingerprint: row.fingerprint,
2813
+ sessionId: row.session_id,
2814
+ count: row.count,
2815
+ sampleMessage: row.sample_message,
2816
+ sampleStack: row.sample_stack ?? undefined,
2817
+ firstSeenAt: row.first_seen_at,
2818
+ lastSeenAt: row.last_seen_at,
2819
+ }));
2820
+ const bytePage = applyByteBudget(fingerprints, maxResponseBytes);
2821
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
964
2822
  return {
965
2823
  ...createBaseResponse(sessionId),
966
2824
  limitsApplied: {
967
2825
  maxResults: limit,
968
2826
  truncated,
969
2827
  },
970
- pagination: {
971
- offset,
972
- returned: Math.min(rows.length, limit),
973
- },
974
- fingerprints: rows.slice(0, limit).map((row) => ({
975
- fingerprint: row.fingerprint,
976
- sessionId: row.session_id,
977
- count: row.count,
978
- sampleMessage: row.sample_message,
979
- sampleStack: row.sample_stack ?? undefined,
980
- firstSeenAt: row.first_seen_at,
981
- lastSeenAt: row.last_seen_at,
982
- })),
2828
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2829
+ responseBytes: bytePage.responseBytes,
2830
+ fingerprints: bytePage.items,
983
2831
  };
984
2832
  },
985
2833
  get_network_failures: async (input) => {
@@ -991,6 +2839,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
991
2839
  const errorType = typeof input.errorType === 'string' ? input.errorType : undefined;
992
2840
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
993
2841
  const offset = resolveOffset(input.offset);
2842
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
994
2843
  const params = [];
995
2844
  const where = [];
996
2845
  const errorFilter = buildNetworkFailureFilter(errorType);
@@ -1024,58 +2873,327 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1024
2873
  LIMIT ? OFFSET ?
1025
2874
  `)
1026
2875
  .all(...params, limit + 1, offset);
1027
- const truncated = rows.length > limit;
2876
+ const truncatedByLimit = rows.length > limit;
2877
+ const groups = rows.slice(0, limit).map((row) => ({
2878
+ key: row.group_key,
2879
+ count: row.count,
2880
+ firstSeenAt: row.first_ts,
2881
+ lastSeenAt: row.last_ts,
2882
+ }));
2883
+ const bytePage = applyByteBudget(groups, maxResponseBytes);
2884
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
1028
2885
  return {
1029
2886
  ...createBaseResponse(sessionId),
1030
2887
  limitsApplied: {
1031
2888
  maxResults: limit,
1032
2889
  truncated,
1033
2890
  },
1034
- pagination: {
1035
- offset,
1036
- returned: Math.min(rows.length, limit),
1037
- },
2891
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2892
+ responseBytes: bytePage.responseBytes,
1038
2893
  groupBy,
1039
- groups: rows.slice(0, limit).map((row) => ({
1040
- key: row.group_key,
1041
- count: row.count,
1042
- firstSeenAt: row.first_ts,
1043
- lastSeenAt: row.last_ts,
1044
- })),
2894
+ groups: bytePage.items,
1045
2895
  };
1046
2896
  }
1047
2897
  const rows = db
1048
2898
  .prepare(`
1049
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
2899
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1050
2900
  FROM network
1051
2901
  ${whereClause}
1052
2902
  ORDER BY ts_start DESC
1053
2903
  LIMIT ? OFFSET ?
1054
2904
  `)
1055
2905
  .all(...params, limit + 1, offset);
1056
- const truncated = rows.length > limit;
2906
+ const truncatedByLimit = rows.length > limit;
2907
+ const failures = rows.slice(0, limit).map((row) => ({
2908
+ requestId: row.request_id,
2909
+ sessionId: row.session_id,
2910
+ traceId: row.trace_id ?? undefined,
2911
+ tabId: row.tab_id ?? undefined,
2912
+ timestamp: row.ts_start,
2913
+ durationMs: row.duration_ms ?? undefined,
2914
+ method: row.method,
2915
+ url: row.url,
2916
+ origin: row.origin ?? undefined,
2917
+ status: row.status ?? undefined,
2918
+ initiator: row.initiator ?? undefined,
2919
+ errorType: classifyNetworkFailure(row.status, row.error_class),
2920
+ }));
2921
+ const bytePage = applyByteBudget(failures, maxResponseBytes);
2922
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
1057
2923
  return {
1058
2924
  ...createBaseResponse(sessionId),
1059
2925
  limitsApplied: {
1060
2926
  maxResults: limit,
1061
2927
  truncated,
1062
2928
  },
1063
- pagination: {
1064
- offset,
1065
- returned: Math.min(rows.length, limit),
2929
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2930
+ responseBytes: bytePage.responseBytes,
2931
+ failures: bytePage.items,
2932
+ };
2933
+ },
2934
+ get_network_calls: async (input) => {
2935
+ const db = getDb();
2936
+ const sessionId = getSessionId(input);
2937
+ if (!sessionId) {
2938
+ throw new Error('sessionId is required');
2939
+ }
2940
+ const includeBodies = input.includeBodies === true;
2941
+ const urlContains = normalizeOptionalString(input.urlContains);
2942
+ const urlRegex = compileSafeRegex(normalizeOptionalString(input.urlRegex));
2943
+ const method = normalizeHttpMethod(input.method);
2944
+ const statusIn = normalizeStatusIn(input.statusIn);
2945
+ const tabId = resolveOptionalTabId(input.tabId);
2946
+ const timeFrom = resolveOptionalTimestamp(input.timeFrom);
2947
+ const timeTo = resolveOptionalTimestamp(input.timeTo);
2948
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
2949
+ const offset = resolveOffset(input.offset);
2950
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
2951
+ if (timeFrom !== undefined && timeTo !== undefined && timeFrom > timeTo) {
2952
+ throw new Error('timeFrom must be <= timeTo');
2953
+ }
2954
+ const where = ['session_id = ?'];
2955
+ const params = [sessionId];
2956
+ if (urlContains) {
2957
+ where.push('url LIKE ?');
2958
+ params.push(`%${urlContains}%`);
2959
+ }
2960
+ if (method) {
2961
+ where.push('method = ?');
2962
+ params.push(method);
2963
+ }
2964
+ if (statusIn.length > 0) {
2965
+ where.push(`status IN (${statusIn.map(() => '?').join(', ')})`);
2966
+ params.push(...statusIn);
2967
+ }
2968
+ if (tabId !== undefined) {
2969
+ where.push('tab_id = ?');
2970
+ params.push(tabId);
2971
+ }
2972
+ if (timeFrom !== undefined) {
2973
+ where.push('ts_start >= ?');
2974
+ params.push(timeFrom);
2975
+ }
2976
+ if (timeTo !== undefined) {
2977
+ where.push('ts_start <= ?');
2978
+ params.push(timeTo);
2979
+ }
2980
+ const whereClause = `WHERE ${where.join(' AND ')}`;
2981
+ if (!urlRegex) {
2982
+ const rows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
2983
+ FROM network
2984
+ ${whereClause}
2985
+ ORDER BY ts_start DESC
2986
+ LIMIT ? OFFSET ?`).all(...params, limit + 1, offset);
2987
+ const truncatedByLimit = rows.length > limit;
2988
+ const calls = rows
2989
+ .slice(0, limit)
2990
+ .map((row) => mapNetworkCallRecord(row, includeBodies));
2991
+ const bytePage = applyByteBudget(calls, maxResponseBytes);
2992
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
2993
+ return {
2994
+ ...createBaseResponse(sessionId),
2995
+ limitsApplied: {
2996
+ maxResults: limit,
2997
+ truncated,
2998
+ },
2999
+ filtersApplied: {
3000
+ sessionId,
3001
+ urlContains,
3002
+ method,
3003
+ statusIn,
3004
+ tabId,
3005
+ timeFrom,
3006
+ timeTo,
3007
+ includeBodies,
3008
+ },
3009
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
3010
+ responseBytes: bytePage.responseBytes,
3011
+ calls: bytePage.items,
3012
+ };
3013
+ }
3014
+ const regexScanLimit = Math.min(Math.max(limit + offset + 200, 500), 5000);
3015
+ const regex = urlRegex;
3016
+ const regexRows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
3017
+ FROM network
3018
+ ${whereClause}
3019
+ ORDER BY ts_start DESC
3020
+ LIMIT ?`).all(...params, regexScanLimit);
3021
+ const matched = regexRows.filter((row) => regex.test(row.url));
3022
+ const sliced = matched.slice(offset, offset + limit + 1);
3023
+ const truncatedByLimit = matched.length > offset + limit;
3024
+ const calls = sliced
3025
+ .slice(0, limit)
3026
+ .map((row) => mapNetworkCallRecord(row, includeBodies));
3027
+ const bytePage = applyByteBudget(calls, maxResponseBytes);
3028
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
3029
+ return {
3030
+ ...createBaseResponse(sessionId),
3031
+ limitsApplied: {
3032
+ maxResults: limit,
3033
+ truncated,
1066
3034
  },
1067
- failures: rows.slice(0, limit).map((row) => ({
1068
- requestId: row.request_id,
1069
- sessionId: row.session_id,
1070
- timestamp: row.ts_start,
1071
- durationMs: row.duration_ms ?? undefined,
1072
- method: row.method,
1073
- url: row.url,
1074
- origin: row.origin ?? undefined,
1075
- status: row.status ?? undefined,
1076
- initiator: row.initiator ?? undefined,
1077
- errorType: classifyNetworkFailure(row.status, row.error_class),
1078
- })),
3035
+ filtersApplied: {
3036
+ sessionId,
3037
+ urlContains,
3038
+ urlRegex: urlRegex.source,
3039
+ method,
3040
+ statusIn,
3041
+ tabId,
3042
+ timeFrom,
3043
+ timeTo,
3044
+ includeBodies,
3045
+ regexScanLimit,
3046
+ },
3047
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
3048
+ responseBytes: bytePage.responseBytes,
3049
+ calls: bytePage.items,
3050
+ };
3051
+ },
3052
+ wait_for_network_call: async (input) => {
3053
+ const db = getDb();
3054
+ const sessionId = getSessionId(input);
3055
+ if (!sessionId) {
3056
+ throw new Error('sessionId is required');
3057
+ }
3058
+ const urlPattern = normalizeOptionalString(input.urlPattern);
3059
+ if (!urlPattern) {
3060
+ throw new Error('urlPattern is required');
3061
+ }
3062
+ const method = normalizeHttpMethod(input.method);
3063
+ const timeoutMs = resolveTimeoutMs(input.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
3064
+ const includeBodies = input.includeBodies === true;
3065
+ const startedAt = Date.now();
3066
+ const deadline = startedAt + timeoutMs;
3067
+ const urlRegex = compileSafeRegex(urlPattern);
3068
+ if (!urlRegex) {
3069
+ throw new Error('urlPattern is required');
3070
+ }
3071
+ while (Date.now() <= deadline) {
3072
+ const where = ['session_id = ?', 'ts_start >= ?'];
3073
+ const params = [sessionId, startedAt];
3074
+ if (method) {
3075
+ where.push('method = ?');
3076
+ params.push(method);
3077
+ }
3078
+ const rows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
3079
+ FROM network
3080
+ WHERE ${where.join(' AND ')}
3081
+ ORDER BY ts_start ASC
3082
+ LIMIT 200`).all(...params);
3083
+ const matched = rows.find((row) => urlRegex.test(row.url));
3084
+ if (matched) {
3085
+ return {
3086
+ ...createBaseResponse(sessionId),
3087
+ limitsApplied: {
3088
+ maxResults: 1,
3089
+ truncated: false,
3090
+ },
3091
+ waitedMs: Date.now() - startedAt,
3092
+ filter: {
3093
+ urlPattern,
3094
+ method,
3095
+ timeoutMs,
3096
+ includeBodies,
3097
+ },
3098
+ call: mapNetworkCallRecord(matched, includeBodies),
3099
+ };
3100
+ }
3101
+ await sleep(DEFAULT_NETWORK_POLL_INTERVAL_MS);
3102
+ }
3103
+ throw new Error(`No matching network call for pattern "${urlPattern}" within ${timeoutMs}ms.`);
3104
+ },
3105
+ get_request_trace: async (input) => {
3106
+ const db = getDb();
3107
+ const sessionId = getSessionId(input);
3108
+ const includeBodies = input.includeBodies === true;
3109
+ const requestId = normalizeOptionalString(input.requestId);
3110
+ const traceIdInput = normalizeOptionalString(input.traceId);
3111
+ const eventLimit = resolveLimit(input.eventLimit, DEFAULT_EVENT_LIMIT);
3112
+ if (!requestId && !traceIdInput) {
3113
+ throw new Error('requestId or traceId is required');
3114
+ }
3115
+ let anchor;
3116
+ if (requestId) {
3117
+ const params = [requestId];
3118
+ let sql = `SELECT ${NETWORK_CALL_SELECT_COLUMNS} FROM network WHERE request_id = ?`;
3119
+ if (sessionId) {
3120
+ sql += ' AND session_id = ?';
3121
+ params.push(sessionId);
3122
+ }
3123
+ sql += ' LIMIT 1';
3124
+ anchor = db.prepare(sql).get(...params);
3125
+ if (!anchor) {
3126
+ throw new Error(`Request not found: ${requestId}`);
3127
+ }
3128
+ }
3129
+ const traceId = traceIdInput ?? anchor?.trace_id ?? null;
3130
+ const traceSessionId = sessionId ?? anchor?.session_id;
3131
+ const networkWhere = [];
3132
+ const networkParams = [];
3133
+ if (traceId) {
3134
+ networkWhere.push('trace_id = ?');
3135
+ networkParams.push(traceId);
3136
+ }
3137
+ else if (requestId) {
3138
+ networkWhere.push('request_id = ?');
3139
+ networkParams.push(requestId);
3140
+ }
3141
+ if (traceSessionId) {
3142
+ networkWhere.push('session_id = ?');
3143
+ networkParams.push(traceSessionId);
3144
+ }
3145
+ const networkRows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
3146
+ FROM network
3147
+ WHERE ${networkWhere.join(' AND ')}
3148
+ ORDER BY ts_start ASC
3149
+ LIMIT 500`).all(...networkParams);
3150
+ const eventRows = traceId
3151
+ ? db.prepare(`SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
3152
+ FROM events
3153
+ WHERE json_extract(payload_json, '$.traceId') = ?
3154
+ ${traceSessionId ? 'AND session_id = ?' : ''}
3155
+ ORDER BY ts ASC
3156
+ LIMIT ?`).all(...(traceSessionId ? [traceId, traceSessionId, eventLimit + 1] : [traceId, eventLimit + 1]))
3157
+ : [];
3158
+ const eventsTruncated = eventRows.length > eventLimit;
3159
+ const correlatedEvents = eventRows.slice(0, eventLimit).map((row) => mapEventRecord(row));
3160
+ return {
3161
+ ...createBaseResponse(traceSessionId),
3162
+ limitsApplied: {
3163
+ maxResults: eventLimit,
3164
+ truncated: eventsTruncated,
3165
+ },
3166
+ traceId: traceId ?? undefined,
3167
+ requestId: requestId ?? anchor?.request_id ?? undefined,
3168
+ anchorRequest: anchor ? mapNetworkCallRecord(anchor, includeBodies) : undefined,
3169
+ networkCalls: networkRows.map((row) => mapNetworkCallRecord(row, includeBodies)),
3170
+ correlatedEvents,
3171
+ };
3172
+ },
3173
+ get_body_chunk: async (input) => {
3174
+ const db = getDb();
3175
+ const chunkRef = normalizeOptionalString(input.chunkRef);
3176
+ if (!chunkRef) {
3177
+ throw new Error('chunkRef is required');
3178
+ }
3179
+ const sessionId = getSessionId(input);
3180
+ const offset = resolveOffset(input.offset);
3181
+ const limit = resolveBodyChunkBytes(input.limit);
3182
+ const row = db.prepare(`SELECT chunk_ref, session_id, request_id, trace_id, body_kind, content_type, body_text, body_bytes, truncated, created_at
3183
+ FROM body_chunks
3184
+ WHERE chunk_ref = ?
3185
+ ${sessionId ? 'AND session_id = ?' : ''}
3186
+ LIMIT 1`).get(...(sessionId ? [chunkRef, sessionId] : [chunkRef]));
3187
+ if (!row) {
3188
+ throw new Error(`Body chunk not found: ${chunkRef}`);
3189
+ }
3190
+ return {
3191
+ ...createBaseResponse(row.session_id),
3192
+ limitsApplied: {
3193
+ maxResults: limit,
3194
+ truncated: offset + limit < row.body_bytes,
3195
+ },
3196
+ ...mapBodyChunkRecord(row, offset, limit),
1079
3197
  };
1080
3198
  },
1081
3199
  get_element_refs: async (input) => {
@@ -1090,6 +3208,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1090
3208
  }
1091
3209
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
1092
3210
  const offset = resolveOffset(input.offset);
3211
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1093
3212
  const rows = db
1094
3213
  .prepare(`
1095
3214
  SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
@@ -1101,19 +3220,20 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1101
3220
  LIMIT ? OFFSET ?
1102
3221
  `)
1103
3222
  .all(sessionId, selector, limit + 1, offset);
1104
- const truncated = rows.length > limit;
3223
+ const truncatedByLimit = rows.length > limit;
3224
+ const refs = rows.slice(0, limit).map((row) => mapEventRecord(row));
3225
+ const bytePage = applyByteBudget(refs, maxResponseBytes);
3226
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
1105
3227
  return {
1106
3228
  ...createBaseResponse(sessionId),
1107
3229
  limitsApplied: {
1108
3230
  maxResults: limit,
1109
3231
  truncated,
1110
3232
  },
1111
- pagination: {
1112
- offset,
1113
- returned: Math.min(rows.length, limit),
1114
- },
3233
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
3234
+ responseBytes: bytePage.responseBytes,
1115
3235
  selector,
1116
- refs: rows.slice(0, limit).map((row) => mapEventRecord(row)),
3236
+ refs: bytePage.items,
1117
3237
  };
1118
3238
  },
1119
3239
  explain_last_failure: async (input) => {
@@ -1136,7 +3256,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1136
3256
  .get(sessionId);
1137
3257
  const latestNetworkFailure = db
1138
3258
  .prepare(`
1139
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
3259
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1140
3260
  FROM network
1141
3261
  WHERE session_id = ?
1142
3262
  AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
@@ -1173,7 +3293,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1173
3293
  .all(sessionId, windowStart, windowEnd);
1174
3294
  const networkRows = db
1175
3295
  .prepare(`
1176
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
3296
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1177
3297
  FROM network
1178
3298
  WHERE session_id = ?
1179
3299
  AND ts_start BETWEEN ? AND ?
@@ -1280,7 +3400,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1280
3400
  .all(sessionId, eventId, windowStart, windowEnd);
1281
3401
  const nearbyNetworkFailures = db
1282
3402
  .prepare(`
1283
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
3403
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1284
3404
  FROM network
1285
3405
  WHERE session_id = ?
1286
3406
  AND ts_start BETWEEN ? AND ?
@@ -1352,6 +3472,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1352
3472
  const untilTimestamp = resolveOptionalTimestamp(input.untilTimestamp);
1353
3473
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
1354
3474
  const offset = resolveOffset(input.offset);
3475
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1355
3476
  const where = ['session_id = ?'];
1356
3477
  const params = [sessionId];
1357
3478
  if (trigger) {
@@ -1376,18 +3497,19 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1376
3497
  ORDER BY ts DESC
1377
3498
  LIMIT ? OFFSET ?`)
1378
3499
  .all(...params, limit + 1, offset);
1379
- const truncated = rows.length > limit;
3500
+ const truncatedByLimit = rows.length > limit;
3501
+ const snapshots = rows.slice(0, limit).map((row) => mapSnapshotMetadata(row));
3502
+ const bytePage = applyByteBudget(snapshots, maxResponseBytes);
3503
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
1380
3504
  return {
1381
3505
  ...createBaseResponse(sessionId),
1382
3506
  limitsApplied: {
1383
3507
  maxResults: limit,
1384
3508
  truncated,
1385
3509
  },
1386
- pagination: {
1387
- offset,
1388
- returned: Math.min(rows.length, limit),
1389
- },
1390
- snapshots: rows.slice(0, limit).map((row) => mapSnapshotMetadata(row)),
3510
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
3511
+ responseBytes: bytePage.responseBytes,
3512
+ snapshots: bytePage.items,
1391
3513
  };
1392
3514
  },
1393
3515
  get_snapshot_for_event: async (input) => {
@@ -1469,7 +3591,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1469
3591
  throw new Error('snapshotId is required');
1470
3592
  }
1471
3593
  const assetType = input.asset === 'png' ? 'png' : 'png';
1472
- const encoding = input.encoding === 'base64' ? 'base64' : 'raw';
3594
+ const encoding = input.encoding === 'raw' ? 'raw' : 'base64';
1473
3595
  const offset = resolveOffset(input.offset);
1474
3596
  const maxBytes = resolveChunkBytes(input.maxBytes, DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES);
1475
3597
  const snapshot = db
@@ -1499,6 +3621,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1499
3621
  },
1500
3622
  snapshotId,
1501
3623
  asset: assetType,
3624
+ assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
1502
3625
  mime: snapshot.png_mime ?? 'image/png',
1503
3626
  totalBytes: fullBuffer.byteLength,
1504
3627
  offset,
@@ -1522,6 +3645,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1522
3645
  },
1523
3646
  snapshotId,
1524
3647
  asset: assetType,
3648
+ assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
1525
3649
  mime: snapshot.png_mime ?? 'image/png',
1526
3650
  totalBytes: fullBuffer.byteLength,
1527
3651
  offset,
@@ -1533,9 +3657,193 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1533
3657
  chunkBase64: encoding === 'base64' ? chunkBuffer.toString('base64') : undefined,
1534
3658
  };
1535
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
+ },
1536
3823
  };
1537
3824
  }
1538
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
+ };
1539
3847
  return {
1540
3848
  get_dom_subtree: async (input) => {
1541
3849
  const sessionId = getSessionId(input);
@@ -1630,6 +3938,345 @@ export function createV2ToolHandlers(captureClient) {
1630
3938
  ...ensureCaptureSuccess(capture, sessionId),
1631
3939
  };
1632
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
+ },
1633
4280
  capture_ui_snapshot: async (input) => {
1634
4281
  const sessionId = getSessionId(input);
1635
4282
  if (!sessionId) {
@@ -1649,6 +4296,9 @@ export function createV2ToolHandlers(captureClient) {
1649
4296
  const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
1650
4297
  const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
1651
4298
  const maxAncestors = resolveCaptureAncestors(input.maxAncestors, 4);
4299
+ const includeDom = typeof input.includeDom === 'boolean' ? input.includeDom : mode !== 'png';
4300
+ const includeStyles = typeof input.includeStyles === 'boolean' ? input.includeStyles : mode !== 'png';
4301
+ const includePngDataUrl = typeof input.includePngDataUrl === 'boolean' ? input.includePngDataUrl : mode !== 'png';
1652
4302
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
1653
4303
  selector,
1654
4304
  trigger,
@@ -1658,15 +4308,27 @@ export function createV2ToolHandlers(captureClient) {
1658
4308
  maxDepth,
1659
4309
  maxBytes,
1660
4310
  maxAncestors,
4311
+ includeDom,
4312
+ includeStyles,
4313
+ includePngDataUrl,
1661
4314
  llmRequested: true,
1662
4315
  }, 5_000);
4316
+ const payload = ensureCaptureSuccess(capture, sessionId);
4317
+ const snapshotRecord = normalizeSnapshotResponsePayload(payload, {
4318
+ includeDom,
4319
+ includeStyles,
4320
+ includePngDataUrl,
4321
+ });
1663
4322
  return {
1664
4323
  ...createBaseResponse(sessionId),
1665
4324
  limitsApplied: {
1666
4325
  maxResults: maxBytes,
1667
4326
  truncated: capture.truncated ?? false,
1668
4327
  },
1669
- ...ensureCaptureSuccess(capture, sessionId),
4328
+ includeDom,
4329
+ includeStyles,
4330
+ includePngDataUrl,
4331
+ ...snapshotRecord,
1670
4332
  };
1671
4333
  },
1672
4334
  get_live_console_logs: async (input) => {
@@ -1683,6 +4345,10 @@ export function createV2ToolHandlers(captureClient) {
1683
4345
  const sinceTs = resolveOptionalTimestamp(input.sinceTs);
1684
4346
  const includeRuntimeErrors = input.includeRuntimeErrors !== false;
1685
4347
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
4348
+ const responseProfile = resolveResponseProfile(input.responseProfile);
4349
+ const includeArgs = responseProfile === 'compact' && input.includeArgs === true;
4350
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
4351
+ const dedupeWindowMs = resolveDurationMs(input.dedupeWindowMs, 0, 60_000);
1686
4352
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
1687
4353
  origin,
1688
4354
  tabId,
@@ -1690,15 +4356,113 @@ export function createV2ToolHandlers(captureClient) {
1690
4356
  contains,
1691
4357
  sinceTs,
1692
4358
  includeRuntimeErrors,
4359
+ dedupeWindowMs,
1693
4360
  limit,
1694
4361
  }, 3_000);
4362
+ const payload = ensureCaptureSuccess(capture, sessionId);
4363
+ const rawLogs = asRecordArray(payload.logs);
4364
+ const logs = rawLogs.map((entry) => mapLiveConsoleLogRecord(entry, responseProfile, { includeArgs }));
4365
+ const bytePage = applyByteBudget(logs, maxResponseBytes);
4366
+ const truncated = (capture.truncated ?? false) || bytePage.truncatedByBytes;
4367
+ const paginationRecord = typeof payload.pagination === 'object' && payload.pagination !== null
4368
+ ? payload.pagination
4369
+ : {};
4370
+ const matched = typeof paginationRecord.matched === 'number'
4371
+ ? Math.max(0, Math.floor(paginationRecord.matched))
4372
+ : rawLogs.length;
1695
4373
  return {
1696
4374
  ...createBaseResponse(sessionId),
1697
4375
  limitsApplied: {
1698
4376
  maxResults: limit,
1699
- truncated: capture.truncated ?? false,
4377
+ truncated,
4378
+ },
4379
+ responseProfile,
4380
+ responseBytes: bytePage.responseBytes,
4381
+ logs: bytePage.items,
4382
+ pagination: {
4383
+ returned: bytePage.items.length,
4384
+ matched,
4385
+ hasMore: truncated,
4386
+ maxResponseBytes,
4387
+ },
4388
+ filtersApplied: typeof payload.filtersApplied === 'object' && payload.filtersApplied !== null
4389
+ ? payload.filtersApplied
4390
+ : {
4391
+ tabId,
4392
+ origin,
4393
+ levels,
4394
+ contains,
4395
+ sinceTs,
4396
+ includeRuntimeErrors,
4397
+ dedupeWindowMs,
4398
+ },
4399
+ bufferStats: payload.bufferStats,
4400
+ };
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,
1700
4465
  },
1701
- ...ensureCaptureSuccess(capture, sessionId),
1702
4466
  };
1703
4467
  },
1704
4468
  };
@@ -1728,6 +4492,15 @@ function createDefaultHandler(toolName) {
1728
4492
  };
1729
4493
  };
1730
4494
  }
4495
+ function attachResponseBytes(response) {
4496
+ if (typeof response.responseBytes === 'number' && Number.isFinite(response.responseBytes)) {
4497
+ return response;
4498
+ }
4499
+ return {
4500
+ ...response,
4501
+ responseBytes: estimateJsonBytes(response),
4502
+ };
4503
+ }
1731
4504
  export function createToolRegistry(overrides = {}) {
1732
4505
  return ALL_TOOLS.map((toolName) => {
1733
4506
  const schema = TOOL_SCHEMAS[toolName] ?? { type: 'object', properties: {} };
@@ -1744,7 +4517,8 @@ export async function routeToolCall(tools, toolName, input) {
1744
4517
  if (!tool) {
1745
4518
  throw new Error(`Unknown tool: ${toolName}`);
1746
4519
  }
1747
- return tool.handler(isRecord(input) ? input : {});
4520
+ const response = await tool.handler(isRecord(input) ? input : {});
4521
+ return attachResponseBytes(response);
1748
4522
  }
1749
4523
  export function createMCPServer(overrides = {}, options = {}) {
1750
4524
  const logger = options.logger ?? createDefaultMcpLogger();