browser-debug-mcp-bridge 1.6.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 +14 -0
- package/apps/mcp-server/dist/db/events-repository.js +245 -13
- package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
- package/apps/mcp-server/dist/db/migrations.js +87 -0
- 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 +18 -2
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +972 -86
- package/apps/mcp-server/dist/mcp/server.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/websocket/messages.js +22 -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 +13 -6
|
@@ -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: {
|
|
@@ -44,6 +45,9 @@ const TOOL_SCHEMAS = {
|
|
|
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: {
|
|
@@ -53,6 +57,9 @@ const TOOL_SCHEMAS = {
|
|
|
53
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: {
|
|
@@ -63,6 +70,29 @@ const TOOL_SCHEMAS = {
|
|
|
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,6 +102,7 @@ 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: {
|
|
@@ -83,6 +114,56 @@ const TOOL_SCHEMAS = {
|
|
|
83
114
|
groupBy: { type: 'string' },
|
|
84
115
|
limit: { type: 'number' },
|
|
85
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' },
|
|
86
167
|
},
|
|
87
168
|
},
|
|
88
169
|
get_element_refs: {
|
|
@@ -93,6 +174,7 @@ const TOOL_SCHEMAS = {
|
|
|
93
174
|
selector: { type: 'string' },
|
|
94
175
|
limit: { type: 'number' },
|
|
95
176
|
offset: { type: 'number' },
|
|
177
|
+
maxResponseBytes: { type: 'number' },
|
|
96
178
|
},
|
|
97
179
|
},
|
|
98
180
|
get_dom_subtree: {
|
|
@@ -142,6 +224,9 @@ const TOOL_SCHEMAS = {
|
|
|
142
224
|
maxDepth: { type: 'number' },
|
|
143
225
|
maxBytes: { type: 'number' },
|
|
144
226
|
maxAncestors: { type: 'number' },
|
|
227
|
+
includeDom: { type: 'boolean' },
|
|
228
|
+
includeStyles: { type: 'boolean' },
|
|
229
|
+
includePngDataUrl: { type: 'boolean' },
|
|
145
230
|
},
|
|
146
231
|
},
|
|
147
232
|
get_live_console_logs: {
|
|
@@ -155,7 +240,11 @@ const TOOL_SCHEMAS = {
|
|
|
155
240
|
contains: { type: 'string' },
|
|
156
241
|
sinceTs: { type: 'number' },
|
|
157
242
|
includeRuntimeErrors: { type: 'boolean' },
|
|
243
|
+
dedupeWindowMs: { type: 'number' },
|
|
158
244
|
limit: { type: 'number' },
|
|
245
|
+
responseProfile: { type: 'string' },
|
|
246
|
+
includeArgs: { type: 'boolean' },
|
|
247
|
+
maxResponseBytes: { type: 'number' },
|
|
159
248
|
},
|
|
160
249
|
},
|
|
161
250
|
explain_last_failure: {
|
|
@@ -185,6 +274,7 @@ const TOOL_SCHEMAS = {
|
|
|
185
274
|
untilTimestamp: { type: 'number' },
|
|
186
275
|
limit: { type: 'number' },
|
|
187
276
|
offset: { type: 'number' },
|
|
277
|
+
maxResponseBytes: { type: 'number' },
|
|
188
278
|
},
|
|
189
279
|
},
|
|
190
280
|
get_snapshot_for_event: {
|
|
@@ -215,8 +305,14 @@ const TOOL_DESCRIPTIONS = {
|
|
|
215
305
|
get_recent_events: 'Read recent events from a session',
|
|
216
306
|
get_navigation_history: 'Read navigation events for a session',
|
|
217
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',
|
|
218
310
|
get_error_fingerprints: 'List aggregated error fingerprints',
|
|
219
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',
|
|
220
316
|
get_element_refs: 'Get element references by selector',
|
|
221
317
|
get_dom_subtree: 'Capture a bounded DOM subtree',
|
|
222
318
|
get_dom_document: 'Capture full document as outline or html',
|
|
@@ -239,9 +335,21 @@ const DEFAULT_REDACTION_SUMMARY = {
|
|
|
239
335
|
const DEFAULT_LIST_LIMIT = 25;
|
|
240
336
|
const DEFAULT_EVENT_LIMIT = 50;
|
|
241
337
|
const MAX_LIMIT = 200;
|
|
338
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024;
|
|
339
|
+
const MAX_RESPONSE_BYTES = 512 * 1024;
|
|
242
340
|
const DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES = 64 * 1024;
|
|
243
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;
|
|
244
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
|
+
`;
|
|
245
353
|
const NETWORK_DOMAIN_GROUP_SQL = `
|
|
246
354
|
CASE
|
|
247
355
|
WHEN instr(replace(replace(url, 'https://', ''), 'http://', ''), '/') > 0
|
|
@@ -280,6 +388,62 @@ function resolveOffset(value) {
|
|
|
280
388
|
const floored = Math.floor(value);
|
|
281
389
|
return floored < 0 ? 0 : floored;
|
|
282
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
|
+
}
|
|
283
447
|
function readJsonPayload(payloadJson) {
|
|
284
448
|
try {
|
|
285
449
|
const parsed = JSON.parse(payloadJson);
|
|
@@ -387,8 +551,28 @@ function resolveLastUrl(payload) {
|
|
|
387
551
|
}
|
|
388
552
|
return undefined;
|
|
389
553
|
}
|
|
390
|
-
function mapEventRecord(row) {
|
|
554
|
+
function mapEventRecord(row, profile = 'legacy', options = {}) {
|
|
391
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
|
+
}
|
|
392
576
|
return {
|
|
393
577
|
eventId: row.event_id,
|
|
394
578
|
sessionId: row.session_id,
|
|
@@ -456,6 +640,153 @@ function resolveDurationMs(value, fallback, maxValue) {
|
|
|
456
640
|
}
|
|
457
641
|
return Math.min(floored, maxValue);
|
|
458
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
|
+
}
|
|
459
790
|
function normalizeAssetPath(pathValue) {
|
|
460
791
|
return pathValue.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, '');
|
|
461
792
|
}
|
|
@@ -615,6 +946,39 @@ function resolveLiveConsoleLevels(value) {
|
|
|
615
946
|
.filter((entry) => LIVE_CONSOLE_LEVELS.has(entry));
|
|
616
947
|
return Array.from(new Set(levels));
|
|
617
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
|
+
}
|
|
618
982
|
function resolveOptionalTabId(value) {
|
|
619
983
|
if (value === undefined || value === null || value === '') {
|
|
620
984
|
return undefined;
|
|
@@ -670,6 +1034,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
670
1034
|
const sinceMinutes = typeof input.sinceMinutes === 'number' ? input.sinceMinutes : undefined;
|
|
671
1035
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
672
1036
|
const offset = resolveOffset(input.offset);
|
|
1037
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
673
1038
|
const where = [];
|
|
674
1039
|
const params = [];
|
|
675
1040
|
if (sinceMinutes !== undefined && Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
|
|
@@ -681,6 +1046,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
681
1046
|
SELECT
|
|
682
1047
|
session_id,
|
|
683
1048
|
created_at,
|
|
1049
|
+
paused_at,
|
|
684
1050
|
ended_at,
|
|
685
1051
|
tab_id,
|
|
686
1052
|
window_id,
|
|
@@ -698,11 +1064,13 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
698
1064
|
LIMIT ? OFFSET ?
|
|
699
1065
|
`;
|
|
700
1066
|
const rows = db.prepare(sql).all(...params, limit + 1, offset);
|
|
701
|
-
const
|
|
1067
|
+
const truncatedByLimit = rows.length > limit;
|
|
702
1068
|
const sessions = rows.slice(0, limit).map((row) => ({
|
|
703
1069
|
sessionId: row.session_id,
|
|
704
1070
|
createdAt: row.created_at,
|
|
1071
|
+
pausedAt: row.paused_at ?? undefined,
|
|
705
1072
|
endedAt: row.ended_at ?? undefined,
|
|
1073
|
+
status: row.ended_at ? 'ended' : row.paused_at ? 'paused' : 'active',
|
|
706
1074
|
tabId: row.tab_id ?? undefined,
|
|
707
1075
|
windowId: row.window_id ?? undefined,
|
|
708
1076
|
urlStart: row.url_start ?? undefined,
|
|
@@ -735,17 +1103,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
735
1103
|
};
|
|
736
1104
|
})(),
|
|
737
1105
|
}));
|
|
1106
|
+
const bytePage = applyByteBudget(sessions, maxResponseBytes);
|
|
1107
|
+
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
738
1108
|
return {
|
|
739
1109
|
...createBaseResponse(),
|
|
740
1110
|
limitsApplied: {
|
|
741
1111
|
maxResults: limit,
|
|
742
1112
|
truncated,
|
|
743
1113
|
},
|
|
744
|
-
pagination:
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
},
|
|
748
|
-
sessions,
|
|
1114
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1115
|
+
responseBytes: bytePage.responseBytes,
|
|
1116
|
+
sessions: bytePage.items,
|
|
749
1117
|
};
|
|
750
1118
|
},
|
|
751
1119
|
get_session_summary: async (input) => {
|
|
@@ -817,6 +1185,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
817
1185
|
ensureSessionOrOriginFilter(sessionId, origin);
|
|
818
1186
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
819
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;
|
|
820
1191
|
const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
|
|
821
1192
|
const params = [];
|
|
822
1193
|
const where = [];
|
|
@@ -839,18 +1210,22 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
839
1210
|
LIMIT ? OFFSET ?
|
|
840
1211
|
`)
|
|
841
1212
|
.all(...params, limit + 1, offset);
|
|
842
|
-
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;
|
|
843
1219
|
return {
|
|
844
1220
|
...createBaseResponse(sessionId),
|
|
845
1221
|
limitsApplied: {
|
|
846
1222
|
maxResults: limit,
|
|
847
1223
|
truncated,
|
|
848
1224
|
},
|
|
849
|
-
pagination:
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
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,
|
|
854
1229
|
};
|
|
855
1230
|
},
|
|
856
1231
|
get_navigation_history: async (input) => {
|
|
@@ -860,6 +1235,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
860
1235
|
ensureSessionOrOriginFilter(sessionId, origin);
|
|
861
1236
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
862
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;
|
|
863
1241
|
const params = [];
|
|
864
1242
|
const where = ["type = 'nav'"];
|
|
865
1243
|
if (sessionId) {
|
|
@@ -876,18 +1254,22 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
876
1254
|
LIMIT ? OFFSET ?
|
|
877
1255
|
`)
|
|
878
1256
|
.all(...params, limit + 1, offset);
|
|
879
|
-
const
|
|
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;
|
|
880
1263
|
return {
|
|
881
1264
|
...createBaseResponse(sessionId),
|
|
882
1265
|
limitsApplied: {
|
|
883
1266
|
maxResults: limit,
|
|
884
1267
|
truncated,
|
|
885
1268
|
},
|
|
886
|
-
pagination:
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
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,
|
|
891
1273
|
};
|
|
892
1274
|
},
|
|
893
1275
|
get_console_events: async (input) => {
|
|
@@ -898,6 +1280,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
898
1280
|
const level = typeof input.level === 'string' ? input.level : undefined;
|
|
899
1281
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
900
1282
|
const offset = resolveOffset(input.offset);
|
|
1283
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1284
|
+
const responseProfile = resolveResponseProfile(input.responseProfile);
|
|
1285
|
+
const includePayload = responseProfile === 'compact' && input.includePayload === true;
|
|
901
1286
|
const params = [];
|
|
902
1287
|
const where = ["type = 'console'"];
|
|
903
1288
|
if (sessionId) {
|
|
@@ -918,18 +1303,170 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
918
1303
|
LIMIT ? OFFSET ?
|
|
919
1304
|
`)
|
|
920
1305
|
.all(...params, limit + 1, offset);
|
|
921
|
-
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;
|
|
922
1312
|
return {
|
|
923
1313
|
...createBaseResponse(sessionId),
|
|
924
1314
|
limitsApplied: {
|
|
925
1315
|
maxResults: limit,
|
|
926
1316
|
truncated,
|
|
927
1317
|
},
|
|
928
|
-
pagination:
|
|
929
|
-
|
|
930
|
-
|
|
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
|
+
},
|
|
931
1397
|
},
|
|
932
|
-
|
|
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
|
+
})),
|
|
933
1470
|
};
|
|
934
1471
|
},
|
|
935
1472
|
get_error_fingerprints: async (input) => {
|
|
@@ -940,6 +1477,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
940
1477
|
: undefined;
|
|
941
1478
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
942
1479
|
const offset = resolveOffset(input.offset);
|
|
1480
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
943
1481
|
const params = [];
|
|
944
1482
|
const where = [];
|
|
945
1483
|
if (sessionId) {
|
|
@@ -960,26 +1498,27 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
960
1498
|
LIMIT ? OFFSET ?
|
|
961
1499
|
`)
|
|
962
1500
|
.all(...params, limit + 1, offset);
|
|
963
|
-
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;
|
|
964
1513
|
return {
|
|
965
1514
|
...createBaseResponse(sessionId),
|
|
966
1515
|
limitsApplied: {
|
|
967
1516
|
maxResults: limit,
|
|
968
1517
|
truncated,
|
|
969
1518
|
},
|
|
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
|
-
})),
|
|
1519
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1520
|
+
responseBytes: bytePage.responseBytes,
|
|
1521
|
+
fingerprints: bytePage.items,
|
|
983
1522
|
};
|
|
984
1523
|
},
|
|
985
1524
|
get_network_failures: async (input) => {
|
|
@@ -991,6 +1530,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
991
1530
|
const errorType = typeof input.errorType === 'string' ? input.errorType : undefined;
|
|
992
1531
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
993
1532
|
const offset = resolveOffset(input.offset);
|
|
1533
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
994
1534
|
const params = [];
|
|
995
1535
|
const where = [];
|
|
996
1536
|
const errorFilter = buildNetworkFailureFilter(errorType);
|
|
@@ -1024,58 +1564,327 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1024
1564
|
LIMIT ? OFFSET ?
|
|
1025
1565
|
`)
|
|
1026
1566
|
.all(...params, limit + 1, offset);
|
|
1027
|
-
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;
|
|
1028
1576
|
return {
|
|
1029
1577
|
...createBaseResponse(sessionId),
|
|
1030
1578
|
limitsApplied: {
|
|
1031
1579
|
maxResults: limit,
|
|
1032
1580
|
truncated,
|
|
1033
1581
|
},
|
|
1034
|
-
pagination:
|
|
1035
|
-
|
|
1036
|
-
returned: Math.min(rows.length, limit),
|
|
1037
|
-
},
|
|
1582
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1583
|
+
responseBytes: bytePage.responseBytes,
|
|
1038
1584
|
groupBy,
|
|
1039
|
-
groups:
|
|
1040
|
-
key: row.group_key,
|
|
1041
|
-
count: row.count,
|
|
1042
|
-
firstSeenAt: row.first_ts,
|
|
1043
|
-
lastSeenAt: row.last_ts,
|
|
1044
|
-
})),
|
|
1585
|
+
groups: bytePage.items,
|
|
1045
1586
|
};
|
|
1046
1587
|
}
|
|
1047
1588
|
const rows = db
|
|
1048
1589
|
.prepare(`
|
|
1049
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
|
|
1050
1591
|
FROM network
|
|
1051
1592
|
${whereClause}
|
|
1052
1593
|
ORDER BY ts_start DESC
|
|
1053
1594
|
LIMIT ? OFFSET ?
|
|
1054
1595
|
`)
|
|
1055
1596
|
.all(...params, limit + 1, offset);
|
|
1056
|
-
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;
|
|
1057
1614
|
return {
|
|
1058
1615
|
...createBaseResponse(sessionId),
|
|
1059
1616
|
limitsApplied: {
|
|
1060
1617
|
maxResults: limit,
|
|
1061
1618
|
truncated,
|
|
1062
1619
|
},
|
|
1063
|
-
pagination:
|
|
1064
|
-
|
|
1065
|
-
|
|
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,
|
|
1066
1725
|
},
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
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),
|
|
1079
1888
|
};
|
|
1080
1889
|
},
|
|
1081
1890
|
get_element_refs: async (input) => {
|
|
@@ -1090,6 +1899,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1090
1899
|
}
|
|
1091
1900
|
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
1092
1901
|
const offset = resolveOffset(input.offset);
|
|
1902
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1093
1903
|
const rows = db
|
|
1094
1904
|
.prepare(`
|
|
1095
1905
|
SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
|
|
@@ -1101,19 +1911,20 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1101
1911
|
LIMIT ? OFFSET ?
|
|
1102
1912
|
`)
|
|
1103
1913
|
.all(sessionId, selector, limit + 1, offset);
|
|
1104
|
-
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;
|
|
1105
1918
|
return {
|
|
1106
1919
|
...createBaseResponse(sessionId),
|
|
1107
1920
|
limitsApplied: {
|
|
1108
1921
|
maxResults: limit,
|
|
1109
1922
|
truncated,
|
|
1110
1923
|
},
|
|
1111
|
-
pagination:
|
|
1112
|
-
|
|
1113
|
-
returned: Math.min(rows.length, limit),
|
|
1114
|
-
},
|
|
1924
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
1925
|
+
responseBytes: bytePage.responseBytes,
|
|
1115
1926
|
selector,
|
|
1116
|
-
refs:
|
|
1927
|
+
refs: bytePage.items,
|
|
1117
1928
|
};
|
|
1118
1929
|
},
|
|
1119
1930
|
explain_last_failure: async (input) => {
|
|
@@ -1136,7 +1947,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1136
1947
|
.get(sessionId);
|
|
1137
1948
|
const latestNetworkFailure = db
|
|
1138
1949
|
.prepare(`
|
|
1139
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
|
|
1140
1951
|
FROM network
|
|
1141
1952
|
WHERE session_id = ?
|
|
1142
1953
|
AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
|
|
@@ -1173,7 +1984,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1173
1984
|
.all(sessionId, windowStart, windowEnd);
|
|
1174
1985
|
const networkRows = db
|
|
1175
1986
|
.prepare(`
|
|
1176
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
|
|
1177
1988
|
FROM network
|
|
1178
1989
|
WHERE session_id = ?
|
|
1179
1990
|
AND ts_start BETWEEN ? AND ?
|
|
@@ -1280,7 +2091,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1280
2091
|
.all(sessionId, eventId, windowStart, windowEnd);
|
|
1281
2092
|
const nearbyNetworkFailures = db
|
|
1282
2093
|
.prepare(`
|
|
1283
|
-
SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
|
|
1284
2095
|
FROM network
|
|
1285
2096
|
WHERE session_id = ?
|
|
1286
2097
|
AND ts_start BETWEEN ? AND ?
|
|
@@ -1352,6 +2163,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1352
2163
|
const untilTimestamp = resolveOptionalTimestamp(input.untilTimestamp);
|
|
1353
2164
|
const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
|
|
1354
2165
|
const offset = resolveOffset(input.offset);
|
|
2166
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
1355
2167
|
const where = ['session_id = ?'];
|
|
1356
2168
|
const params = [sessionId];
|
|
1357
2169
|
if (trigger) {
|
|
@@ -1376,18 +2188,19 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1376
2188
|
ORDER BY ts DESC
|
|
1377
2189
|
LIMIT ? OFFSET ?`)
|
|
1378
2190
|
.all(...params, limit + 1, offset);
|
|
1379
|
-
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;
|
|
1380
2195
|
return {
|
|
1381
2196
|
...createBaseResponse(sessionId),
|
|
1382
2197
|
limitsApplied: {
|
|
1383
2198
|
maxResults: limit,
|
|
1384
2199
|
truncated,
|
|
1385
2200
|
},
|
|
1386
|
-
pagination:
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
},
|
|
1390
|
-
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,
|
|
1391
2204
|
};
|
|
1392
2205
|
},
|
|
1393
2206
|
get_snapshot_for_event: async (input) => {
|
|
@@ -1469,7 +2282,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1469
2282
|
throw new Error('snapshotId is required');
|
|
1470
2283
|
}
|
|
1471
2284
|
const assetType = input.asset === 'png' ? 'png' : 'png';
|
|
1472
|
-
const encoding = input.encoding === '
|
|
2285
|
+
const encoding = input.encoding === 'raw' ? 'raw' : 'base64';
|
|
1473
2286
|
const offset = resolveOffset(input.offset);
|
|
1474
2287
|
const maxBytes = resolveChunkBytes(input.maxBytes, DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES);
|
|
1475
2288
|
const snapshot = db
|
|
@@ -1499,6 +2312,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1499
2312
|
},
|
|
1500
2313
|
snapshotId,
|
|
1501
2314
|
asset: assetType,
|
|
2315
|
+
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
1502
2316
|
mime: snapshot.png_mime ?? 'image/png',
|
|
1503
2317
|
totalBytes: fullBuffer.byteLength,
|
|
1504
2318
|
offset,
|
|
@@ -1522,6 +2336,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
1522
2336
|
},
|
|
1523
2337
|
snapshotId,
|
|
1524
2338
|
asset: assetType,
|
|
2339
|
+
assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
|
|
1525
2340
|
mime: snapshot.png_mime ?? 'image/png',
|
|
1526
2341
|
totalBytes: fullBuffer.byteLength,
|
|
1527
2342
|
offset,
|
|
@@ -1649,6 +2464,9 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
1649
2464
|
const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
|
|
1650
2465
|
const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
|
|
1651
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';
|
|
1652
2470
|
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
|
|
1653
2471
|
selector,
|
|
1654
2472
|
trigger,
|
|
@@ -1658,15 +2476,37 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
1658
2476
|
maxDepth,
|
|
1659
2477
|
maxBytes,
|
|
1660
2478
|
maxAncestors,
|
|
2479
|
+
includeDom,
|
|
2480
|
+
includeStyles,
|
|
2481
|
+
includePngDataUrl,
|
|
1661
2482
|
llmRequested: true,
|
|
1662
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
|
+
}
|
|
1663
2500
|
return {
|
|
1664
2501
|
...createBaseResponse(sessionId),
|
|
1665
2502
|
limitsApplied: {
|
|
1666
2503
|
maxResults: maxBytes,
|
|
1667
2504
|
truncated: capture.truncated ?? false,
|
|
1668
2505
|
},
|
|
1669
|
-
|
|
2506
|
+
includeDom,
|
|
2507
|
+
includeStyles,
|
|
2508
|
+
includePngDataUrl,
|
|
2509
|
+
...snapshotRecord,
|
|
1670
2510
|
};
|
|
1671
2511
|
},
|
|
1672
2512
|
get_live_console_logs: async (input) => {
|
|
@@ -1683,6 +2523,10 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
1683
2523
|
const sinceTs = resolveOptionalTimestamp(input.sinceTs);
|
|
1684
2524
|
const includeRuntimeErrors = input.includeRuntimeErrors !== false;
|
|
1685
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);
|
|
1686
2530
|
const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
|
|
1687
2531
|
origin,
|
|
1688
2532
|
tabId,
|
|
@@ -1690,15 +2534,47 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
1690
2534
|
contains,
|
|
1691
2535
|
sinceTs,
|
|
1692
2536
|
includeRuntimeErrors,
|
|
2537
|
+
dedupeWindowMs,
|
|
1693
2538
|
limit,
|
|
1694
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;
|
|
1695
2551
|
return {
|
|
1696
2552
|
...createBaseResponse(sessionId),
|
|
1697
2553
|
limitsApplied: {
|
|
1698
2554
|
maxResults: limit,
|
|
1699
|
-
truncated
|
|
2555
|
+
truncated,
|
|
1700
2556
|
},
|
|
1701
|
-
|
|
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,
|
|
1702
2578
|
};
|
|
1703
2579
|
},
|
|
1704
2580
|
};
|
|
@@ -1728,6 +2604,15 @@ function createDefaultHandler(toolName) {
|
|
|
1728
2604
|
};
|
|
1729
2605
|
};
|
|
1730
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
|
+
}
|
|
1731
2616
|
export function createToolRegistry(overrides = {}) {
|
|
1732
2617
|
return ALL_TOOLS.map((toolName) => {
|
|
1733
2618
|
const schema = TOOL_SCHEMAS[toolName] ?? { type: 'object', properties: {} };
|
|
@@ -1744,7 +2629,8 @@ export async function routeToolCall(tools, toolName, input) {
|
|
|
1744
2629
|
if (!tool) {
|
|
1745
2630
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
1746
2631
|
}
|
|
1747
|
-
|
|
2632
|
+
const response = await tool.handler(isRecord(input) ? input : {});
|
|
2633
|
+
return attachResponseBytes(response);
|
|
1748
2634
|
}
|
|
1749
2635
|
export function createMCPServer(overrides = {}, options = {}) {
|
|
1750
2636
|
const logger = options.logger ?? createDefaultMcpLogger();
|