browser-debug-mcp-bridge 1.5.0 → 1.9.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 +136 -26
- package/apps/mcp-server/dist/db/events-repository.js +296 -26
- package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
- package/apps/mcp-server/dist/db/migrations.js +191 -1
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +35 -1
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/main.js +21 -4
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +1148 -112
- package/apps/mcp-server/dist/mcp/server.js.map +1 -1
- package/apps/mcp-server/dist/retention.js +97 -8
- package/apps/mcp-server/dist/retention.js.map +1 -1
- package/apps/mcp-server/dist/websocket/messages.js +27 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/apps/mcp-server/dist/websocket/websocket-server.js +20 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
- package/apps/mcp-server/package.json +2 -2
- package/package.json +13 -6
- package/scripts/mcp-start.cjs +178 -18
|
@@ -27,6 +27,7 @@ const TOOL_SCHEMAS = {
|
|
|
27
27
|
sinceMinutes: { type: 'number' },
|
|
28
28
|
limit: { type: 'number' },
|
|
29
29
|
offset: { type: 'number' },
|
|
30
|
+
maxResponseBytes: { type: 'number' },
|
|
30
31
|
},
|
|
31
32
|
},
|
|
32
33
|
get_session_summary: {
|
|
@@ -38,31 +39,60 @@ const TOOL_SCHEMAS = {
|
|
|
38
39
|
},
|
|
39
40
|
get_recent_events: {
|
|
40
41
|
type: 'object',
|
|
41
|
-
required: ['sessionId'],
|
|
42
42
|
properties: {
|
|
43
43
|
sessionId: { type: 'string' },
|
|
44
|
+
url: { type: 'string' },
|
|
44
45
|
eventTypes: { type: 'array', items: { type: 'string' } },
|
|
45
46
|
limit: { type: 'number' },
|
|
46
47
|
offset: { type: 'number' },
|
|
48
|
+
responseProfile: { type: 'string' },
|
|
49
|
+
includePayload: { type: 'boolean' },
|
|
50
|
+
maxResponseBytes: { type: 'number' },
|
|
47
51
|
},
|
|
48
52
|
},
|
|
49
53
|
get_navigation_history: {
|
|
50
54
|
type: 'object',
|
|
51
|
-
required: ['sessionId'],
|
|
52
55
|
properties: {
|
|
53
56
|
sessionId: { type: 'string' },
|
|
57
|
+
url: { type: 'string' },
|
|
54
58
|
limit: { type: 'number' },
|
|
55
59
|
offset: { type: 'number' },
|
|
60
|
+
responseProfile: { type: 'string' },
|
|
61
|
+
includePayload: { type: 'boolean' },
|
|
62
|
+
maxResponseBytes: { type: 'number' },
|
|
56
63
|
},
|
|
57
64
|
},
|
|
58
65
|
get_console_events: {
|
|
59
66
|
type: 'object',
|
|
60
|
-
required: ['sessionId'],
|
|
61
67
|
properties: {
|
|
62
68
|
sessionId: { type: 'string' },
|
|
69
|
+
url: { type: 'string' },
|
|
63
70
|
level: { type: 'string' },
|
|
64
71
|
limit: { type: 'number' },
|
|
65
72
|
offset: { type: 'number' },
|
|
73
|
+
responseProfile: { type: 'string' },
|
|
74
|
+
includePayload: { type: 'boolean' },
|
|
75
|
+
maxResponseBytes: { type: 'number' },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
get_console_summary: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
sessionId: { type: 'string' },
|
|
82
|
+
url: { type: 'string' },
|
|
83
|
+
level: { type: 'string' },
|
|
84
|
+
sinceMinutes: { type: 'number' },
|
|
85
|
+
limit: { type: 'number' },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
get_event_summary: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
sessionId: { type: 'string' },
|
|
92
|
+
url: { type: 'string' },
|
|
93
|
+
eventTypes: { type: 'array', items: { type: 'string' } },
|
|
94
|
+
sinceMinutes: { type: 'number' },
|
|
95
|
+
limit: { type: 'number' },
|
|
66
96
|
},
|
|
67
97
|
},
|
|
68
98
|
get_error_fingerprints: {
|
|
@@ -72,16 +102,68 @@ const TOOL_SCHEMAS = {
|
|
|
72
102
|
sinceMinutes: { type: 'number' },
|
|
73
103
|
limit: { type: 'number' },
|
|
74
104
|
offset: { type: 'number' },
|
|
105
|
+
maxResponseBytes: { type: 'number' },
|
|
75
106
|
},
|
|
76
107
|
},
|
|
77
108
|
get_network_failures: {
|
|
78
109
|
type: 'object',
|
|
79
110
|
properties: {
|
|
80
111
|
sessionId: { type: 'string' },
|
|
112
|
+
url: { type: 'string' },
|
|
81
113
|
errorType: { type: 'string' },
|
|
82
114
|
groupBy: { type: 'string' },
|
|
83
115
|
limit: { type: 'number' },
|
|
84
116
|
offset: { type: 'number' },
|
|
117
|
+
maxResponseBytes: { type: 'number' },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
get_network_calls: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
required: ['sessionId'],
|
|
123
|
+
properties: {
|
|
124
|
+
sessionId: { type: 'string' },
|
|
125
|
+
urlContains: { type: 'string' },
|
|
126
|
+
urlRegex: { type: 'string' },
|
|
127
|
+
method: { type: 'string' },
|
|
128
|
+
statusIn: { type: 'array', items: { type: 'number' } },
|
|
129
|
+
tabId: { type: 'number' },
|
|
130
|
+
timeFrom: { type: 'number' },
|
|
131
|
+
timeTo: { type: 'number' },
|
|
132
|
+
includeBodies: { type: 'boolean' },
|
|
133
|
+
limit: { type: 'number' },
|
|
134
|
+
offset: { type: 'number' },
|
|
135
|
+
maxResponseBytes: { type: 'number' },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
wait_for_network_call: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
required: ['sessionId', 'urlPattern'],
|
|
141
|
+
properties: {
|
|
142
|
+
sessionId: { type: 'string' },
|
|
143
|
+
urlPattern: { type: 'string' },
|
|
144
|
+
method: { type: 'string' },
|
|
145
|
+
timeoutMs: { type: 'number' },
|
|
146
|
+
includeBodies: { type: 'boolean' },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
get_request_trace: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
sessionId: { type: 'string' },
|
|
153
|
+
requestId: { type: 'string' },
|
|
154
|
+
traceId: { type: 'string' },
|
|
155
|
+
includeBodies: { type: 'boolean' },
|
|
156
|
+
eventLimit: { type: 'number' },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
get_body_chunk: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
required: ['chunkRef'],
|
|
162
|
+
properties: {
|
|
163
|
+
chunkRef: { type: 'string' },
|
|
164
|
+
sessionId: { type: 'string' },
|
|
165
|
+
offset: { type: 'number' },
|
|
166
|
+
limit: { type: 'number' },
|
|
85
167
|
},
|
|
86
168
|
},
|
|
87
169
|
get_element_refs: {
|
|
@@ -92,6 +174,7 @@ const TOOL_SCHEMAS = {
|
|
|
92
174
|
selector: { type: 'string' },
|
|
93
175
|
limit: { type: 'number' },
|
|
94
176
|
offset: { type: 'number' },
|
|
177
|
+
maxResponseBytes: { type: 'number' },
|
|
95
178
|
},
|
|
96
179
|
},
|
|
97
180
|
get_dom_subtree: {
|
|
@@ -141,6 +224,27 @@ const TOOL_SCHEMAS = {
|
|
|
141
224
|
maxDepth: { type: 'number' },
|
|
142
225
|
maxBytes: { type: 'number' },
|
|
143
226
|
maxAncestors: { type: 'number' },
|
|
227
|
+
includeDom: { type: 'boolean' },
|
|
228
|
+
includeStyles: { type: 'boolean' },
|
|
229
|
+
includePngDataUrl: { type: 'boolean' },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
get_live_console_logs: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
required: ['sessionId'],
|
|
235
|
+
properties: {
|
|
236
|
+
sessionId: { type: 'string' },
|
|
237
|
+
url: { type: 'string' },
|
|
238
|
+
tabId: { type: 'number' },
|
|
239
|
+
levels: { type: 'array', items: { type: 'string' } },
|
|
240
|
+
contains: { type: 'string' },
|
|
241
|
+
sinceTs: { type: 'number' },
|
|
242
|
+
includeRuntimeErrors: { type: 'boolean' },
|
|
243
|
+
dedupeWindowMs: { type: 'number' },
|
|
244
|
+
limit: { type: 'number' },
|
|
245
|
+
responseProfile: { type: 'string' },
|
|
246
|
+
includeArgs: { type: 'boolean' },
|
|
247
|
+
maxResponseBytes: { type: 'number' },
|
|
144
248
|
},
|
|
145
249
|
},
|
|
146
250
|
explain_last_failure: {
|
|
@@ -170,6 +274,7 @@ const TOOL_SCHEMAS = {
|
|
|
170
274
|
untilTimestamp: { type: 'number' },
|
|
171
275
|
limit: { type: 'number' },
|
|
172
276
|
offset: { type: 'number' },
|
|
277
|
+
maxResponseBytes: { type: 'number' },
|
|
173
278
|
},
|
|
174
279
|
},
|
|
175
280
|
get_snapshot_for_event: {
|
|
@@ -200,14 +305,21 @@ const TOOL_DESCRIPTIONS = {
|
|
|
200
305
|
get_recent_events: 'Read recent events from a session',
|
|
201
306
|
get_navigation_history: 'Read navigation events for a session',
|
|
202
307
|
get_console_events: 'Read console events for a session',
|
|
308
|
+
get_console_summary: 'Summarize console volume and top repeated messages',
|
|
309
|
+
get_event_summary: 'Summarize event volume and type distribution',
|
|
203
310
|
get_error_fingerprints: 'List aggregated error fingerprints',
|
|
204
311
|
get_network_failures: 'List network failures and groupings',
|
|
312
|
+
get_network_calls: 'Query network calls with targeted filters and optional sanitized bodies',
|
|
313
|
+
wait_for_network_call: 'Wait for the next matching network call and return it deterministically',
|
|
314
|
+
get_request_trace: 'Get correlated UI/events/network chain by requestId or traceId',
|
|
315
|
+
get_body_chunk: 'Retrieve a chunk from a stored large body payload',
|
|
205
316
|
get_element_refs: 'Get element references by selector',
|
|
206
317
|
get_dom_subtree: 'Capture a bounded DOM subtree',
|
|
207
318
|
get_dom_document: 'Capture full document as outline or html',
|
|
208
319
|
get_computed_styles: 'Read computed CSS styles for an element',
|
|
209
320
|
get_layout_metrics: 'Read viewport and element layout metrics',
|
|
210
321
|
capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
|
|
322
|
+
get_live_console_logs: 'Read in-memory live console logs for a connected session',
|
|
211
323
|
explain_last_failure: 'Explain the latest failure timeline',
|
|
212
324
|
get_event_correlation: 'Correlate related events by window',
|
|
213
325
|
list_snapshots: 'List snapshot metadata by session/time/trigger',
|
|
@@ -223,9 +335,21 @@ const DEFAULT_REDACTION_SUMMARY = {
|
|
|
223
335
|
const DEFAULT_LIST_LIMIT = 25;
|
|
224
336
|
const DEFAULT_EVENT_LIMIT = 50;
|
|
225
337
|
const MAX_LIMIT = 200;
|
|
338
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024;
|
|
339
|
+
const MAX_RESPONSE_BYTES = 512 * 1024;
|
|
226
340
|
const DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES = 64 * 1024;
|
|
227
341
|
const MAX_SNAPSHOT_ASSET_CHUNK_BYTES = 256 * 1024;
|
|
342
|
+
const DEFAULT_BODY_CHUNK_BYTES = 64 * 1024;
|
|
343
|
+
const MAX_BODY_CHUNK_BYTES = 256 * 1024;
|
|
344
|
+
const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
|
|
345
|
+
const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
|
|
346
|
+
const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
|
|
228
347
|
const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
|
|
348
|
+
const NETWORK_CALL_SELECT_COLUMNS = `
|
|
349
|
+
request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class, response_size_est,
|
|
350
|
+
request_content_type, request_body_text, request_body_json, request_body_bytes, request_body_truncated, request_body_chunk_ref,
|
|
351
|
+
response_content_type, response_body_text, response_body_json, response_body_bytes, response_body_truncated, response_body_chunk_ref
|
|
352
|
+
`;
|
|
229
353
|
const NETWORK_DOMAIN_GROUP_SQL = `
|
|
230
354
|
CASE
|
|
231
355
|
WHEN instr(replace(replace(url, 'https://', ''), 'http://', ''), '/') > 0
|
|
@@ -264,6 +388,62 @@ function resolveOffset(value) {
|
|
|
264
388
|
const floored = Math.floor(value);
|
|
265
389
|
return floored < 0 ? 0 : floored;
|
|
266
390
|
}
|
|
391
|
+
function resolveResponseProfile(value) {
|
|
392
|
+
return value === 'compact' ? 'compact' : 'legacy';
|
|
393
|
+
}
|
|
394
|
+
function resolveMaxResponseBytes(value) {
|
|
395
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
396
|
+
return DEFAULT_MAX_RESPONSE_BYTES;
|
|
397
|
+
}
|
|
398
|
+
const floored = Math.floor(value);
|
|
399
|
+
if (floored < 1_024) {
|
|
400
|
+
return DEFAULT_MAX_RESPONSE_BYTES;
|
|
401
|
+
}
|
|
402
|
+
return Math.min(floored, MAX_RESPONSE_BYTES);
|
|
403
|
+
}
|
|
404
|
+
function estimateJsonBytes(value) {
|
|
405
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf-8');
|
|
406
|
+
}
|
|
407
|
+
function applyByteBudget(items, maxResponseBytes) {
|
|
408
|
+
if (items.length === 0) {
|
|
409
|
+
return {
|
|
410
|
+
items: [],
|
|
411
|
+
responseBytes: 2, // []
|
|
412
|
+
truncatedByBytes: false,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const selected = [];
|
|
416
|
+
let usedBytes = 2; // []
|
|
417
|
+
let truncatedByBytes = false;
|
|
418
|
+
for (const item of items) {
|
|
419
|
+
const itemBytes = estimateJsonBytes(item);
|
|
420
|
+
const separatorBytes = selected.length > 0 ? 1 : 0; // comma
|
|
421
|
+
const nextBytes = usedBytes + separatorBytes + itemBytes;
|
|
422
|
+
if (nextBytes > maxResponseBytes && selected.length > 0) {
|
|
423
|
+
truncatedByBytes = true;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
selected.push(item);
|
|
427
|
+
usedBytes = nextBytes;
|
|
428
|
+
}
|
|
429
|
+
if (!truncatedByBytes && selected.length < items.length) {
|
|
430
|
+
truncatedByBytes = true;
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
items: selected,
|
|
434
|
+
responseBytes: usedBytes,
|
|
435
|
+
truncatedByBytes,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function buildOffsetPagination(offset, returned, hasMore, maxResponseBytes) {
|
|
439
|
+
return {
|
|
440
|
+
offset,
|
|
441
|
+
returned,
|
|
442
|
+
hasMore,
|
|
443
|
+
nextOffset: hasMore ? offset + returned : null,
|
|
444
|
+
maxResponseBytes,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
267
447
|
function readJsonPayload(payloadJson) {
|
|
268
448
|
try {
|
|
269
449
|
const parsed = JSON.parse(payloadJson);
|
|
@@ -303,6 +483,65 @@ function parseRequestedTypes(value) {
|
|
|
303
483
|
.map((entry) => mapRequestedEventType(entry));
|
|
304
484
|
return Array.from(new Set(normalized));
|
|
305
485
|
}
|
|
486
|
+
function normalizeRequestedOrigin(value) {
|
|
487
|
+
if (value === undefined || value === null || value === '') {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
if (typeof value !== 'string') {
|
|
491
|
+
throw new Error('url must be a string');
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const parsed = new URL(value);
|
|
495
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
496
|
+
throw new Error('url must use http:// or https://');
|
|
497
|
+
}
|
|
498
|
+
return parsed.origin;
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
throw new Error('url must be a valid absolute http(s) URL');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function ensureSessionOrOriginFilter(sessionId, origin) {
|
|
505
|
+
if (!sessionId && !origin) {
|
|
506
|
+
throw new Error('sessionId or url is required');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function resolveUrlPrefixFromOrigin(origin) {
|
|
510
|
+
return origin.endsWith('/') ? origin : origin + '/';
|
|
511
|
+
}
|
|
512
|
+
function appendEventOriginFilter(where, params, origin) {
|
|
513
|
+
if (!origin) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const prefix = resolveUrlPrefixFromOrigin(origin);
|
|
517
|
+
where.push(`
|
|
518
|
+
(
|
|
519
|
+
origin = ?
|
|
520
|
+
OR (
|
|
521
|
+
origin IS NULL AND (
|
|
522
|
+
json_extract(payload_json, '$.origin') = ?
|
|
523
|
+
OR json_extract(payload_json, '$.url') = ?
|
|
524
|
+
OR json_extract(payload_json, '$.url') LIKE ?
|
|
525
|
+
OR json_extract(payload_json, '$.to') = ?
|
|
526
|
+
OR json_extract(payload_json, '$.to') LIKE ?
|
|
527
|
+
OR json_extract(payload_json, '$.href') = ?
|
|
528
|
+
OR json_extract(payload_json, '$.href') LIKE ?
|
|
529
|
+
OR json_extract(payload_json, '$.location') = ?
|
|
530
|
+
OR json_extract(payload_json, '$.location') LIKE ?
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
`);
|
|
535
|
+
params.push(origin, origin, origin, `${prefix}%`, origin, `${prefix}%`, origin, `${prefix}%`, origin, `${prefix}%`);
|
|
536
|
+
}
|
|
537
|
+
function appendNetworkOriginFilter(where, params, origin) {
|
|
538
|
+
if (!origin) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const prefix = resolveUrlPrefixFromOrigin(origin);
|
|
542
|
+
where.push('(origin = ? OR (origin IS NULL AND (url = ? OR url LIKE ?)))');
|
|
543
|
+
params.push(origin, origin, `${prefix}%`);
|
|
544
|
+
}
|
|
306
545
|
function resolveLastUrl(payload) {
|
|
307
546
|
const candidates = [payload.url, payload.to, payload.href, payload.location];
|
|
308
547
|
for (const candidate of candidates) {
|
|
@@ -312,13 +551,38 @@ function resolveLastUrl(payload) {
|
|
|
312
551
|
}
|
|
313
552
|
return undefined;
|
|
314
553
|
}
|
|
315
|
-
function mapEventRecord(row) {
|
|
554
|
+
function mapEventRecord(row, profile = 'legacy', options = {}) {
|
|
555
|
+
const payload = readJsonPayload(row.payload_json);
|
|
556
|
+
if (profile === 'compact') {
|
|
557
|
+
const compact = {
|
|
558
|
+
eventId: row.event_id,
|
|
559
|
+
sessionId: row.session_id,
|
|
560
|
+
timestamp: row.ts,
|
|
561
|
+
type: row.type,
|
|
562
|
+
summary: describeEvent(row.type, payload),
|
|
563
|
+
};
|
|
564
|
+
if (row.type === 'console') {
|
|
565
|
+
compact.level = typeof payload.level === 'string' ? payload.level : undefined;
|
|
566
|
+
compact.message = typeof payload.message === 'string' ? payload.message : undefined;
|
|
567
|
+
}
|
|
568
|
+
if (row.type === 'nav') {
|
|
569
|
+
compact.url = resolveLastUrl(payload);
|
|
570
|
+
}
|
|
571
|
+
if (options.includePayload === true) {
|
|
572
|
+
compact.payload = payload;
|
|
573
|
+
}
|
|
574
|
+
return compact;
|
|
575
|
+
}
|
|
316
576
|
return {
|
|
317
577
|
eventId: row.event_id,
|
|
318
578
|
sessionId: row.session_id,
|
|
319
579
|
timestamp: row.ts,
|
|
320
580
|
type: row.type,
|
|
321
|
-
|
|
581
|
+
tabId: row.tab_id ?? (typeof payload.tabId === 'number' ? payload.tabId : undefined),
|
|
582
|
+
origin: row.origin
|
|
583
|
+
?? (typeof payload.origin === 'string' ? payload.origin : undefined)
|
|
584
|
+
?? undefined,
|
|
585
|
+
payload,
|
|
322
586
|
};
|
|
323
587
|
}
|
|
324
588
|
function classifyNetworkFailure(status, errorClass) {
|
|
@@ -376,6 +640,153 @@ function resolveDurationMs(value, fallback, maxValue) {
|
|
|
376
640
|
}
|
|
377
641
|
return Math.min(floored, maxValue);
|
|
378
642
|
}
|
|
643
|
+
function resolveBodyChunkBytes(value) {
|
|
644
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
645
|
+
return DEFAULT_BODY_CHUNK_BYTES;
|
|
646
|
+
}
|
|
647
|
+
const floored = Math.floor(value);
|
|
648
|
+
if (floored < 1) {
|
|
649
|
+
return DEFAULT_BODY_CHUNK_BYTES;
|
|
650
|
+
}
|
|
651
|
+
return Math.min(floored, MAX_BODY_CHUNK_BYTES);
|
|
652
|
+
}
|
|
653
|
+
function resolveTimeoutMs(value, fallback, maxValue) {
|
|
654
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
655
|
+
return fallback;
|
|
656
|
+
}
|
|
657
|
+
const floored = Math.floor(value);
|
|
658
|
+
if (floored < 100) {
|
|
659
|
+
return fallback;
|
|
660
|
+
}
|
|
661
|
+
return Math.min(floored, maxValue);
|
|
662
|
+
}
|
|
663
|
+
function normalizeHttpMethod(value) {
|
|
664
|
+
if (typeof value !== 'string') {
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
const normalized = value.trim().toUpperCase();
|
|
668
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
669
|
+
}
|
|
670
|
+
function normalizeOptionalString(value) {
|
|
671
|
+
if (typeof value !== 'string') {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
const trimmed = value.trim();
|
|
675
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
676
|
+
}
|
|
677
|
+
function normalizeStatusIn(value) {
|
|
678
|
+
if (!Array.isArray(value)) {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
const statuses = value
|
|
682
|
+
.filter((entry) => typeof entry === 'number' && Number.isFinite(entry))
|
|
683
|
+
.map((entry) => Math.floor(entry))
|
|
684
|
+
.filter((entry) => entry >= 100 && entry <= 599);
|
|
685
|
+
return Array.from(new Set(statuses));
|
|
686
|
+
}
|
|
687
|
+
function parseJsonOrUndefined(value) {
|
|
688
|
+
if (!value) {
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
return JSON.parse(value);
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function compileSafeRegex(value) {
|
|
699
|
+
if (!value) {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
return new RegExp(value);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
throw new Error('urlRegex must be a valid regular expression');
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function mapNetworkCallRecord(row, includeBodies) {
|
|
710
|
+
const requestBodyJson = parseJsonOrUndefined(row.request_body_json);
|
|
711
|
+
const responseBodyJson = parseJsonOrUndefined(row.response_body_json);
|
|
712
|
+
return {
|
|
713
|
+
requestId: row.request_id,
|
|
714
|
+
sessionId: row.session_id,
|
|
715
|
+
traceId: row.trace_id ?? undefined,
|
|
716
|
+
tabId: row.tab_id ?? undefined,
|
|
717
|
+
timestamp: row.ts_start,
|
|
718
|
+
durationMs: row.duration_ms ?? undefined,
|
|
719
|
+
method: row.method,
|
|
720
|
+
url: row.url,
|
|
721
|
+
origin: row.origin ?? undefined,
|
|
722
|
+
status: row.status ?? undefined,
|
|
723
|
+
initiator: row.initiator ?? undefined,
|
|
724
|
+
errorType: classifyNetworkFailure(row.status, row.error_class),
|
|
725
|
+
responseSizeEst: row.response_size_est ?? undefined,
|
|
726
|
+
request: {
|
|
727
|
+
contentType: row.request_content_type ?? undefined,
|
|
728
|
+
bodyBytes: row.request_body_bytes ?? undefined,
|
|
729
|
+
truncated: row.request_body_truncated === 1,
|
|
730
|
+
bodyChunkRef: row.request_body_chunk_ref ?? undefined,
|
|
731
|
+
bodyJson: includeBodies ? requestBodyJson : undefined,
|
|
732
|
+
bodyText: includeBodies ? row.request_body_text ?? undefined : undefined,
|
|
733
|
+
},
|
|
734
|
+
response: {
|
|
735
|
+
contentType: row.response_content_type ?? undefined,
|
|
736
|
+
bodyBytes: row.response_body_bytes ?? undefined,
|
|
737
|
+
truncated: row.response_body_truncated === 1,
|
|
738
|
+
bodyChunkRef: row.response_body_chunk_ref ?? undefined,
|
|
739
|
+
bodyJson: includeBodies ? responseBodyJson : undefined,
|
|
740
|
+
bodyText: includeBodies ? row.response_body_text ?? undefined : undefined,
|
|
741
|
+
},
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function mapBodyChunkRecord(row, offset, limit) {
|
|
745
|
+
const fullBuffer = Buffer.from(row.body_text, 'utf-8');
|
|
746
|
+
if (offset >= fullBuffer.byteLength) {
|
|
747
|
+
return {
|
|
748
|
+
chunkRef: row.chunk_ref,
|
|
749
|
+
sessionId: row.session_id,
|
|
750
|
+
requestId: row.request_id ?? undefined,
|
|
751
|
+
traceId: row.trace_id ?? undefined,
|
|
752
|
+
bodyKind: row.body_kind,
|
|
753
|
+
contentType: row.content_type ?? undefined,
|
|
754
|
+
totalBytes: fullBuffer.byteLength,
|
|
755
|
+
offset,
|
|
756
|
+
returnedBytes: 0,
|
|
757
|
+
hasMore: false,
|
|
758
|
+
nextOffset: null,
|
|
759
|
+
chunkText: '',
|
|
760
|
+
truncated: row.truncated === 1,
|
|
761
|
+
createdAt: row.created_at,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const chunkBuffer = fullBuffer.subarray(offset, Math.min(offset + limit, fullBuffer.byteLength));
|
|
765
|
+
const returnedBytes = chunkBuffer.byteLength;
|
|
766
|
+
const nextOffset = offset + returnedBytes;
|
|
767
|
+
const hasMore = nextOffset < fullBuffer.byteLength;
|
|
768
|
+
return {
|
|
769
|
+
chunkRef: row.chunk_ref,
|
|
770
|
+
sessionId: row.session_id,
|
|
771
|
+
requestId: row.request_id ?? undefined,
|
|
772
|
+
traceId: row.trace_id ?? undefined,
|
|
773
|
+
bodyKind: row.body_kind,
|
|
774
|
+
contentType: row.content_type ?? undefined,
|
|
775
|
+
totalBytes: fullBuffer.byteLength,
|
|
776
|
+
offset,
|
|
777
|
+
returnedBytes,
|
|
778
|
+
hasMore,
|
|
779
|
+
nextOffset: hasMore ? nextOffset : null,
|
|
780
|
+
chunkText: chunkBuffer.toString('utf-8'),
|
|
781
|
+
truncated: row.truncated === 1,
|
|
782
|
+
createdAt: row.created_at,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function sleep(ms) {
|
|
786
|
+
return new Promise((resolvePromise) => {
|
|
787
|
+
setTimeout(resolvePromise, ms);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
379
790
|
function normalizeAssetPath(pathValue) {
|
|
380
791
|
return pathValue.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, '');
|
|
381
792
|
}
|
|
@@ -528,6 +939,59 @@ function asStringArray(value, maxItems) {
|
|
|
528
939
|
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
529
940
|
.slice(0, maxItems);
|
|
530
941
|
}
|
|
942
|
+
const LIVE_CONSOLE_LEVELS = new Set(['log', 'info', 'warn', 'error', 'debug', 'trace']);
|
|
943
|
+
function resolveLiveConsoleLevels(value) {
|
|
944
|
+
const levels = asStringArray(value, 16)
|
|
945
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
946
|
+
.filter((entry) => LIVE_CONSOLE_LEVELS.has(entry));
|
|
947
|
+
return Array.from(new Set(levels));
|
|
948
|
+
}
|
|
949
|
+
function asRecordArray(value) {
|
|
950
|
+
if (!Array.isArray(value)) {
|
|
951
|
+
return [];
|
|
952
|
+
}
|
|
953
|
+
return value.filter((entry) => typeof entry === 'object' && entry !== null);
|
|
954
|
+
}
|
|
955
|
+
function mapLiveConsoleLogRecord(log, profile, options = {}) {
|
|
956
|
+
if (profile === 'compact') {
|
|
957
|
+
const compact = {
|
|
958
|
+
timestamp: typeof log.timestamp === 'number'
|
|
959
|
+
? log.timestamp
|
|
960
|
+
: typeof log.ts === 'number'
|
|
961
|
+
? log.ts
|
|
962
|
+
: undefined,
|
|
963
|
+
level: typeof log.level === 'string' ? log.level : undefined,
|
|
964
|
+
message: typeof log.message === 'string' ? log.message : '',
|
|
965
|
+
};
|
|
966
|
+
if (typeof log.count === 'number') {
|
|
967
|
+
compact.count = log.count;
|
|
968
|
+
}
|
|
969
|
+
if (typeof log.firstTimestamp === 'number') {
|
|
970
|
+
compact.firstTimestamp = log.firstTimestamp;
|
|
971
|
+
}
|
|
972
|
+
if (typeof log.lastTimestamp === 'number') {
|
|
973
|
+
compact.lastTimestamp = log.lastTimestamp;
|
|
974
|
+
}
|
|
975
|
+
if (options.includeArgs === true && Array.isArray(log.args)) {
|
|
976
|
+
compact.args = log.args;
|
|
977
|
+
}
|
|
978
|
+
return compact;
|
|
979
|
+
}
|
|
980
|
+
return log;
|
|
981
|
+
}
|
|
982
|
+
function resolveOptionalTabId(value) {
|
|
983
|
+
if (value === undefined || value === null || value === '') {
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
987
|
+
throw new Error('tabId must be an integer');
|
|
988
|
+
}
|
|
989
|
+
const tabId = Math.floor(value);
|
|
990
|
+
if (!Number.isInteger(tabId) || tabId < 0) {
|
|
991
|
+
throw new Error('tabId must be an integer');
|
|
992
|
+
}
|
|
993
|
+
return tabId;
|
|
994
|
+
}
|
|
531
995
|
function isLiveSessionDisconnectedMessage(message) {
|
|
532
996
|
const normalized = message.toLowerCase();
|
|
533
997
|
return normalized.includes('no active extension connection')
|
|
@@ -570,6 +1034,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
570
1034
|
const sinceMinutes = typeof input.sinceMinutes === 'number' ? input.sinceMinutes : undefined;
|
|
571
1035
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
572
1036
|
const offset = resolveOffset(input.offset);
|
|
1037
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
573
1038
|
const where = [];
|
|
574
1039
|
const params = [];
|
|
575
1040
|
if (sinceMinutes !== undefined && Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
|
|
@@ -581,6 +1046,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
581
1046
|
SELECT
|
|
582
1047
|
session_id,
|
|
583
1048
|
created_at,
|
|
1049
|
+
paused_at,
|
|
584
1050
|
ended_at,
|
|
585
1051
|
tab_id,
|
|
586
1052
|
window_id,
|
|
@@ -598,11 +1064,13 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
598
1064
|
LIMIT ? OFFSET ?
|
|
599
1065
|
`;
|
|
600
1066
|
const rows = db.prepare(sql).all(...params, limit + 1, offset);
|
|
601
|
-
const
|
|
1067
|
+
const truncatedByLimit = rows.length > limit;
|
|
602
1068
|
const sessions = rows.slice(0, limit).map((row) => ({
|
|
603
1069
|
sessionId: row.session_id,
|
|
604
1070
|
createdAt: row.created_at,
|
|
1071
|
+
pausedAt: row.paused_at ?? undefined,
|
|
605
1072
|
endedAt: row.ended_at ?? undefined,
|
|
1073
|
+
status: row.ended_at ? 'ended' : row.paused_at ? 'paused' : 'active',
|
|
606
1074
|
tabId: row.tab_id ?? undefined,
|
|
607
1075
|
windowId: row.window_id ?? undefined,
|
|
608
1076
|
urlStart: row.url_start ?? undefined,
|
|
@@ -635,17 +1103,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
635
1103
|
};
|
|
636
1104
|
})(),
|
|
637
1105
|
}));
|
|
1106
|
+
const bytePage = applyByteBudget(sessions, maxResponseBytes);
|
|
1107
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
638
1108
|
return {
|
|
639
1109
|
...createBaseResponse(),
|
|
640
1110
|
limitsApplied: {
|
|
641
1111
|
maxResults: limit,
|
|
642
1112
|
truncated,
|
|
643
1113
|
},
|
|
644
|
-
pagination:
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
},
|
|
648
|
-
sessions,
|
|
1114
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1115
|
+
responseBytes: bytePage.responseBytes,
|
|
1116
|
+
sessions: bytePage.items,
|
|
649
1117
|
};
|
|
650
1118
|
},
|
|
651
1119
|
get_session_summary: async (input) => {
|
|
@@ -713,14 +1181,21 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
713
1181
|
get_recent_events: async (input) => {
|
|
714
1182
|
const db = getDb();
|
|
715
1183
|
const sessionId = getSessionId(input);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
1184
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
1185
|
+
ensureSessionOrOriginFilter(sessionId, origin);
|
|
719
1186
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
720
1187
|
const offset = resolveOffset(input.offset);
|
|
1188
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1189
|
+
const responseProfile = resolveResponseProfile(input.responseProfile);
|
|
1190
|
+
const includePayload = responseProfile === 'compact' && input.includePayload === true;
|
|
721
1191
|
const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
|
|
722
|
-
const params = [
|
|
723
|
-
const where = [
|
|
1192
|
+
const params = [];
|
|
1193
|
+
const where = [];
|
|
1194
|
+
if (sessionId) {
|
|
1195
|
+
where.push('session_id = ?');
|
|
1196
|
+
params.push(sessionId);
|
|
1197
|
+
}
|
|
1198
|
+
appendEventOriginFilter(where, params, origin);
|
|
724
1199
|
if (requestedTypes.length > 0) {
|
|
725
1200
|
const placeholders = requestedTypes.map(() => '?').join(', ');
|
|
726
1201
|
where.push(`type IN (${placeholders})`);
|
|
@@ -728,94 +1203,270 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
728
1203
|
}
|
|
729
1204
|
const rows = db
|
|
730
1205
|
.prepare(`
|
|
731
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
1206
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
732
1207
|
FROM events
|
|
733
1208
|
WHERE ${where.join(' AND ')}
|
|
734
1209
|
ORDER BY ts DESC
|
|
735
1210
|
LIMIT ? OFFSET ?
|
|
736
1211
|
`)
|
|
737
1212
|
.all(...params, limit + 1, offset);
|
|
738
|
-
const
|
|
1213
|
+
const truncatedByLimit = rows.length > limit;
|
|
1214
|
+
const events = rows
|
|
1215
|
+
.slice(0, limit)
|
|
1216
|
+
.map((row) => mapEventRecord(row, responseProfile, { includePayload }));
|
|
1217
|
+
const bytePage = applyByteBudget(events, maxResponseBytes);
|
|
1218
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
739
1219
|
return {
|
|
740
1220
|
...createBaseResponse(sessionId),
|
|
741
1221
|
limitsApplied: {
|
|
742
1222
|
maxResults: limit,
|
|
743
1223
|
truncated,
|
|
744
1224
|
},
|
|
745
|
-
pagination:
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
|
|
1225
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1226
|
+
responseProfile,
|
|
1227
|
+
responseBytes: bytePage.responseBytes,
|
|
1228
|
+
events: bytePage.items,
|
|
750
1229
|
};
|
|
751
1230
|
},
|
|
752
1231
|
get_navigation_history: async (input) => {
|
|
753
1232
|
const db = getDb();
|
|
754
1233
|
const sessionId = getSessionId(input);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
1234
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
1235
|
+
ensureSessionOrOriginFilter(sessionId, origin);
|
|
758
1236
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
759
1237
|
const offset = resolveOffset(input.offset);
|
|
1238
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1239
|
+
const responseProfile = resolveResponseProfile(input.responseProfile);
|
|
1240
|
+
const includePayload = responseProfile === 'compact' && input.includePayload === true;
|
|
1241
|
+
const params = [];
|
|
1242
|
+
const where = ["type = 'nav'"];
|
|
1243
|
+
if (sessionId) {
|
|
1244
|
+
where.push('session_id = ?');
|
|
1245
|
+
params.push(sessionId);
|
|
1246
|
+
}
|
|
1247
|
+
appendEventOriginFilter(where, params, origin);
|
|
760
1248
|
const rows = db
|
|
761
1249
|
.prepare(`
|
|
762
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
1250
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
763
1251
|
FROM events
|
|
764
|
-
WHERE
|
|
1252
|
+
WHERE ${where.join(' AND ')}
|
|
765
1253
|
ORDER BY ts DESC
|
|
766
1254
|
LIMIT ? OFFSET ?
|
|
767
1255
|
`)
|
|
768
|
-
.all(
|
|
769
|
-
const
|
|
1256
|
+
.all(...params, limit + 1, offset);
|
|
1257
|
+
const truncatedByLimit = rows.length > limit;
|
|
1258
|
+
const events = rows
|
|
1259
|
+
.slice(0, limit)
|
|
1260
|
+
.map((row) => mapEventRecord(row, responseProfile, { includePayload }));
|
|
1261
|
+
const bytePage = applyByteBudget(events, maxResponseBytes);
|
|
1262
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
770
1263
|
return {
|
|
771
1264
|
...createBaseResponse(sessionId),
|
|
772
1265
|
limitsApplied: {
|
|
773
1266
|
maxResults: limit,
|
|
774
1267
|
truncated,
|
|
775
1268
|
},
|
|
776
|
-
pagination:
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
|
|
1269
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1270
|
+
responseProfile,
|
|
1271
|
+
responseBytes: bytePage.responseBytes,
|
|
1272
|
+
events: bytePage.items,
|
|
781
1273
|
};
|
|
782
1274
|
},
|
|
783
1275
|
get_console_events: async (input) => {
|
|
784
1276
|
const db = getDb();
|
|
785
1277
|
const sessionId = getSessionId(input);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
1278
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
1279
|
+
ensureSessionOrOriginFilter(sessionId, origin);
|
|
789
1280
|
const level = typeof input.level === 'string' ? input.level : undefined;
|
|
790
1281
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
791
1282
|
const offset = resolveOffset(input.offset);
|
|
792
|
-
const
|
|
793
|
-
|
|
1283
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1284
|
+
const responseProfile = resolveResponseProfile(input.responseProfile);
|
|
1285
|
+
const includePayload = responseProfile === 'compact' && input.includePayload === true;
|
|
1286
|
+
const params = [];
|
|
1287
|
+
const where = ["type = 'console'"];
|
|
1288
|
+
if (sessionId) {
|
|
1289
|
+
where.push('session_id = ?');
|
|
1290
|
+
params.push(sessionId);
|
|
1291
|
+
}
|
|
1292
|
+
appendEventOriginFilter(where, params, origin);
|
|
794
1293
|
if (level) {
|
|
795
|
-
|
|
1294
|
+
where.push("json_extract(payload_json, '$.level') = ?");
|
|
796
1295
|
params.push(level);
|
|
797
1296
|
}
|
|
798
1297
|
const rows = db
|
|
799
1298
|
.prepare(`
|
|
800
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
1299
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
801
1300
|
FROM events
|
|
802
|
-
WHERE
|
|
1301
|
+
WHERE ${where.join(' AND ')}
|
|
803
1302
|
ORDER BY ts DESC
|
|
804
1303
|
LIMIT ? OFFSET ?
|
|
805
1304
|
`)
|
|
806
1305
|
.all(...params, limit + 1, offset);
|
|
807
|
-
const
|
|
1306
|
+
const truncatedByLimit = rows.length > limit;
|
|
1307
|
+
const events = rows
|
|
1308
|
+
.slice(0, limit)
|
|
1309
|
+
.map((row) => mapEventRecord(row, responseProfile, { includePayload }));
|
|
1310
|
+
const bytePage = applyByteBudget(events, maxResponseBytes);
|
|
1311
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
808
1312
|
return {
|
|
809
1313
|
...createBaseResponse(sessionId),
|
|
810
1314
|
limitsApplied: {
|
|
811
1315
|
maxResults: limit,
|
|
812
1316
|
truncated,
|
|
813
1317
|
},
|
|
814
|
-
pagination:
|
|
815
|
-
|
|
816
|
-
|
|
1318
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1319
|
+
responseProfile,
|
|
1320
|
+
responseBytes: bytePage.responseBytes,
|
|
1321
|
+
events: bytePage.items,
|
|
1322
|
+
};
|
|
1323
|
+
},
|
|
1324
|
+
get_console_summary: async (input) => {
|
|
1325
|
+
const db = getDb();
|
|
1326
|
+
const sessionId = getSessionId(input);
|
|
1327
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
1328
|
+
ensureSessionOrOriginFilter(sessionId, origin);
|
|
1329
|
+
const level = typeof input.level === 'string' && input.level.length > 0 ? input.level : undefined;
|
|
1330
|
+
const sinceMinutes = typeof input.sinceMinutes === 'number' && Number.isFinite(input.sinceMinutes)
|
|
1331
|
+
? Math.floor(input.sinceMinutes)
|
|
1332
|
+
: undefined;
|
|
1333
|
+
const limit = resolveLimit(input.limit, 10);
|
|
1334
|
+
const where = ["type = 'console'"];
|
|
1335
|
+
const params = [];
|
|
1336
|
+
if (sessionId) {
|
|
1337
|
+
where.push('session_id = ?');
|
|
1338
|
+
params.push(sessionId);
|
|
1339
|
+
}
|
|
1340
|
+
appendEventOriginFilter(where, params, origin);
|
|
1341
|
+
if (level) {
|
|
1342
|
+
where.push("json_extract(payload_json, '$.level') = ?");
|
|
1343
|
+
params.push(level);
|
|
1344
|
+
}
|
|
1345
|
+
if (sinceMinutes !== undefined && sinceMinutes > 0) {
|
|
1346
|
+
where.push('ts >= ?');
|
|
1347
|
+
params.push(Date.now() - sinceMinutes * 60_000);
|
|
1348
|
+
}
|
|
1349
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
1350
|
+
const totals = db
|
|
1351
|
+
.prepare(`
|
|
1352
|
+
SELECT
|
|
1353
|
+
COUNT(*) AS total,
|
|
1354
|
+
SUM(CASE WHEN json_extract(payload_json, '$.level') = 'log' THEN 1 ELSE 0 END) AS log_count,
|
|
1355
|
+
SUM(CASE WHEN json_extract(payload_json, '$.level') = 'info' THEN 1 ELSE 0 END) AS info_count,
|
|
1356
|
+
SUM(CASE WHEN json_extract(payload_json, '$.level') = 'warn' THEN 1 ELSE 0 END) AS warn_count,
|
|
1357
|
+
SUM(CASE WHEN json_extract(payload_json, '$.level') = 'error' THEN 1 ELSE 0 END) AS error_count,
|
|
1358
|
+
SUM(CASE WHEN json_extract(payload_json, '$.level') = 'debug' THEN 1 ELSE 0 END) AS debug_count,
|
|
1359
|
+
SUM(CASE WHEN json_extract(payload_json, '$.level') = 'trace' THEN 1 ELSE 0 END) AS trace_count,
|
|
1360
|
+
MIN(ts) AS first_ts,
|
|
1361
|
+
MAX(ts) AS last_ts
|
|
1362
|
+
FROM events
|
|
1363
|
+
${whereClause}
|
|
1364
|
+
`)
|
|
1365
|
+
.get(...params);
|
|
1366
|
+
const topMessages = db
|
|
1367
|
+
.prepare(`
|
|
1368
|
+
SELECT
|
|
1369
|
+
COALESCE(json_extract(payload_json, '$.message'), 'console event') AS message,
|
|
1370
|
+
COALESCE(json_extract(payload_json, '$.level'), 'log') AS level,
|
|
1371
|
+
COUNT(*) AS count,
|
|
1372
|
+
MIN(ts) AS first_ts,
|
|
1373
|
+
MAX(ts) AS last_ts
|
|
1374
|
+
FROM events
|
|
1375
|
+
${whereClause}
|
|
1376
|
+
GROUP BY message, level
|
|
1377
|
+
ORDER BY count DESC, last_ts DESC
|
|
1378
|
+
LIMIT ?
|
|
1379
|
+
`)
|
|
1380
|
+
.all(...params, limit);
|
|
1381
|
+
return {
|
|
1382
|
+
...createBaseResponse(sessionId),
|
|
1383
|
+
limitsApplied: {
|
|
1384
|
+
maxResults: limit,
|
|
1385
|
+
truncated: false,
|
|
1386
|
+
},
|
|
1387
|
+
counts: {
|
|
1388
|
+
total: totals.total ?? 0,
|
|
1389
|
+
byLevel: {
|
|
1390
|
+
log: totals.log_count ?? 0,
|
|
1391
|
+
info: totals.info_count ?? 0,
|
|
1392
|
+
warn: totals.warn_count ?? 0,
|
|
1393
|
+
error: totals.error_count ?? 0,
|
|
1394
|
+
debug: totals.debug_count ?? 0,
|
|
1395
|
+
trace: totals.trace_count ?? 0,
|
|
1396
|
+
},
|
|
817
1397
|
},
|
|
818
|
-
|
|
1398
|
+
firstSeenAt: totals.first_ts ?? undefined,
|
|
1399
|
+
lastSeenAt: totals.last_ts ?? undefined,
|
|
1400
|
+
topMessages: topMessages.map((entry) => ({
|
|
1401
|
+
level: entry.level,
|
|
1402
|
+
message: entry.message,
|
|
1403
|
+
count: entry.count,
|
|
1404
|
+
firstSeenAt: entry.first_ts,
|
|
1405
|
+
lastSeenAt: entry.last_ts,
|
|
1406
|
+
})),
|
|
1407
|
+
};
|
|
1408
|
+
},
|
|
1409
|
+
get_event_summary: async (input) => {
|
|
1410
|
+
const db = getDb();
|
|
1411
|
+
const sessionId = getSessionId(input);
|
|
1412
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
1413
|
+
ensureSessionOrOriginFilter(sessionId, origin);
|
|
1414
|
+
const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
|
|
1415
|
+
const sinceMinutes = typeof input.sinceMinutes === 'number' && Number.isFinite(input.sinceMinutes)
|
|
1416
|
+
? Math.floor(input.sinceMinutes)
|
|
1417
|
+
: undefined;
|
|
1418
|
+
const limit = resolveLimit(input.limit, 20);
|
|
1419
|
+
const where = [];
|
|
1420
|
+
const params = [];
|
|
1421
|
+
if (sessionId) {
|
|
1422
|
+
where.push('session_id = ?');
|
|
1423
|
+
params.push(sessionId);
|
|
1424
|
+
}
|
|
1425
|
+
appendEventOriginFilter(where, params, origin);
|
|
1426
|
+
if (requestedTypes.length > 0) {
|
|
1427
|
+
const placeholders = requestedTypes.map(() => '?').join(', ');
|
|
1428
|
+
where.push(`type IN (${placeholders})`);
|
|
1429
|
+
params.push(...requestedTypes);
|
|
1430
|
+
}
|
|
1431
|
+
if (sinceMinutes !== undefined && sinceMinutes > 0) {
|
|
1432
|
+
where.push('ts >= ?');
|
|
1433
|
+
params.push(Date.now() - sinceMinutes * 60_000);
|
|
1434
|
+
}
|
|
1435
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
1436
|
+
const totals = db
|
|
1437
|
+
.prepare(`
|
|
1438
|
+
SELECT COUNT(*) AS total, MIN(ts) AS first_ts, MAX(ts) AS last_ts
|
|
1439
|
+
FROM events
|
|
1440
|
+
${whereClause}
|
|
1441
|
+
`)
|
|
1442
|
+
.get(...params);
|
|
1443
|
+
const byType = db
|
|
1444
|
+
.prepare(`
|
|
1445
|
+
SELECT type, COUNT(*) AS count, MIN(ts) AS first_ts, MAX(ts) AS last_ts
|
|
1446
|
+
FROM events
|
|
1447
|
+
${whereClause}
|
|
1448
|
+
GROUP BY type
|
|
1449
|
+
ORDER BY count DESC, last_ts DESC
|
|
1450
|
+
LIMIT ?
|
|
1451
|
+
`)
|
|
1452
|
+
.all(...params, limit);
|
|
1453
|
+
return {
|
|
1454
|
+
...createBaseResponse(sessionId),
|
|
1455
|
+
limitsApplied: {
|
|
1456
|
+
maxResults: limit,
|
|
1457
|
+
truncated: false,
|
|
1458
|
+
},
|
|
1459
|
+
counts: {
|
|
1460
|
+
total: totals.total ?? 0,
|
|
1461
|
+
},
|
|
1462
|
+
firstSeenAt: totals.first_ts ?? undefined,
|
|
1463
|
+
lastSeenAt: totals.last_ts ?? undefined,
|
|
1464
|
+
byType: byType.map((entry) => ({
|
|
1465
|
+
type: entry.type,
|
|
1466
|
+
count: entry.count,
|
|
1467
|
+
firstSeenAt: entry.first_ts,
|
|
1468
|
+
lastSeenAt: entry.last_ts,
|
|
1469
|
+
})),
|
|
819
1470
|
};
|
|
820
1471
|
},
|
|
821
1472
|
get_error_fingerprints: async (input) => {
|
|
@@ -826,6 +1477,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
826
1477
|
: undefined;
|
|
827
1478
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
828
1479
|
const offset = resolveOffset(input.offset);
|
|
1480
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
829
1481
|
const params = [];
|
|
830
1482
|
const where = [];
|
|
831
1483
|
if (sessionId) {
|
|
@@ -846,35 +1498,39 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
846
1498
|
LIMIT ? OFFSET ?
|
|
847
1499
|
`)
|
|
848
1500
|
.all(...params, limit + 1, offset);
|
|
849
|
-
const
|
|
1501
|
+
const truncatedByLimit = rows.length > limit;
|
|
1502
|
+
const fingerprints = rows.slice(0, limit).map((row) => ({
|
|
1503
|
+
fingerprint: row.fingerprint,
|
|
1504
|
+
sessionId: row.session_id,
|
|
1505
|
+
count: row.count,
|
|
1506
|
+
sampleMessage: row.sample_message,
|
|
1507
|
+
sampleStack: row.sample_stack ?? undefined,
|
|
1508
|
+
firstSeenAt: row.first_seen_at,
|
|
1509
|
+
lastSeenAt: row.last_seen_at,
|
|
1510
|
+
}));
|
|
1511
|
+
const bytePage = applyByteBudget(fingerprints, maxResponseBytes);
|
|
1512
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
850
1513
|
return {
|
|
851
1514
|
...createBaseResponse(sessionId),
|
|
852
1515
|
limitsApplied: {
|
|
853
1516
|
maxResults: limit,
|
|
854
1517
|
truncated,
|
|
855
1518
|
},
|
|
856
|
-
pagination:
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
},
|
|
860
|
-
fingerprints: rows.slice(0, limit).map((row) => ({
|
|
861
|
-
fingerprint: row.fingerprint,
|
|
862
|
-
sessionId: row.session_id,
|
|
863
|
-
count: row.count,
|
|
864
|
-
sampleMessage: row.sample_message,
|
|
865
|
-
sampleStack: row.sample_stack ?? undefined,
|
|
866
|
-
firstSeenAt: row.first_seen_at,
|
|
867
|
-
lastSeenAt: row.last_seen_at,
|
|
868
|
-
})),
|
|
1519
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1520
|
+
responseBytes: bytePage.responseBytes,
|
|
1521
|
+
fingerprints: bytePage.items,
|
|
869
1522
|
};
|
|
870
1523
|
},
|
|
871
1524
|
get_network_failures: async (input) => {
|
|
872
1525
|
const db = getDb();
|
|
873
1526
|
const sessionId = typeof input.sessionId === 'string' ? input.sessionId : undefined;
|
|
1527
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
1528
|
+
ensureSessionOrOriginFilter(sessionId, origin);
|
|
874
1529
|
const groupBy = typeof input.groupBy === 'string' ? input.groupBy : undefined;
|
|
875
1530
|
const errorType = typeof input.errorType === 'string' ? input.errorType : undefined;
|
|
876
1531
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
877
1532
|
const offset = resolveOffset(input.offset);
|
|
1533
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
878
1534
|
const params = [];
|
|
879
1535
|
const where = [];
|
|
880
1536
|
const errorFilter = buildNetworkFailureFilter(errorType);
|
|
@@ -882,6 +1538,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
882
1538
|
where.push('session_id = ?');
|
|
883
1539
|
params.push(sessionId);
|
|
884
1540
|
}
|
|
1541
|
+
appendNetworkOriginFilter(where, params, origin);
|
|
885
1542
|
where.push(errorFilter);
|
|
886
1543
|
if (errorFilter === 'error_class = ?' && errorType) {
|
|
887
1544
|
params.push(errorType);
|
|
@@ -907,57 +1564,327 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
907
1564
|
LIMIT ? OFFSET ?
|
|
908
1565
|
`)
|
|
909
1566
|
.all(...params, limit + 1, offset);
|
|
910
|
-
const
|
|
1567
|
+
const truncatedByLimit = rows.length > limit;
|
|
1568
|
+
const groups = rows.slice(0, limit).map((row) => ({
|
|
1569
|
+
key: row.group_key,
|
|
1570
|
+
count: row.count,
|
|
1571
|
+
firstSeenAt: row.first_ts,
|
|
1572
|
+
lastSeenAt: row.last_ts,
|
|
1573
|
+
}));
|
|
1574
|
+
const bytePage = applyByteBudget(groups, maxResponseBytes);
|
|
1575
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
911
1576
|
return {
|
|
912
1577
|
...createBaseResponse(sessionId),
|
|
913
1578
|
limitsApplied: {
|
|
914
1579
|
maxResults: limit,
|
|
915
1580
|
truncated,
|
|
916
1581
|
},
|
|
917
|
-
pagination:
|
|
918
|
-
|
|
919
|
-
returned: Math.min(rows.length, limit),
|
|
920
|
-
},
|
|
1582
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1583
|
+
responseBytes: bytePage.responseBytes,
|
|
921
1584
|
groupBy,
|
|
922
|
-
groups:
|
|
923
|
-
key: row.group_key,
|
|
924
|
-
count: row.count,
|
|
925
|
-
firstSeenAt: row.first_ts,
|
|
926
|
-
lastSeenAt: row.last_ts,
|
|
927
|
-
})),
|
|
1585
|
+
groups: bytePage.items,
|
|
928
1586
|
};
|
|
929
1587
|
}
|
|
930
1588
|
const rows = db
|
|
931
1589
|
.prepare(`
|
|
932
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
|
|
1590
|
+
SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
|
|
933
1591
|
FROM network
|
|
934
1592
|
${whereClause}
|
|
935
1593
|
ORDER BY ts_start DESC
|
|
936
1594
|
LIMIT ? OFFSET ?
|
|
937
1595
|
`)
|
|
938
1596
|
.all(...params, limit + 1, offset);
|
|
939
|
-
const
|
|
1597
|
+
const truncatedByLimit = rows.length > limit;
|
|
1598
|
+
const failures = rows.slice(0, limit).map((row) => ({
|
|
1599
|
+
requestId: row.request_id,
|
|
1600
|
+
sessionId: row.session_id,
|
|
1601
|
+
traceId: row.trace_id ?? undefined,
|
|
1602
|
+
tabId: row.tab_id ?? undefined,
|
|
1603
|
+
timestamp: row.ts_start,
|
|
1604
|
+
durationMs: row.duration_ms ?? undefined,
|
|
1605
|
+
method: row.method,
|
|
1606
|
+
url: row.url,
|
|
1607
|
+
origin: row.origin ?? undefined,
|
|
1608
|
+
status: row.status ?? undefined,
|
|
1609
|
+
initiator: row.initiator ?? undefined,
|
|
1610
|
+
errorType: classifyNetworkFailure(row.status, row.error_class),
|
|
1611
|
+
}));
|
|
1612
|
+
const bytePage = applyByteBudget(failures, maxResponseBytes);
|
|
1613
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
940
1614
|
return {
|
|
941
1615
|
...createBaseResponse(sessionId),
|
|
942
1616
|
limitsApplied: {
|
|
943
1617
|
maxResults: limit,
|
|
944
1618
|
truncated,
|
|
945
1619
|
},
|
|
946
|
-
pagination:
|
|
947
|
-
|
|
948
|
-
|
|
1620
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1621
|
+
responseBytes: bytePage.responseBytes,
|
|
1622
|
+
failures: bytePage.items,
|
|
1623
|
+
};
|
|
1624
|
+
},
|
|
1625
|
+
get_network_calls: async (input) => {
|
|
1626
|
+
const db = getDb();
|
|
1627
|
+
const sessionId = getSessionId(input);
|
|
1628
|
+
if (!sessionId) {
|
|
1629
|
+
throw new Error('sessionId is required');
|
|
1630
|
+
}
|
|
1631
|
+
const includeBodies = input.includeBodies === true;
|
|
1632
|
+
const urlContains = normalizeOptionalString(input.urlContains);
|
|
1633
|
+
const urlRegex = compileSafeRegex(normalizeOptionalString(input.urlRegex));
|
|
1634
|
+
const method = normalizeHttpMethod(input.method);
|
|
1635
|
+
const statusIn = normalizeStatusIn(input.statusIn);
|
|
1636
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
1637
|
+
const timeFrom = resolveOptionalTimestamp(input.timeFrom);
|
|
1638
|
+
const timeTo = resolveOptionalTimestamp(input.timeTo);
|
|
1639
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
1640
|
+
const offset = resolveOffset(input.offset);
|
|
1641
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1642
|
+
if (timeFrom !== undefined && timeTo !== undefined && timeFrom > timeTo) {
|
|
1643
|
+
throw new Error('timeFrom must be <= timeTo');
|
|
1644
|
+
}
|
|
1645
|
+
const where = ['session_id = ?'];
|
|
1646
|
+
const params = [sessionId];
|
|
1647
|
+
if (urlContains) {
|
|
1648
|
+
where.push('url LIKE ?');
|
|
1649
|
+
params.push(`%${urlContains}%`);
|
|
1650
|
+
}
|
|
1651
|
+
if (method) {
|
|
1652
|
+
where.push('method = ?');
|
|
1653
|
+
params.push(method);
|
|
1654
|
+
}
|
|
1655
|
+
if (statusIn.length > 0) {
|
|
1656
|
+
where.push(`status IN (${statusIn.map(() => '?').join(', ')})`);
|
|
1657
|
+
params.push(...statusIn);
|
|
1658
|
+
}
|
|
1659
|
+
if (tabId !== undefined) {
|
|
1660
|
+
where.push('tab_id = ?');
|
|
1661
|
+
params.push(tabId);
|
|
1662
|
+
}
|
|
1663
|
+
if (timeFrom !== undefined) {
|
|
1664
|
+
where.push('ts_start >= ?');
|
|
1665
|
+
params.push(timeFrom);
|
|
1666
|
+
}
|
|
1667
|
+
if (timeTo !== undefined) {
|
|
1668
|
+
where.push('ts_start <= ?');
|
|
1669
|
+
params.push(timeTo);
|
|
1670
|
+
}
|
|
1671
|
+
const whereClause = `WHERE ${where.join(' AND ')}`;
|
|
1672
|
+
if (!urlRegex) {
|
|
1673
|
+
const rows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
1674
|
+
FROM network
|
|
1675
|
+
${whereClause}
|
|
1676
|
+
ORDER BY ts_start DESC
|
|
1677
|
+
LIMIT ? OFFSET ?`).all(...params, limit + 1, offset);
|
|
1678
|
+
const truncatedByLimit = rows.length > limit;
|
|
1679
|
+
const calls = rows
|
|
1680
|
+
.slice(0, limit)
|
|
1681
|
+
.map((row) => mapNetworkCallRecord(row, includeBodies));
|
|
1682
|
+
const bytePage = applyByteBudget(calls, maxResponseBytes);
|
|
1683
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
1684
|
+
return {
|
|
1685
|
+
...createBaseResponse(sessionId),
|
|
1686
|
+
limitsApplied: {
|
|
1687
|
+
maxResults: limit,
|
|
1688
|
+
truncated,
|
|
1689
|
+
},
|
|
1690
|
+
filtersApplied: {
|
|
1691
|
+
sessionId,
|
|
1692
|
+
urlContains,
|
|
1693
|
+
method,
|
|
1694
|
+
statusIn,
|
|
1695
|
+
tabId,
|
|
1696
|
+
timeFrom,
|
|
1697
|
+
timeTo,
|
|
1698
|
+
includeBodies,
|
|
1699
|
+
},
|
|
1700
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1701
|
+
responseBytes: bytePage.responseBytes,
|
|
1702
|
+
calls: bytePage.items,
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
const regexScanLimit = Math.min(Math.max(limit + offset + 200, 500), 5000);
|
|
1706
|
+
const regex = urlRegex;
|
|
1707
|
+
const regexRows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
1708
|
+
FROM network
|
|
1709
|
+
${whereClause}
|
|
1710
|
+
ORDER BY ts_start DESC
|
|
1711
|
+
LIMIT ?`).all(...params, regexScanLimit);
|
|
1712
|
+
const matched = regexRows.filter((row) => regex.test(row.url));
|
|
1713
|
+
const sliced = matched.slice(offset, offset + limit + 1);
|
|
1714
|
+
const truncatedByLimit = matched.length > offset + limit;
|
|
1715
|
+
const calls = sliced
|
|
1716
|
+
.slice(0, limit)
|
|
1717
|
+
.map((row) => mapNetworkCallRecord(row, includeBodies));
|
|
1718
|
+
const bytePage = applyByteBudget(calls, maxResponseBytes);
|
|
1719
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
1720
|
+
return {
|
|
1721
|
+
...createBaseResponse(sessionId),
|
|
1722
|
+
limitsApplied: {
|
|
1723
|
+
maxResults: limit,
|
|
1724
|
+
truncated,
|
|
949
1725
|
},
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1726
|
+
filtersApplied: {
|
|
1727
|
+
sessionId,
|
|
1728
|
+
urlContains,
|
|
1729
|
+
urlRegex: urlRegex.source,
|
|
1730
|
+
method,
|
|
1731
|
+
statusIn,
|
|
1732
|
+
tabId,
|
|
1733
|
+
timeFrom,
|
|
1734
|
+
timeTo,
|
|
1735
|
+
includeBodies,
|
|
1736
|
+
regexScanLimit,
|
|
1737
|
+
},
|
|
1738
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1739
|
+
responseBytes: bytePage.responseBytes,
|
|
1740
|
+
calls: bytePage.items,
|
|
1741
|
+
};
|
|
1742
|
+
},
|
|
1743
|
+
wait_for_network_call: async (input) => {
|
|
1744
|
+
const db = getDb();
|
|
1745
|
+
const sessionId = getSessionId(input);
|
|
1746
|
+
if (!sessionId) {
|
|
1747
|
+
throw new Error('sessionId is required');
|
|
1748
|
+
}
|
|
1749
|
+
const urlPattern = normalizeOptionalString(input.urlPattern);
|
|
1750
|
+
if (!urlPattern) {
|
|
1751
|
+
throw new Error('urlPattern is required');
|
|
1752
|
+
}
|
|
1753
|
+
const method = normalizeHttpMethod(input.method);
|
|
1754
|
+
const timeoutMs = resolveTimeoutMs(input.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
|
|
1755
|
+
const includeBodies = input.includeBodies === true;
|
|
1756
|
+
const startedAt = Date.now();
|
|
1757
|
+
const deadline = startedAt + timeoutMs;
|
|
1758
|
+
const urlRegex = compileSafeRegex(urlPattern);
|
|
1759
|
+
if (!urlRegex) {
|
|
1760
|
+
throw new Error('urlPattern is required');
|
|
1761
|
+
}
|
|
1762
|
+
while (Date.now() <= deadline) {
|
|
1763
|
+
const where = ['session_id = ?', 'ts_start >= ?'];
|
|
1764
|
+
const params = [sessionId, startedAt];
|
|
1765
|
+
if (method) {
|
|
1766
|
+
where.push('method = ?');
|
|
1767
|
+
params.push(method);
|
|
1768
|
+
}
|
|
1769
|
+
const rows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
1770
|
+
FROM network
|
|
1771
|
+
WHERE ${where.join(' AND ')}
|
|
1772
|
+
ORDER BY ts_start ASC
|
|
1773
|
+
LIMIT 200`).all(...params);
|
|
1774
|
+
const matched = rows.find((row) => urlRegex.test(row.url));
|
|
1775
|
+
if (matched) {
|
|
1776
|
+
return {
|
|
1777
|
+
...createBaseResponse(sessionId),
|
|
1778
|
+
limitsApplied: {
|
|
1779
|
+
maxResults: 1,
|
|
1780
|
+
truncated: false,
|
|
1781
|
+
},
|
|
1782
|
+
waitedMs: Date.now() - startedAt,
|
|
1783
|
+
filter: {
|
|
1784
|
+
urlPattern,
|
|
1785
|
+
method,
|
|
1786
|
+
timeoutMs,
|
|
1787
|
+
includeBodies,
|
|
1788
|
+
},
|
|
1789
|
+
call: mapNetworkCallRecord(matched, includeBodies),
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
await sleep(DEFAULT_NETWORK_POLL_INTERVAL_MS);
|
|
1793
|
+
}
|
|
1794
|
+
throw new Error(`No matching network call for pattern "${urlPattern}" within ${timeoutMs}ms.`);
|
|
1795
|
+
},
|
|
1796
|
+
get_request_trace: async (input) => {
|
|
1797
|
+
const db = getDb();
|
|
1798
|
+
const sessionId = getSessionId(input);
|
|
1799
|
+
const includeBodies = input.includeBodies === true;
|
|
1800
|
+
const requestId = normalizeOptionalString(input.requestId);
|
|
1801
|
+
const traceIdInput = normalizeOptionalString(input.traceId);
|
|
1802
|
+
const eventLimit = resolveLimit(input.eventLimit, DEFAULT_EVENT_LIMIT);
|
|
1803
|
+
if (!requestId && !traceIdInput) {
|
|
1804
|
+
throw new Error('requestId or traceId is required');
|
|
1805
|
+
}
|
|
1806
|
+
let anchor;
|
|
1807
|
+
if (requestId) {
|
|
1808
|
+
const params = [requestId];
|
|
1809
|
+
let sql = `SELECT ${NETWORK_CALL_SELECT_COLUMNS} FROM network WHERE request_id = ?`;
|
|
1810
|
+
if (sessionId) {
|
|
1811
|
+
sql += ' AND session_id = ?';
|
|
1812
|
+
params.push(sessionId);
|
|
1813
|
+
}
|
|
1814
|
+
sql += ' LIMIT 1';
|
|
1815
|
+
anchor = db.prepare(sql).get(...params);
|
|
1816
|
+
if (!anchor) {
|
|
1817
|
+
throw new Error(`Request not found: ${requestId}`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
const traceId = traceIdInput ?? anchor?.trace_id ?? null;
|
|
1821
|
+
const traceSessionId = sessionId ?? anchor?.session_id;
|
|
1822
|
+
const networkWhere = [];
|
|
1823
|
+
const networkParams = [];
|
|
1824
|
+
if (traceId) {
|
|
1825
|
+
networkWhere.push('trace_id = ?');
|
|
1826
|
+
networkParams.push(traceId);
|
|
1827
|
+
}
|
|
1828
|
+
else if (requestId) {
|
|
1829
|
+
networkWhere.push('request_id = ?');
|
|
1830
|
+
networkParams.push(requestId);
|
|
1831
|
+
}
|
|
1832
|
+
if (traceSessionId) {
|
|
1833
|
+
networkWhere.push('session_id = ?');
|
|
1834
|
+
networkParams.push(traceSessionId);
|
|
1835
|
+
}
|
|
1836
|
+
const networkRows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
|
|
1837
|
+
FROM network
|
|
1838
|
+
WHERE ${networkWhere.join(' AND ')}
|
|
1839
|
+
ORDER BY ts_start ASC
|
|
1840
|
+
LIMIT 500`).all(...networkParams);
|
|
1841
|
+
const eventRows = traceId
|
|
1842
|
+
? db.prepare(`SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
1843
|
+
FROM events
|
|
1844
|
+
WHERE json_extract(payload_json, '$.traceId') = ?
|
|
1845
|
+
${traceSessionId ? 'AND session_id = ?' : ''}
|
|
1846
|
+
ORDER BY ts ASC
|
|
1847
|
+
LIMIT ?`).all(...(traceSessionId ? [traceId, traceSessionId, eventLimit + 1] : [traceId, eventLimit + 1]))
|
|
1848
|
+
: [];
|
|
1849
|
+
const eventsTruncated = eventRows.length > eventLimit;
|
|
1850
|
+
const correlatedEvents = eventRows.slice(0, eventLimit).map((row) => mapEventRecord(row));
|
|
1851
|
+
return {
|
|
1852
|
+
...createBaseResponse(traceSessionId),
|
|
1853
|
+
limitsApplied: {
|
|
1854
|
+
maxResults: eventLimit,
|
|
1855
|
+
truncated: eventsTruncated,
|
|
1856
|
+
},
|
|
1857
|
+
traceId: traceId ?? undefined,
|
|
1858
|
+
requestId: requestId ?? anchor?.request_id ?? undefined,
|
|
1859
|
+
anchorRequest: anchor ? mapNetworkCallRecord(anchor, includeBodies) : undefined,
|
|
1860
|
+
networkCalls: networkRows.map((row) => mapNetworkCallRecord(row, includeBodies)),
|
|
1861
|
+
correlatedEvents,
|
|
1862
|
+
};
|
|
1863
|
+
},
|
|
1864
|
+
get_body_chunk: async (input) => {
|
|
1865
|
+
const db = getDb();
|
|
1866
|
+
const chunkRef = normalizeOptionalString(input.chunkRef);
|
|
1867
|
+
if (!chunkRef) {
|
|
1868
|
+
throw new Error('chunkRef is required');
|
|
1869
|
+
}
|
|
1870
|
+
const sessionId = getSessionId(input);
|
|
1871
|
+
const offset = resolveOffset(input.offset);
|
|
1872
|
+
const limit = resolveBodyChunkBytes(input.limit);
|
|
1873
|
+
const row = db.prepare(`SELECT chunk_ref, session_id, request_id, trace_id, body_kind, content_type, body_text, body_bytes, truncated, created_at
|
|
1874
|
+
FROM body_chunks
|
|
1875
|
+
WHERE chunk_ref = ?
|
|
1876
|
+
${sessionId ? 'AND session_id = ?' : ''}
|
|
1877
|
+
LIMIT 1`).get(...(sessionId ? [chunkRef, sessionId] : [chunkRef]));
|
|
1878
|
+
if (!row) {
|
|
1879
|
+
throw new Error(`Body chunk not found: ${chunkRef}`);
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
...createBaseResponse(row.session_id),
|
|
1883
|
+
limitsApplied: {
|
|
1884
|
+
maxResults: limit,
|
|
1885
|
+
truncated: offset + limit < row.body_bytes,
|
|
1886
|
+
},
|
|
1887
|
+
...mapBodyChunkRecord(row, offset, limit),
|
|
961
1888
|
};
|
|
962
1889
|
},
|
|
963
1890
|
get_element_refs: async (input) => {
|
|
@@ -972,9 +1899,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
972
1899
|
}
|
|
973
1900
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
974
1901
|
const offset = resolveOffset(input.offset);
|
|
1902
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
975
1903
|
const rows = db
|
|
976
1904
|
.prepare(`
|
|
977
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
1905
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
978
1906
|
FROM events
|
|
979
1907
|
WHERE session_id = ?
|
|
980
1908
|
AND type IN ('ui', 'element_ref')
|
|
@@ -983,19 +1911,20 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
983
1911
|
LIMIT ? OFFSET ?
|
|
984
1912
|
`)
|
|
985
1913
|
.all(sessionId, selector, limit + 1, offset);
|
|
986
|
-
const
|
|
1914
|
+
const truncatedByLimit = rows.length > limit;
|
|
1915
|
+
const refs = rows.slice(0, limit).map((row) => mapEventRecord(row));
|
|
1916
|
+
const bytePage = applyByteBudget(refs, maxResponseBytes);
|
|
1917
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
987
1918
|
return {
|
|
988
1919
|
...createBaseResponse(sessionId),
|
|
989
1920
|
limitsApplied: {
|
|
990
1921
|
maxResults: limit,
|
|
991
1922
|
truncated,
|
|
992
1923
|
},
|
|
993
|
-
pagination:
|
|
994
|
-
|
|
995
|
-
returned: Math.min(rows.length, limit),
|
|
996
|
-
},
|
|
1924
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1925
|
+
responseBytes: bytePage.responseBytes,
|
|
997
1926
|
selector,
|
|
998
|
-
refs:
|
|
1927
|
+
refs: bytePage.items,
|
|
999
1928
|
};
|
|
1000
1929
|
},
|
|
1001
1930
|
explain_last_failure: async (input) => {
|
|
@@ -1008,7 +1937,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1008
1937
|
const windowMs = lookbackSeconds * 1000;
|
|
1009
1938
|
const latestErrorEvent = db
|
|
1010
1939
|
.prepare(`
|
|
1011
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
1940
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
1012
1941
|
FROM events
|
|
1013
1942
|
WHERE session_id = ?
|
|
1014
1943
|
AND (type = 'error' OR (type = 'console' AND json_extract(payload_json, '$.level') = 'error'))
|
|
@@ -1018,7 +1947,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1018
1947
|
.get(sessionId);
|
|
1019
1948
|
const latestNetworkFailure = db
|
|
1020
1949
|
.prepare(`
|
|
1021
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
|
|
1950
|
+
SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
|
|
1022
1951
|
FROM network
|
|
1023
1952
|
WHERE session_id = ?
|
|
1024
1953
|
AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
|
|
@@ -1046,7 +1975,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1046
1975
|
const windowEnd = anchorTs + 1_000;
|
|
1047
1976
|
const eventRows = db
|
|
1048
1977
|
.prepare(`
|
|
1049
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
1978
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
1050
1979
|
FROM events
|
|
1051
1980
|
WHERE session_id = ?
|
|
1052
1981
|
AND ts BETWEEN ? AND ?
|
|
@@ -1055,7 +1984,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1055
1984
|
.all(sessionId, windowStart, windowEnd);
|
|
1056
1985
|
const networkRows = db
|
|
1057
1986
|
.prepare(`
|
|
1058
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
|
|
1987
|
+
SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
|
|
1059
1988
|
FROM network
|
|
1060
1989
|
WHERE session_id = ?
|
|
1061
1990
|
AND ts_start BETWEEN ? AND ?
|
|
@@ -1138,7 +2067,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1138
2067
|
}
|
|
1139
2068
|
const anchorEvent = db
|
|
1140
2069
|
.prepare(`
|
|
1141
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
2070
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
1142
2071
|
FROM events
|
|
1143
2072
|
WHERE session_id = ? AND event_id = ?
|
|
1144
2073
|
LIMIT 1
|
|
@@ -1153,7 +2082,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1153
2082
|
const windowEnd = anchorEvent.ts + windowMs;
|
|
1154
2083
|
const nearbyEvents = db
|
|
1155
2084
|
.prepare(`
|
|
1156
|
-
SELECT event_id, session_id, ts, type, payload_json
|
|
2085
|
+
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
1157
2086
|
FROM events
|
|
1158
2087
|
WHERE session_id = ?
|
|
1159
2088
|
AND event_id != ?
|
|
@@ -1162,7 +2091,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1162
2091
|
.all(sessionId, eventId, windowStart, windowEnd);
|
|
1163
2092
|
const nearbyNetworkFailures = db
|
|
1164
2093
|
.prepare(`
|
|
1165
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
|
|
2094
|
+
SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
|
|
1166
2095
|
FROM network
|
|
1167
2096
|
WHERE session_id = ?
|
|
1168
2097
|
AND ts_start BETWEEN ? AND ?
|
|
@@ -1234,6 +2163,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1234
2163
|
const untilTimestamp = resolveOptionalTimestamp(input.untilTimestamp);
|
|
1235
2164
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
1236
2165
|
const offset = resolveOffset(input.offset);
|
|
2166
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1237
2167
|
const where = ['session_id = ?'];
|
|
1238
2168
|
const params = [sessionId];
|
|
1239
2169
|
if (trigger) {
|
|
@@ -1258,18 +2188,19 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1258
2188
|
ORDER BY ts DESC
|
|
1259
2189
|
LIMIT ? OFFSET ?`)
|
|
1260
2190
|
.all(...params, limit + 1, offset);
|
|
1261
|
-
const
|
|
2191
|
+
const truncatedByLimit = rows.length > limit;
|
|
2192
|
+
const snapshots = rows.slice(0, limit).map((row) => mapSnapshotMetadata(row));
|
|
2193
|
+
const bytePage = applyByteBudget(snapshots, maxResponseBytes);
|
|
2194
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
1262
2195
|
return {
|
|
1263
2196
|
...createBaseResponse(sessionId),
|
|
1264
2197
|
limitsApplied: {
|
|
1265
2198
|
maxResults: limit,
|
|
1266
2199
|
truncated,
|
|
1267
2200
|
},
|
|
1268
|
-
pagination:
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
},
|
|
1272
|
-
snapshots: rows.slice(0, limit).map((row) => mapSnapshotMetadata(row)),
|
|
2201
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
2202
|
+
responseBytes: bytePage.responseBytes,
|
|
2203
|
+
snapshots: bytePage.items,
|
|
1273
2204
|
};
|
|
1274
2205
|
},
|
|
1275
2206
|
get_snapshot_for_event: async (input) => {
|
|
@@ -1351,7 +2282,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1351
2282
|
throw new Error('snapshotId is required');
|
|
1352
2283
|
}
|
|
1353
2284
|
const assetType = input.asset === 'png' ? 'png' : 'png';
|
|
1354
|
-
const encoding = input.encoding === '
|
|
2285
|
+
const encoding = input.encoding === 'raw' ? 'raw' : 'base64';
|
|
1355
2286
|
const offset = resolveOffset(input.offset);
|
|
1356
2287
|
const maxBytes = resolveChunkBytes(input.maxBytes, DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES);
|
|
1357
2288
|
const snapshot = db
|
|
@@ -1381,6 +2312,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1381
2312
|
},
|
|
1382
2313
|
snapshotId,
|
|
1383
2314
|
asset: assetType,
|
|
2315
|
+
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
1384
2316
|
mime: snapshot.png_mime ?? 'image/png',
|
|
1385
2317
|
totalBytes: fullBuffer.byteLength,
|
|
1386
2318
|
offset,
|
|
@@ -1404,6 +2336,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1404
2336
|
},
|
|
1405
2337
|
snapshotId,
|
|
1406
2338
|
asset: assetType,
|
|
2339
|
+
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
1407
2340
|
mime: snapshot.png_mime ?? 'image/png',
|
|
1408
2341
|
totalBytes: fullBuffer.byteLength,
|
|
1409
2342
|
offset,
|
|
@@ -1531,6 +2464,9 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
1531
2464
|
const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
|
|
1532
2465
|
const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
|
|
1533
2466
|
const maxAncestors = resolveCaptureAncestors(input.maxAncestors, 4);
|
|
2467
|
+
const includeDom = typeof input.includeDom === 'boolean' ? input.includeDom : mode !== 'png';
|
|
2468
|
+
const includeStyles = typeof input.includeStyles === 'boolean' ? input.includeStyles : mode !== 'png';
|
|
2469
|
+
const includePngDataUrl = typeof input.includePngDataUrl === 'boolean' ? input.includePngDataUrl : mode !== 'png';
|
|
1534
2470
|
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
|
|
1535
2471
|
selector,
|
|
1536
2472
|
trigger,
|
|
@@ -1540,15 +2476,105 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
1540
2476
|
maxDepth,
|
|
1541
2477
|
maxBytes,
|
|
1542
2478
|
maxAncestors,
|
|
2479
|
+
includeDom,
|
|
2480
|
+
includeStyles,
|
|
2481
|
+
includePngDataUrl,
|
|
1543
2482
|
llmRequested: true,
|
|
1544
2483
|
}, 5_000);
|
|
2484
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
2485
|
+
const snapshotRecord = structuredClone(payload);
|
|
2486
|
+
const snapshotRoot = snapshotRecord.snapshot;
|
|
2487
|
+
if (typeof snapshotRoot === 'object' && snapshotRoot !== null) {
|
|
2488
|
+
const snapshotObject = snapshotRoot;
|
|
2489
|
+
if (!includeDom) {
|
|
2490
|
+
delete snapshotObject.dom;
|
|
2491
|
+
}
|
|
2492
|
+
if (!includeStyles) {
|
|
2493
|
+
delete snapshotObject.styles;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const png = snapshotRecord.png;
|
|
2497
|
+
if (!includePngDataUrl && typeof png === 'object' && png !== null) {
|
|
2498
|
+
delete png.dataUrl;
|
|
2499
|
+
}
|
|
1545
2500
|
return {
|
|
1546
2501
|
...createBaseResponse(sessionId),
|
|
1547
2502
|
limitsApplied: {
|
|
1548
2503
|
maxResults: maxBytes,
|
|
1549
2504
|
truncated: capture.truncated ?? false,
|
|
1550
2505
|
},
|
|
1551
|
-
|
|
2506
|
+
includeDom,
|
|
2507
|
+
includeStyles,
|
|
2508
|
+
includePngDataUrl,
|
|
2509
|
+
...snapshotRecord,
|
|
2510
|
+
};
|
|
2511
|
+
},
|
|
2512
|
+
get_live_console_logs: async (input) => {
|
|
2513
|
+
const sessionId = getSessionId(input);
|
|
2514
|
+
if (!sessionId) {
|
|
2515
|
+
throw new Error('sessionId is required');
|
|
2516
|
+
}
|
|
2517
|
+
const origin = normalizeRequestedOrigin(input.url);
|
|
2518
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
2519
|
+
const levels = resolveLiveConsoleLevels(input.levels);
|
|
2520
|
+
const contains = typeof input.contains === 'string' && input.contains.trim().length > 0
|
|
2521
|
+
? input.contains.trim()
|
|
2522
|
+
: undefined;
|
|
2523
|
+
const sinceTs = resolveOptionalTimestamp(input.sinceTs);
|
|
2524
|
+
const includeRuntimeErrors = input.includeRuntimeErrors !== false;
|
|
2525
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
2526
|
+
const responseProfile = resolveResponseProfile(input.responseProfile);
|
|
2527
|
+
const includeArgs = responseProfile === 'compact' && input.includeArgs === true;
|
|
2528
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
2529
|
+
const dedupeWindowMs = resolveDurationMs(input.dedupeWindowMs, 0, 60_000);
|
|
2530
|
+
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
|
|
2531
|
+
origin,
|
|
2532
|
+
tabId,
|
|
2533
|
+
levels,
|
|
2534
|
+
contains,
|
|
2535
|
+
sinceTs,
|
|
2536
|
+
includeRuntimeErrors,
|
|
2537
|
+
dedupeWindowMs,
|
|
2538
|
+
limit,
|
|
2539
|
+
}, 3_000);
|
|
2540
|
+
const payload = ensureCaptureSuccess(capture, sessionId);
|
|
2541
|
+
const rawLogs = asRecordArray(payload.logs);
|
|
2542
|
+
const logs = rawLogs.map((entry) => mapLiveConsoleLogRecord(entry, responseProfile, { includeArgs }));
|
|
2543
|
+
const bytePage = applyByteBudget(logs, maxResponseBytes);
|
|
2544
|
+
const truncated = (capture.truncated ?? false) || bytePage.truncatedByBytes;
|
|
2545
|
+
const paginationRecord = typeof payload.pagination === 'object' && payload.pagination !== null
|
|
2546
|
+
? payload.pagination
|
|
2547
|
+
: {};
|
|
2548
|
+
const matched = typeof paginationRecord.matched === 'number'
|
|
2549
|
+
? Math.max(0, Math.floor(paginationRecord.matched))
|
|
2550
|
+
: rawLogs.length;
|
|
2551
|
+
return {
|
|
2552
|
+
...createBaseResponse(sessionId),
|
|
2553
|
+
limitsApplied: {
|
|
2554
|
+
maxResults: limit,
|
|
2555
|
+
truncated,
|
|
2556
|
+
},
|
|
2557
|
+
responseProfile,
|
|
2558
|
+
responseBytes: bytePage.responseBytes,
|
|
2559
|
+
logs: bytePage.items,
|
|
2560
|
+
pagination: {
|
|
2561
|
+
returned: bytePage.items.length,
|
|
2562
|
+
matched,
|
|
2563
|
+
hasMore: truncated,
|
|
2564
|
+
maxResponseBytes,
|
|
2565
|
+
},
|
|
2566
|
+
filtersApplied: typeof payload.filtersApplied === 'object' && payload.filtersApplied !== null
|
|
2567
|
+
? payload.filtersApplied
|
|
2568
|
+
: {
|
|
2569
|
+
tabId,
|
|
2570
|
+
origin,
|
|
2571
|
+
levels,
|
|
2572
|
+
contains,
|
|
2573
|
+
sinceTs,
|
|
2574
|
+
includeRuntimeErrors,
|
|
2575
|
+
dedupeWindowMs,
|
|
2576
|
+
},
|
|
2577
|
+
bufferStats: payload.bufferStats,
|
|
1552
2578
|
};
|
|
1553
2579
|
},
|
|
1554
2580
|
};
|
|
@@ -1578,6 +2604,15 @@ function createDefaultHandler(toolName) {
|
|
|
1578
2604
|
};
|
|
1579
2605
|
};
|
|
1580
2606
|
}
|
|
2607
|
+
function attachResponseBytes(response) {
|
|
2608
|
+
if (typeof response.responseBytes === 'number' && Number.isFinite(response.responseBytes)) {
|
|
2609
|
+
return response;
|
|
2610
|
+
}
|
|
2611
|
+
return {
|
|
2612
|
+
...response,
|
|
2613
|
+
responseBytes: estimateJsonBytes(response),
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
1581
2616
|
export function createToolRegistry(overrides = {}) {
|
|
1582
2617
|
return ALL_TOOLS.map((toolName) => {
|
|
1583
2618
|
const schema = TOOL_SCHEMAS[toolName] ?? { type: 'object', properties: {} };
|
|
@@ -1594,7 +2629,8 @@ export async function routeToolCall(tools, toolName, input) {
|
|
|
1594
2629
|
if (!tool) {
|
|
1595
2630
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
1596
2631
|
}
|
|
1597
|
-
|
|
2632
|
+
const response = await tool.handler(isRecord(input) ? input : {});
|
|
2633
|
+
return attachResponseBytes(response);
|
|
1598
2634
|
}
|
|
1599
2635
|
export function createMCPServer(overrides = {}, options = {}) {
|
|
1600
2636
|
const logger = options.logger ?? createDefaultMcpLogger();
|