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.
- package/README.md +25 -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 +263 -14
- 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 +180 -0
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +93 -1
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/main.js +54 -4
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +2860 -86
- 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/retention.js +67 -4
- package/apps/mcp-server/dist/retention.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 +30 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/apps/mcp-server/dist/websocket/websocket-server.js +18 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
- package/apps/mcp-server/package.json +2 -2
- package/package.json +17 -6
- 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
|
|
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
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
851
|
-
|
|
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
|
|
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
|
-
|
|
888
|
-
|
|
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
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
returned: Math.min(rows.length, limit),
|
|
2768
|
+
counts: {
|
|
2769
|
+
total: totals.total ?? 0,
|
|
931
2770
|
},
|
|
932
|
-
|
|
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
|
|
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
|
-
|
|
972
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
1388
|
-
|
|
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 === '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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();
|