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