zeitzeuge 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/analysis/agent.d.ts +19 -0
  4. package/dist/analysis/agent.d.ts.map +1 -0
  5. package/dist/analysis/parser.d.ts +3 -0
  6. package/dist/analysis/parser.d.ts.map +1 -0
  7. package/dist/analysis/prompts.d.ts +2 -0
  8. package/dist/analysis/prompts.d.ts.map +1 -0
  9. package/dist/browser/capture.d.ts +14 -0
  10. package/dist/browser/capture.d.ts.map +1 -0
  11. package/dist/browser/launch.d.ts +6 -0
  12. package/dist/browser/launch.d.ts.map +1 -0
  13. package/dist/browser/runtime-trace.d.ts +52 -0
  14. package/dist/browser/runtime-trace.d.ts.map +1 -0
  15. package/dist/browser/trace.d.ts +8 -0
  16. package/dist/browser/trace.d.ts.map +1 -0
  17. package/dist/cli.d.ts +3 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +1695 -0
  20. package/dist/models/init.d.ts +3 -0
  21. package/dist/models/init.d.ts.map +1 -0
  22. package/dist/output/report.d.ts +38 -0
  23. package/dist/output/report.d.ts.map +1 -0
  24. package/dist/output/terminal.d.ts +31 -0
  25. package/dist/output/terminal.d.ts.map +1 -0
  26. package/dist/sandbox/workspace.d.ts +33 -0
  27. package/dist/sandbox/workspace.d.ts.map +1 -0
  28. package/dist/schema.d.ts +64 -0
  29. package/dist/schema.d.ts.map +1 -0
  30. package/dist/types.d.ts +245 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/vitest/classify.d.ts +19 -0
  33. package/dist/vitest/classify.d.ts.map +1 -0
  34. package/dist/vitest/heap-profile-parser.d.ts +12 -0
  35. package/dist/vitest/heap-profile-parser.d.ts.map +1 -0
  36. package/dist/vitest/index.d.ts +17 -0
  37. package/dist/vitest/index.d.ts.map +1 -0
  38. package/dist/vitest/index.js +1616 -0
  39. package/dist/vitest/plugin.d.ts +17 -0
  40. package/dist/vitest/plugin.d.ts.map +1 -0
  41. package/dist/vitest/profile-parser.d.ts +13 -0
  42. package/dist/vitest/profile-parser.d.ts.map +1 -0
  43. package/dist/vitest/prompts.d.ts +10 -0
  44. package/dist/vitest/prompts.d.ts.map +1 -0
  45. package/dist/vitest/reporter.d.ts +79 -0
  46. package/dist/vitest/reporter.d.ts.map +1 -0
  47. package/dist/vitest/types.d.ts +231 -0
  48. package/dist/vitest/types.d.ts.map +1 -0
  49. package/dist/vitest/workspace.d.ts +25 -0
  50. package/dist/vitest/workspace.d.ts.map +1 -0
  51. package/package.json +76 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1695 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/cli.ts
6
+ import yargs from "yargs";
7
+ import { hideBin } from "yargs/helpers";
8
+
9
+ // src/models/init.ts
10
+ import { ChatOpenAI } from "@langchain/openai";
11
+ import { ChatAnthropic } from "@langchain/anthropic";
12
+ function initModel() {
13
+ const modelOverride = process.env.ZEITZEUGE_MODEL;
14
+ const openaiKey = process.env.OPENAI_API_KEY;
15
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
16
+ if (openaiKey) {
17
+ return new ChatOpenAI({
18
+ model: modelOverride ?? "gpt-5.2",
19
+ apiKey: openaiKey
20
+ });
21
+ }
22
+ if (anthropicKey) {
23
+ return new ChatAnthropic({
24
+ model: modelOverride ?? "claude-opus-4-6",
25
+ apiKey: anthropicKey
26
+ });
27
+ }
28
+ throw new Error(`No API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment.
29
+
30
+ ` + ` export OPENAI_API_KEY=sk-...
31
+ ` + ` # or
32
+ ` + ` export ANTHROPIC_API_KEY=sk-ant-...
33
+ `);
34
+ }
35
+
36
+ // src/browser/launch.ts
37
+ import { remote } from "webdriverio";
38
+ async function launchBrowser(options = {}) {
39
+ const { headless = true } = options;
40
+ const browser = await remote({
41
+ capabilities: {
42
+ browserName: "chrome",
43
+ "goog:chromeOptions": {
44
+ args: [
45
+ ...headless ? ["--headless=new"] : [],
46
+ "--no-sandbox",
47
+ "--disable-gpu",
48
+ "--disable-dev-shm-usage"
49
+ ]
50
+ }
51
+ },
52
+ logLevel: "warn"
53
+ });
54
+ return browser;
55
+ }
56
+ async function closeBrowser(browser) {
57
+ try {
58
+ await browser.deleteSession();
59
+ } catch {}
60
+ }
61
+
62
+ // src/browser/runtime-trace.ts
63
+ var SCRIPTING_EVENTS = new Set([
64
+ "FunctionCall",
65
+ "EvaluateScript",
66
+ "TimerFire",
67
+ "RequestAnimationFrame",
68
+ "FireAnimationFrame"
69
+ ]);
70
+ var LAYOUT_EVENTS = new Set(["Layout", "UpdateLayoutTree", "RecalculateStyles"]);
71
+ var PAINTING_EVENTS = new Set(["Paint", "CompositeLayers", "RasterTask"]);
72
+ var GC_EVENT_NAMES = new Set(["MajorGC", "MinorGC"]);
73
+ var BLOCKING_EVENT_NAMES = new Set(["FunctionCall", "EvaluateScript"]);
74
+ var BLOCKING_THRESHOLD_US = 50000;
75
+ function parseRuntimeTrace(traceEvents, navigationStartTs) {
76
+ if (traceEvents.length === 0) {
77
+ return emptyRuntimeTrace();
78
+ }
79
+ const mainThreadId = findMainThread(traceEvents);
80
+ const mainEvents = traceEvents.filter((e) => e.tid === mainThreadId);
81
+ const blockingFunctions = extractBlockingFunctions(mainEvents, navigationStartTs);
82
+ const eventListeners = extractEventListenerInfo(mainEvents);
83
+ const frameBreakdown = buildFrameBreakdown(mainEvents);
84
+ const gcEvents = extractGCEvents(mainEvents, navigationStartTs);
85
+ const frequentEvents = findFrequentEvents(mainEvents);
86
+ let minTs = Infinity;
87
+ let maxTs = -Infinity;
88
+ for (const e of traceEvents) {
89
+ if (e.ts < minTs)
90
+ minTs = e.ts;
91
+ const endTs = e.ts + (e.dur ?? 0);
92
+ if (endTs > maxTs)
93
+ maxTs = endTs;
94
+ }
95
+ const traceDuration = (maxTs - minTs) / 1000;
96
+ return {
97
+ totalEvents: traceEvents.length,
98
+ mainThreadId,
99
+ traceDuration,
100
+ frameBreakdown,
101
+ blockingFunctions,
102
+ eventListeners,
103
+ gcEvents,
104
+ frequentEvents
105
+ };
106
+ }
107
+ function findMainThread(events) {
108
+ const metadata = events.find((e) => e.cat === "__metadata" && e.name === "thread_name" && e.args?.name === "CrRendererMain");
109
+ if (metadata)
110
+ return metadata.tid;
111
+ const threadCounts = new Map;
112
+ for (const e of events) {
113
+ if (e.name === "FunctionCall") {
114
+ threadCounts.set(e.tid, (threadCounts.get(e.tid) ?? 0) + 1);
115
+ }
116
+ }
117
+ let maxTid = 0;
118
+ let maxCount = 0;
119
+ for (const [tid, count] of threadCounts) {
120
+ if (count > maxCount) {
121
+ maxTid = tid;
122
+ maxCount = count;
123
+ }
124
+ }
125
+ return maxTid;
126
+ }
127
+ function extractBlockingFunctions(mainEvents, navigationStartTs) {
128
+ const results = [];
129
+ for (const e of mainEvents) {
130
+ if (e.ph !== "X" || !BLOCKING_EVENT_NAMES.has(e.name) || !e.dur || e.dur < BLOCKING_THRESHOLD_US) {
131
+ continue;
132
+ }
133
+ const data = e.args?.data;
134
+ const functionName = data?.functionName || e.name;
135
+ const scriptUrl = data?.url || "";
136
+ const lineNumber = data?.lineNumber ?? 0;
137
+ const columnNumber = data?.columnNumber ?? 0;
138
+ const callStack = [];
139
+ if (data?.stackTrace) {
140
+ for (const frame of data.stackTrace) {
141
+ callStack.push({
142
+ functionName: frame.functionName || "(anonymous)",
143
+ scriptUrl: frame.url || "",
144
+ lineNumber: frame.lineNumber ?? 0
145
+ });
146
+ }
147
+ }
148
+ results.push({
149
+ functionName,
150
+ scriptUrl,
151
+ lineNumber,
152
+ columnNumber,
153
+ duration: e.dur / 1000,
154
+ startTime: (e.ts - navigationStartTs) / 1000,
155
+ callStack,
156
+ category: "scripting"
157
+ });
158
+ }
159
+ results.sort((a, b) => b.duration - a.duration);
160
+ return results;
161
+ }
162
+ function extractEventListenerInfo(mainEvents) {
163
+ const eventMap = new Map;
164
+ for (const e of mainEvents) {
165
+ if (e.name !== "EventDispatch" || e.ph !== "X")
166
+ continue;
167
+ const eventType = e.args?.data?.type || "unknown";
168
+ let entry = eventMap.get(eventType);
169
+ if (!entry) {
170
+ entry = { count: 0, totalDuration: 0, sources: new Map };
171
+ eventMap.set(eventType, entry);
172
+ }
173
+ entry.count++;
174
+ entry.totalDuration += (e.dur ?? 0) / 1000;
175
+ const url = e.args?.data?.url || "";
176
+ const line = e.args?.data?.lineNumber ?? 0;
177
+ if (url) {
178
+ const key = `${url}:${line}`;
179
+ const src = entry.sources.get(key);
180
+ if (src) {
181
+ src.count++;
182
+ } else {
183
+ entry.sources.set(key, { scriptUrl: url, lineNumber: line, count: 1 });
184
+ }
185
+ }
186
+ }
187
+ const results = [];
188
+ for (const [eventType, data] of eventMap) {
189
+ results.push({
190
+ eventType,
191
+ addCount: data.count,
192
+ removeCount: 0,
193
+ activeCount: data.count,
194
+ sources: Array.from(data.sources.values())
195
+ });
196
+ }
197
+ results.sort((a, b) => b.addCount - a.addCount);
198
+ return results;
199
+ }
200
+ function buildFrameBreakdown(mainEvents) {
201
+ let scripting = 0;
202
+ let layout = 0;
203
+ let painting = 0;
204
+ let gc = 0;
205
+ let other = 0;
206
+ for (const e of mainEvents) {
207
+ if (e.ph !== "X" || !e.dur)
208
+ continue;
209
+ const durMs = e.dur / 1000;
210
+ if (SCRIPTING_EVENTS.has(e.name)) {
211
+ scripting += durMs;
212
+ } else if (LAYOUT_EVENTS.has(e.name)) {
213
+ layout += durMs;
214
+ } else if (PAINTING_EVENTS.has(e.name)) {
215
+ painting += durMs;
216
+ } else if (GC_EVENT_NAMES.has(e.name)) {
217
+ gc += durMs;
218
+ } else {
219
+ other += durMs;
220
+ }
221
+ }
222
+ const totalTime = scripting + layout + painting + gc + other;
223
+ return {
224
+ totalTime: round(totalTime),
225
+ scripting: round(scripting),
226
+ layout: round(layout),
227
+ painting: round(painting),
228
+ gc: round(gc),
229
+ other: round(other)
230
+ };
231
+ }
232
+ function extractGCEvents(mainEvents, navigationStartTs) {
233
+ const results = [];
234
+ for (const e of mainEvents) {
235
+ if (e.ph !== "X" || !GC_EVENT_NAMES.has(e.name))
236
+ continue;
237
+ results.push({
238
+ startTime: round((e.ts - navigationStartTs) / 1000),
239
+ duration: round((e.dur ?? 0) / 1000),
240
+ type: e.name,
241
+ usedHeapSizeBefore: e.args?.data?.usedHeapSizeBefore,
242
+ usedHeapSizeAfter: e.args?.data?.usedHeapSizeAfter
243
+ });
244
+ }
245
+ results.sort((a, b) => a.startTime - b.startTime);
246
+ return results;
247
+ }
248
+ function findFrequentEvents(mainEvents) {
249
+ const counts = new Map;
250
+ for (const e of mainEvents) {
251
+ if (e.name !== "EventDispatch" || e.ph !== "X")
252
+ continue;
253
+ const eventType = e.args?.data?.type || "unknown";
254
+ const entry = counts.get(eventType);
255
+ if (entry) {
256
+ entry.count++;
257
+ entry.totalDuration += (e.dur ?? 0) / 1000;
258
+ } else {
259
+ counts.set(eventType, { count: 1, totalDuration: (e.dur ?? 0) / 1000 });
260
+ }
261
+ }
262
+ const results = [];
263
+ for (const [eventType, data] of counts) {
264
+ if (data.count > 10) {
265
+ results.push({
266
+ eventType,
267
+ count: data.count,
268
+ totalDuration: round(data.totalDuration)
269
+ });
270
+ }
271
+ }
272
+ results.sort((a, b) => b.count - a.count);
273
+ return results;
274
+ }
275
+ function round(n) {
276
+ return Math.round(n * 100) / 100;
277
+ }
278
+ function emptyRuntimeTrace() {
279
+ return {
280
+ totalEvents: 0,
281
+ mainThreadId: 0,
282
+ traceDuration: 0,
283
+ frameBreakdown: {
284
+ totalTime: 0,
285
+ scripting: 0,
286
+ layout: 0,
287
+ painting: 0,
288
+ gc: 0,
289
+ other: 0
290
+ },
291
+ blockingFunctions: [],
292
+ eventListeners: [],
293
+ gcEvents: [],
294
+ frequentEvents: []
295
+ };
296
+ }
297
+
298
+ // src/browser/trace.ts
299
+ var TEXT_RESOURCE_TYPES = new Set(["Script", "Stylesheet", "Document", "XHR", "Fetch"]);
300
+ var MAX_BODY_SIZE = 2 * 1024 * 1024;
301
+ var TRACE_CATEGORIES = [
302
+ "devtools.timeline",
303
+ "disabled-by-default-devtools.timeline",
304
+ "disabled-by-default-devtools.timeline.stack",
305
+ "v8.execute",
306
+ "blink.user_timing",
307
+ "disabled-by-default-v8.gc"
308
+ ];
309
+ var TRACING_COMPLETE_TIMEOUT = 1e4;
310
+ async function tracePageLoad(cdpSession, _options = {}) {
311
+ const requests = new Map;
312
+ const finishedIds = new Set;
313
+ const traceEvents = [];
314
+ let tracingStarted = false;
315
+ try {
316
+ cdpSession.on("Tracing.dataCollected", (params) => {
317
+ if (params.value && Array.isArray(params.value)) {
318
+ traceEvents.push(...params.value);
319
+ }
320
+ });
321
+ await cdpSession.send("Tracing.start", {
322
+ traceConfig: {
323
+ includedCategories: TRACE_CATEGORIES,
324
+ recordMode: "recordUntilFull"
325
+ },
326
+ transferMode: "ReportEvents"
327
+ });
328
+ tracingStarted = true;
329
+ } catch {}
330
+ await cdpSession.send("Network.enable");
331
+ cdpSession.on("Network.requestWillBeSent", (params) => {
332
+ requests.set(params.requestId, {
333
+ requestId: params.requestId,
334
+ url: params.request.url,
335
+ method: params.request.method,
336
+ startTime: params.timestamp * 1000,
337
+ initiator: params.initiator?.type ?? "other",
338
+ resourceType: params.type ?? "Other"
339
+ });
340
+ });
341
+ cdpSession.on("Network.responseReceived", (params) => {
342
+ const req = requests.get(params.requestId);
343
+ if (req) {
344
+ req.status = params.response.status;
345
+ req.mimeType = params.response.mimeType;
346
+ req.encodedSize = params.response.encodedDataLength ?? 0;
347
+ req.priority = params.response.priority ?? "Medium";
348
+ req.isRenderBlocking = params.response.renderBlocking === "blocking";
349
+ req.resourceType = params.type ?? req.resourceType ?? "Other";
350
+ }
351
+ });
352
+ cdpSession.on("Network.loadingFinished", (params) => {
353
+ const req = requests.get(params.requestId);
354
+ if (req) {
355
+ req.endTime = params.timestamp * 1000;
356
+ req.duration = (req.endTime ?? 0) - (req.startTime ?? 0);
357
+ req.decodedSize = params.encodedDataLength ?? 0;
358
+ finishedIds.add(params.requestId);
359
+ }
360
+ });
361
+ await cdpSession.send("Page.enable");
362
+ await cdpSession.send("Page.addScriptToEvaluateOnNewDocument", {
363
+ source: `
364
+ window.__zeitzeuge_longTasks = [];
365
+ new PerformanceObserver((list) => {
366
+ for (const entry of list.getEntries()) {
367
+ window.__zeitzeuge_longTasks.push({
368
+ startTime: entry.startTime,
369
+ duration: entry.duration,
370
+ scriptUrl: null,
371
+ });
372
+ }
373
+ }).observe({ type: "longtask", buffered: true });
374
+ `
375
+ });
376
+ return {
377
+ async stop() {
378
+ let runtimeTrace;
379
+ if (tracingStarted) {
380
+ try {
381
+ const tracingComplete = new Promise((resolve) => {
382
+ cdpSession.on("Tracing.tracingComplete", () => resolve());
383
+ });
384
+ await cdpSession.send("Tracing.end");
385
+ await Promise.race([
386
+ tracingComplete,
387
+ new Promise((resolve) => setTimeout(resolve, TRACING_COMPLETE_TIMEOUT))
388
+ ]);
389
+ } catch {}
390
+ if (traceEvents.length > 0) {
391
+ const navEvent = traceEvents.find((e) => e.name === "navigationStart" || e.name === "NavigationStart");
392
+ const navigationStartTs = navEvent?.ts ?? traceEvents[0]?.ts ?? 0;
393
+ runtimeTrace = parseRuntimeTrace(traceEvents, navigationStartTs);
394
+ }
395
+ }
396
+ const networkRequests = [];
397
+ for (const [id, req] of requests) {
398
+ let responseBody = null;
399
+ if (finishedIds.has(id) && TEXT_RESOURCE_TYPES.has(req.resourceType ?? "")) {
400
+ try {
401
+ const bodyResult = await cdpSession.send("Network.getResponseBody", { requestId: id });
402
+ responseBody = bodyResult.body;
403
+ if (responseBody && responseBody.length > MAX_BODY_SIZE) {
404
+ responseBody = null;
405
+ }
406
+ } catch {}
407
+ }
408
+ networkRequests.push({
409
+ requestId: req.requestId ?? id,
410
+ url: req.url ?? "",
411
+ method: req.method ?? "GET",
412
+ resourceType: req.resourceType ?? "Other",
413
+ mimeType: req.mimeType ?? "",
414
+ status: req.status ?? 0,
415
+ encodedSize: req.encodedSize ?? 0,
416
+ decodedSize: req.decodedSize ?? 0,
417
+ startTime: req.startTime ?? 0,
418
+ endTime: req.endTime ?? 0,
419
+ duration: req.duration ?? 0,
420
+ isRenderBlocking: req.isRenderBlocking ?? false,
421
+ responseBody,
422
+ priority: req.priority ?? "Medium",
423
+ initiator: req.initiator ?? "other"
424
+ });
425
+ }
426
+ let longTasks = [];
427
+ try {
428
+ const longTaskResult = await cdpSession.send("Runtime.evaluate", {
429
+ expression: "JSON.stringify(window.__zeitzeuge_longTasks || [])"
430
+ });
431
+ longTasks = JSON.parse(longTaskResult.result.value || "[]");
432
+ } catch {}
433
+ let perfMetrics = {};
434
+ try {
435
+ const metricsResult = await cdpSession.send("Runtime.evaluate", {
436
+ expression: `JSON.stringify({
437
+ navigationStart: performance.timing.navigationStart,
438
+ domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
439
+ loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart,
440
+ })`
441
+ });
442
+ perfMetrics = JSON.parse(metricsResult.result.value || "{}");
443
+ } catch {}
444
+ let paintEntries = [];
445
+ try {
446
+ const paintResult = await cdpSession.send("Runtime.evaluate", {
447
+ expression: `JSON.stringify(
448
+ performance.getEntriesByType("paint").map(e => ({ name: e.name, startTime: e.startTime }))
449
+ )`
450
+ });
451
+ paintEntries = JSON.parse(paintResult.result.value || "[]");
452
+ } catch {}
453
+ const fp = paintEntries.find((e) => e.name === "first-paint");
454
+ const fcp = paintEntries.find((e) => e.name === "first-contentful-paint");
455
+ await cdpSession.send("Network.disable");
456
+ const metrics = {
457
+ navigationStart: perfMetrics.navigationStart ?? 0,
458
+ domContentLoaded: perfMetrics.domContentLoaded ?? 0,
459
+ loadComplete: perfMetrics.loadComplete ?? 0,
460
+ firstPaint: fp?.startTime ?? 0,
461
+ firstContentfulPaint: fcp?.startTime ?? 0,
462
+ largestContentfulPaint: 0,
463
+ totalBlockingTime: longTasks.reduce((sum, t) => sum + Math.max(0, t.duration - 50), 0),
464
+ longTasks
465
+ };
466
+ return {
467
+ networkRequests,
468
+ metrics,
469
+ runtimeTrace,
470
+ rawTraceEvents: tracingStarted ? traceEvents : undefined
471
+ };
472
+ }
473
+ };
474
+ }
475
+
476
+ // src/browser/capture.ts
477
+ async function capturePage(browser, url, options = {}) {
478
+ const { timeout = 30000 } = options;
479
+ const puppeteerBrowser = await browser.getPuppeteer();
480
+ const pages = await puppeteerBrowser.pages();
481
+ const page = pages[0];
482
+ if (!page) {
483
+ throw new Error("No page found in Puppeteer browser");
484
+ }
485
+ const cdpSession = await page.createCDPSession();
486
+ const traceHandle = await tracePageLoad(cdpSession, options);
487
+ await page.goto(url, {
488
+ waitUntil: "load",
489
+ timeout
490
+ });
491
+ await new Promise((r) => setTimeout(r, 2000));
492
+ const traceResult = await traceHandle.stop();
493
+ await cdpSession.send("HeapProfiler.enable");
494
+ const chunks = [];
495
+ cdpSession.on("HeapProfiler.addHeapSnapshotChunk", (params) => {
496
+ chunks.push(params.chunk);
497
+ });
498
+ await cdpSession.send("HeapProfiler.collectGarbage");
499
+ await cdpSession.send("HeapProfiler.takeHeapSnapshot", {
500
+ reportProgress: false
501
+ });
502
+ await cdpSession.send("HeapProfiler.disable");
503
+ await cdpSession.detach();
504
+ return {
505
+ heapSnapshot: {
506
+ data: chunks.join(""),
507
+ capturedAt: Date.now(),
508
+ url
509
+ },
510
+ trace: traceResult
511
+ };
512
+ }
513
+
514
+ // src/analysis/parser.ts
515
+ function parseSnapshot(rawSnapshot) {
516
+ const v8 = JSON.parse(rawSnapshot.data);
517
+ const meta = v8.snapshot.meta;
518
+ const nodeFieldCount = meta.node_fields.length;
519
+ const edgeFieldCount = meta.edge_fields.length;
520
+ const nodeTypes = meta.node_types[0];
521
+ const edgeTypes = meta.edge_types[0];
522
+ const nodeTypeIdx = meta.node_fields.indexOf("type");
523
+ const nodeNameIdx = meta.node_fields.indexOf("name");
524
+ const nodeIdIdx = meta.node_fields.indexOf("id");
525
+ const nodeSelfSizeIdx = meta.node_fields.indexOf("self_size");
526
+ const nodeEdgeCountIdx = meta.node_fields.indexOf("edge_count");
527
+ const nodeDetachednessIdx = meta.node_fields.indexOf("detachedness");
528
+ const edgeTypeIdx = meta.edge_fields.indexOf("type");
529
+ const edgeNameIdx = meta.edge_fields.indexOf("name_or_index");
530
+ const edgeToNodeIdx = meta.edge_fields.indexOf("to_node");
531
+ const nodeCount = v8.snapshot.node_count;
532
+ const nodes = [];
533
+ let edgeOffset = 0;
534
+ let totalSize = 0;
535
+ for (let i = 0;i < nodeCount; i++) {
536
+ const base = i * nodeFieldCount;
537
+ const selfSize = v8.nodes[base + nodeSelfSizeIdx] ?? 0;
538
+ totalSize += selfSize;
539
+ nodes.push({
540
+ ordinal: i,
541
+ type: nodeTypes[v8.nodes[base + nodeTypeIdx] ?? 0] ?? "unknown",
542
+ name: v8.strings[v8.nodes[base + nodeNameIdx] ?? 0] ?? "",
543
+ id: v8.nodes[base + nodeIdIdx] ?? 0,
544
+ selfSize,
545
+ edgeCount: v8.nodes[base + nodeEdgeCountIdx] ?? 0,
546
+ detachedness: nodeDetachednessIdx >= 0 ? v8.nodes[base + nodeDetachednessIdx] ?? 0 : 0,
547
+ edgeStartIndex: edgeOffset
548
+ });
549
+ edgeOffset += (v8.nodes[base + nodeEdgeCountIdx] ?? 0) * edgeFieldCount;
550
+ }
551
+ const adjacency = Array.from({ length: nodeCount }, () => []);
552
+ for (let i = 0;i < nodeCount; i++)
553
+ adjacency[i] = [];
554
+ const reverseAdj = Array.from({ length: nodeCount }, () => []);
555
+ for (let i = 0;i < nodeCount; i++)
556
+ reverseAdj[i] = [];
557
+ for (let i = 0;i < nodeCount; i++) {
558
+ const node = nodes[i];
559
+ for (let e = 0;e < node.edgeCount; e++) {
560
+ const edgeBase = node.edgeStartIndex + e * edgeFieldCount;
561
+ const edgeTypeVal = edgeTypes[v8.edges[edgeBase + edgeTypeIdx] ?? 0];
562
+ if (edgeTypeVal === "weak")
563
+ continue;
564
+ const toNodeArrayIdx = v8.edges[edgeBase + edgeToNodeIdx] ?? 0;
565
+ const toOrdinal = toNodeArrayIdx / nodeFieldCount;
566
+ if (toOrdinal >= 0 && toOrdinal < nodeCount) {
567
+ adjacency[i].push(toOrdinal);
568
+ reverseAdj[toOrdinal].push(i);
569
+ }
570
+ }
571
+ }
572
+ const visited = new Uint8Array(nodeCount);
573
+ const bfsOrder = [];
574
+ const queue = [0];
575
+ visited[0] = 1;
576
+ while (queue.length > 0) {
577
+ const curr = queue.shift();
578
+ bfsOrder.push(curr);
579
+ for (const next of adjacency[curr]) {
580
+ if (!visited[next]) {
581
+ visited[next] = 1;
582
+ queue.push(next);
583
+ }
584
+ }
585
+ }
586
+ const dominators = new Int32Array(nodeCount).fill(-1);
587
+ dominators[0] = 0;
588
+ const nodeToRpo = new Int32Array(nodeCount).fill(-1);
589
+ for (let i = 0;i < bfsOrder.length; i++) {
590
+ nodeToRpo[bfsOrder[i]] = i;
591
+ }
592
+ function intersect(b1Init, b2Init) {
593
+ let b1 = b1Init;
594
+ let b2 = b2Init;
595
+ let finger1 = nodeToRpo[b1];
596
+ let finger2 = nodeToRpo[b2];
597
+ while (finger1 !== finger2) {
598
+ while (finger1 > finger2) {
599
+ b1 = dominators[b1];
600
+ if (b1 < 0)
601
+ return 0;
602
+ finger1 = nodeToRpo[b1];
603
+ }
604
+ while (finger2 > finger1) {
605
+ b2 = dominators[b2];
606
+ if (b2 < 0)
607
+ return 0;
608
+ finger2 = nodeToRpo[b2];
609
+ }
610
+ }
611
+ return b1;
612
+ }
613
+ let changed = true;
614
+ while (changed) {
615
+ changed = false;
616
+ for (let i = 1;i < bfsOrder.length; i++) {
617
+ const node = bfsOrder[i];
618
+ let newIdom = -1;
619
+ for (const pred of reverseAdj[node]) {
620
+ if (dominators[pred] < 0)
621
+ continue;
622
+ if (newIdom < 0) {
623
+ newIdom = pred;
624
+ } else {
625
+ newIdom = intersect(newIdom, pred);
626
+ }
627
+ }
628
+ if (newIdom >= 0 && dominators[node] !== newIdom) {
629
+ dominators[node] = newIdom;
630
+ changed = true;
631
+ }
632
+ }
633
+ }
634
+ const retainedSizes = new Float64Array(nodeCount);
635
+ for (let i = 0;i < nodeCount; i++) {
636
+ retainedSizes[i] = nodes[i].selfSize;
637
+ }
638
+ for (let i = bfsOrder.length - 1;i > 0; i--) {
639
+ const node = bfsOrder[i];
640
+ const dom = dominators[node];
641
+ if (dom >= 0 && dom !== node) {
642
+ retainedSizes[dom] = retainedSizes[dom] + retainedSizes[node];
643
+ }
644
+ }
645
+ const indexed = bfsOrder.filter((i) => i > 0).map((i) => ({ ordinal: i, retainedSize: retainedSizes[i] })).sort((a, b) => b.retainedSize - a.retainedSize).slice(0, 50);
646
+ function getEdgeName(toOrdinal) {
647
+ const dom = dominators[toOrdinal];
648
+ if (dom < 0)
649
+ return "";
650
+ const domNode = nodes[dom];
651
+ for (let e = 0;e < domNode.edgeCount; e++) {
652
+ const edgeBase = domNode.edgeStartIndex + e * edgeFieldCount;
653
+ const toNodeArrayIdx = v8.edges[edgeBase + edgeToNodeIdx] ?? 0;
654
+ if (toNodeArrayIdx / nodeFieldCount === toOrdinal) {
655
+ const nameOrIndex = v8.edges[edgeBase + edgeNameIdx] ?? 0;
656
+ const edgeTypeVal = edgeTypes[v8.edges[edgeBase + edgeTypeIdx] ?? 0];
657
+ if (edgeTypeVal === "element")
658
+ return `[${nameOrIndex}]`;
659
+ return v8.strings[nameOrIndex] ?? String(nameOrIndex);
660
+ }
661
+ }
662
+ return "";
663
+ }
664
+ function getRetainerPath(targetOrdinal, maxDepth = 10) {
665
+ const path = [];
666
+ let current = targetOrdinal;
667
+ const pathVisited = new Set;
668
+ for (let depth = 0;depth < maxDepth; depth++) {
669
+ const node = nodes[current];
670
+ const edgeName = getEdgeName(current);
671
+ path.unshift(edgeName ? `${node.name || node.type}` : node.name || node.type);
672
+ if (current === 0)
673
+ break;
674
+ pathVisited.add(current);
675
+ const dom = dominators[current];
676
+ if (dom < 0 || dom === current || pathVisited.has(dom))
677
+ break;
678
+ current = dom;
679
+ }
680
+ return path;
681
+ }
682
+ const largestObjects = indexed.map(({ ordinal, retainedSize }) => {
683
+ const node = nodes[ordinal];
684
+ return {
685
+ name: node.name || `(${node.type})`,
686
+ type: node.type,
687
+ selfSize: node.selfSize,
688
+ retainedSize,
689
+ retainerPath: getRetainerPath(ordinal)
690
+ };
691
+ });
692
+ const typeMap = new Map;
693
+ for (const node of nodes) {
694
+ if (!visited[node.ordinal])
695
+ continue;
696
+ const existing = typeMap.get(node.type);
697
+ if (existing) {
698
+ existing.count++;
699
+ existing.totalSize += node.selfSize;
700
+ } else {
701
+ typeMap.set(node.type, { count: 1, totalSize: node.selfSize });
702
+ }
703
+ }
704
+ const typeStats = Array.from(typeMap.entries()).map(([type, stats]) => ({
705
+ type,
706
+ count: stats.count,
707
+ totalSize: stats.totalSize,
708
+ avgSize: stats.count > 0 ? Math.round(stats.totalSize / stats.count) : 0
709
+ })).sort((a, b) => b.totalSize - a.totalSize);
710
+ const ctorMap = new Map;
711
+ for (const node of nodes) {
712
+ if (!visited[node.ordinal])
713
+ continue;
714
+ if (node.type !== "object" || !node.name)
715
+ continue;
716
+ const existing = ctorMap.get(node.name);
717
+ if (existing) {
718
+ existing.count++;
719
+ existing.totalSize += node.selfSize;
720
+ } else {
721
+ ctorMap.set(node.name, { count: 1, totalSize: node.selfSize });
722
+ }
723
+ }
724
+ const constructorStats = Array.from(ctorMap.entries()).map(([ctor, stats]) => ({
725
+ constructor: ctor,
726
+ count: stats.count,
727
+ totalSize: stats.totalSize,
728
+ avgSize: stats.count > 0 ? Math.round(stats.totalSize / stats.count) : 0
729
+ })).sort((a, b) => b.totalSize - a.totalSize).slice(0, 30);
730
+ const detachedExamples = [];
731
+ let detachedCount = 0;
732
+ let detachedTotalSize = 0;
733
+ for (const node of nodes) {
734
+ if (!visited[node.ordinal])
735
+ continue;
736
+ const isDetached = nodeDetachednessIdx >= 0 && node.detachedness > 0 || node.name.includes("Detached") || node.type === "native" && /HTML\w*Element|Document|Node/.test(node.name) && node.name.includes("Detached");
737
+ if (isDetached) {
738
+ detachedCount++;
739
+ detachedTotalSize += node.selfSize;
740
+ if (detachedExamples.length < 10) {
741
+ detachedExamples.push({
742
+ name: node.name || `(${node.type})`,
743
+ retainerPath: getRetainerPath(node.ordinal)
744
+ });
745
+ }
746
+ }
747
+ }
748
+ const detachedNodes = {
749
+ count: detachedCount,
750
+ totalSize: detachedTotalSize,
751
+ examples: detachedExamples
752
+ };
753
+ const closureNodes = nodes.filter((n) => visited[n.ordinal] && n.type === "closure");
754
+ const closureTotalSize = closureNodes.reduce((sum, n) => sum + n.selfSize, 0);
755
+ const topClosures = closureNodes.sort((a, b) => b.selfSize - a.selfSize).slice(0, 20).map((n) => ({
756
+ name: n.name || "(anonymous)",
757
+ contextSize: n.selfSize,
758
+ retainerPath: getRetainerPath(n.ordinal)
759
+ }));
760
+ const closureStats = {
761
+ count: closureNodes.length,
762
+ totalSize: closureTotalSize,
763
+ topClosures
764
+ };
765
+ return {
766
+ metadata: {
767
+ url: rawSnapshot.url,
768
+ capturedAt: rawSnapshot.capturedAt,
769
+ totalSize,
770
+ nodeCount: v8.snapshot.node_count,
771
+ edgeCount: v8.snapshot.edge_count
772
+ },
773
+ largestObjects,
774
+ typeStats,
775
+ constructorStats,
776
+ detachedNodes,
777
+ closureStats
778
+ };
779
+ }
780
+
781
+ // src/analysis/agent.ts
782
+ import { createDeepAgent } from "deepagents";
783
+ import { providerStrategy } from "langchain";
784
+
785
+ // src/analysis/prompts.ts
786
+ var SYSTEM_PROMPT = `You are an expert web performance engineer. You have access to a virtual filesystem workspace containing captured data from a real page load: heap snapshot, network trace, and Chrome runtime trace.
787
+
788
+ ## Workspace structure
789
+
790
+ - /heap/summary.json — Parsed V8 heap snapshot: largest objects, type stats, constructor stats, detached DOM nodes, closure stats
791
+ - /trace/summary.json — Page load metrics: timing, long tasks, render-blocking resources, resource breakdown
792
+ - /trace/network-waterfall.json — Every network request with timing, size, priority, render-blocking status
793
+ - /trace/asset-manifest.json — Index of all assets with paths to stored files
794
+ - /trace/runtime/summary.json — Runtime trace overview: frame breakdown (scripting/layout/paint/GC), blocking function count, listener imbalances, GC stats
795
+ - /trace/runtime/blocking-functions.json — Functions that blocked the main thread > 50ms, with script URL, line number, call stack, and duration
796
+ - /trace/runtime/event-listeners.json — Event listener add/remove counts per event type, with source locations
797
+ - /trace/runtime/frame-breakdown.json — Time spent in scripting vs layout vs paint vs GC
798
+ - /trace/runtime/raw-events.json — Full Chrome trace events (large file — read to investigate specific function calls, layouts, GC, and event dispatches)
799
+ - /scripts/*.js — Actual JavaScript source files captured during page load
800
+ - /styles/*.css — Actual CSS source files
801
+ - /html/document.html — The HTML document
802
+
803
+ ## Your workflow
804
+
805
+ 1. Read /heap/summary.json, /trace/summary.json, AND /trace/runtime/summary.json first for the big picture
806
+ 2. Identify the highest-impact issues from all datasets
807
+ 3. For each issue, dive into the relevant source files to understand the root cause
808
+ 4. Provide specific, code-level fixes
809
+
810
+ ## What to look for
811
+
812
+ ### Memory issues (from heap data)
813
+ - Memory leaks: unbounded arrays, maps, caches that grow without bound
814
+ - Detached DOM nodes: DOM elements removed from the document but still referenced
815
+ - Large retained objects: single objects or trees retaining disproportionate memory
816
+ - Closure leaks: closures capturing variables they no longer need
817
+
818
+ ### Page-load issues (from trace + source code)
819
+ - Render-blocking scripts: <script> in <head> without async/defer — read the script to judge if it must be synchronous
820
+ - Render-blocking CSS: large stylesheets blocking first paint
821
+ - Long tasks (> 50ms): identify the function/module causing the block by reading the source
822
+ - Large bundles: scripts > 100KB — search for unused imports or code that could be lazy-loaded
823
+ - Sequential waterfalls: resources chained sequentially that could load in parallel
824
+
825
+ ### Runtime issues (from Chrome trace)
826
+ - Frame-blocking functions: read /trace/runtime/blocking-functions.json first, then inspect the actual script source at the reported line number to understand what the function does and how to optimize it
827
+ - Event listener leaks: check /trace/runtime/event-listeners.json for event types where addCount >> removeCount, then grep the scripts for those addEventListener calls
828
+ - GC pressure: high GC pause counts or duration suggest excessive short-lived object creation — look for hot loops creating objects
829
+ - Layout thrashing: forced synchronous layouts caused by reading layout properties (offsetHeight, getBoundingClientRect) after DOM writes
830
+
831
+ ## Output guidelines
832
+
833
+ - Report 3–7 findings, ordered by impact (mix of memory, page-load, and runtime if all have issues)
834
+ - Be specific — name actual files, functions, object constructors, and retention paths
835
+ - Provide concrete code fixes, not generic advice
836
+ - If heap, trace, and runtime all look healthy, say so — don't manufacture issues`;
837
+
838
+ // src/vitest/prompts.ts
839
+ var VITEST_SYSTEM_PROMPT = `You are an expert in JavaScript/TypeScript performance optimization.
840
+ You have access to a workspace containing V8 CPU profiling data captured during
841
+ a Vitest test run. The workspace may also include V8 heap profiling data
842
+ captured via Node's \`--heap-prof\` (allocation sampling).
843
+
844
+ The profiling data covers BOTH the test code AND the application code being tested.
845
+
846
+ **Your primary goal is to analyze the PERFORMANCE OF THE APPLICATION CODE
847
+ being tested** — the functions, modules, and algorithms that the developer
848
+ wrote and is benchmarking or testing. Test infrastructure overhead (Vitest,
849
+ tinybench, test setup) is secondary context.
850
+
851
+ ## Source categories
852
+
853
+ Every hot function and script in the workspace has a \`sourceCategory\` field:
854
+
855
+ - **application** — Code in the user's project (the code being tested).
856
+ This is your PRIMARY focus. Find bottlenecks, inefficiencies, and
857
+ optimization opportunities in these functions.
858
+ - **dependency** — Third-party code in node_modules. Report when a dependency
859
+ is a significant bottleneck, since the developer may be able to choose
860
+ an alternative, configure it differently, or avoid calling it in hot paths.
861
+ - **test** — Test files. Only mention if the test setup itself is creating
862
+ artificial overhead that masks application performance.
863
+ - **framework** — Vitest/tinybench/V8 internals. Generally ignore unless
864
+ they dominate the profile in an unexpected way.
865
+
866
+ ## Workspace structure
867
+
868
+ - /summary.json — Overall test run: total tests, duration, pass/fail, GC stats
869
+ - /timing/overview.json — Per-file test durations and individual test times
870
+ - /timing/slow-tests.json — Tests exceeding the slow threshold
871
+ - /profiles/index.json — Manifest mapping test files to their CPU profiles
872
+ - /profiles/<file>.json — CPU profile summary: hot functions (with sourceCategory),
873
+ call trees, GC samples, script breakdown (with sourceCategory)
874
+ - /heap-profiles/index.json — (optional) Manifest mapping test files to heap profiles
875
+ - /heap-profiles/<file>.json — (optional) Heap profile summary: allocation hotspots,
876
+ per-script allocated bytes (with sourceCategory)
877
+ - /hot-functions/application.json — **START HERE**: Hot functions from application code only
878
+ - /hot-functions/dependencies.json — Hot functions from third-party dependencies
879
+ - /hot-functions/global.json — All hot functions across all categories
880
+ - /scripts/application.json — Per-script time breakdown for application code
881
+ - /scripts/dependencies.json — Per-script time breakdown for dependencies
882
+ - /tests/*.ts — Test source files
883
+ - /src/*.ts — Application and dependency source files referenced by hot functions
884
+
885
+ ## Your workflow
886
+
887
+ 1. Read /hot-functions/application.json FIRST — these are the application-level
888
+ bottlenecks the developer wants to optimize
889
+ 2. Read /scripts/application.json for the per-file view of application code time
890
+ 3. Read /hot-functions/dependencies.json for costly dependency calls
891
+ 4. If present, read /heap-profiles/index.json and /heap-profiles/<file>.json to identify
892
+ allocation hotspots (functions/scripts allocating lots of bytes)
893
+ 5. Read /summary.json and /timing/overview.json for the big picture
894
+ 6. Read CPU profiles in /profiles/ for detailed call trees of the slowest tests
895
+ 7. Read the actual source code in /src/ and /tests/ to understand root causes
896
+ 8. Provide specific, actionable fixes targeting the application code
897
+
898
+ ## What to look for
899
+
900
+ ### Application code bottlenecks (PRIMARY FOCUS)
901
+ - Functions with high self time — where is the application spending CPU?
902
+ - Expensive algorithms: O(n²) loops, unnecessary sorting, repeated work
903
+ - String/JSON operations: excessive serialization, string concatenation in loops
904
+ - Object allocation hotspots: functions creating many short-lived objects
905
+ - Synchronous blocking: file I/O, crypto, or compression in hot paths
906
+ - Redundant computation: values computed repeatedly that could be cached/memoized
907
+ - Data structure choices: using arrays where Maps/Sets would be O(1)
908
+
909
+ ### Dependency-related bottlenecks
910
+ - Dependencies consuming disproportionate CPU — suggest alternatives or configuration
911
+ - Unnecessary calls to expensive dependency APIs in hot paths
912
+ - Dependencies pulled in for simple operations that could be hand-written
913
+
914
+ ### GC pressure from application code
915
+ - Application functions creating many temporary objects in tight loops
916
+ - Large array/object allocations that could be pooled or reused
917
+ - Closures capturing large scopes unnecessarily
918
+
919
+ ### Allocation hotspots (from heap profiles, if present)
920
+ - Functions allocating a large share of total bytes (even if CPU isn't dominant)
921
+ - Scripts/modules responsible for most allocation — suggest caching, reuse, pooling,
922
+ or avoiding intermediate arrays/objects
923
+ - When allocation hotspots match CPU hotspots, prioritize fixes there first
924
+
925
+ ### Call chain analysis
926
+ - Trace expensive call trees to find which APPLICATION function triggers them
927
+ - Follow the call tree from application entry points down to the hot leaf functions
928
+ - Identify which application-level design decisions lead to the bottleneck
929
+
930
+ ### Test infrastructure (SECONDARY — only if impactful)
931
+ - Test setup creating artificial overhead that dwarfs application execution
932
+ - Benchmarks measuring setup cost instead of application performance
933
+ - Only mention if it prevents getting clean application performance data
934
+
935
+ ## Finding categories
936
+
937
+ Each finding MUST use one of these exact category values:
938
+
939
+ - **algorithm** — Inefficient algorithm: O(n²) loops, brute-force search, repeated work
940
+ - **serialization** — Excessive JSON.stringify/parse, string concatenation, encoding
941
+ - **allocation** — Excessive object/array creation causing GC pressure
942
+ - **event-handling** — Listener leaks, unbounded event handler accumulation
943
+ - **hot-function** — Generic CPU-hot function that doesn't fit a more specific category
944
+ - **gc-pressure** — High garbage collection overhead
945
+ - **listener-leak** — Event listeners not cleaned up properly
946
+ - **unnecessary-computation** — Redundant work that could be cached or eliminated
947
+ - **blocking-io** — Synchronous I/O or blocking operations in hot paths
948
+ - **dependency-bottleneck** — Expensive dependency call the developer can optimize
949
+ - **slow-test** — Test itself is slow due to setup or teardown
950
+ - **expensive-setup** — Costly test setup (beforeAll/beforeEach)
951
+ - **import-overhead** — Expensive module imports at test time
952
+ - **other** — Doesn't fit any of the above
953
+
954
+ Prefer more specific categories (algorithm, serialization, allocation, event-handling)
955
+ over generic ones (hot-function, other) when the root cause is clear.
956
+
957
+ ## Output guidelines
958
+
959
+ - Report 3–7 findings, ordered by impact ON THE APPLICATION CODE
960
+ - Focus findings on functions the developer CAN change (application code first,
961
+ then dependency usage patterns, then test structure)
962
+ - Be specific — name actual files, functions, line numbers from the source code
963
+ - Provide concrete code-level fixes, not generic advice
964
+ - When reporting a dependency bottleneck, explain what application code is
965
+ calling it and how the developer can reduce that cost
966
+ - If the application code is already efficient, say so — don't force findings
967
+ about test infrastructure just to fill the report`;
968
+
969
+ // src/schema.ts
970
+ import { z } from "zod";
971
+ var ALL_CATEGORIES = [
972
+ "memory-leak",
973
+ "large-retained-object",
974
+ "detached-dom",
975
+ "render-blocking",
976
+ "long-task",
977
+ "unused-code",
978
+ "waterfall-bottleneck",
979
+ "large-asset",
980
+ "frame-blocking-function",
981
+ "listener-leak",
982
+ "gc-pressure",
983
+ "slow-test",
984
+ "expensive-setup",
985
+ "hot-function",
986
+ "unnecessary-computation",
987
+ "import-overhead",
988
+ "dependency-bottleneck",
989
+ "algorithm",
990
+ "serialization",
991
+ "allocation",
992
+ "event-handling",
993
+ "blocking-io",
994
+ "other"
995
+ ];
996
+ var FindingSchema = z.object({
997
+ severity: z.enum(["critical", "warning", "info"]),
998
+ title: z.string().describe("Short title for the finding"),
999
+ description: z.string().describe("Detailed explanation of the issue"),
1000
+ category: z.string().describe(`Category of the performance issue. Use one of: ${ALL_CATEGORIES.join(", ")}`),
1001
+ resourceUrl: z.string().optional().describe("URL of the resource involved"),
1002
+ workspacePath: z.string().optional().describe("Path in the VFS workspace"),
1003
+ impactMs: z.number().optional().describe("Impact on page load time in ms"),
1004
+ retainedSize: z.number().optional().describe("Retained heap size in bytes"),
1005
+ retainerPath: z.array(z.string()).optional().describe("Object retention path in the heap"),
1006
+ suggestedFix: z.string().describe("Code snippet or guidance to fix the issue"),
1007
+ testFile: z.string().optional().describe("Test file path (for test performance findings)"),
1008
+ hotFunction: z.object({
1009
+ name: z.string(),
1010
+ scriptUrl: z.string(),
1011
+ lineNumber: z.number(),
1012
+ selfTime: z.number(),
1013
+ selfPercent: z.number()
1014
+ }).optional().describe("Hot function details (for hot-function findings)")
1015
+ });
1016
+ var FindingsSchema = z.object({
1017
+ findings: z.array(FindingSchema)
1018
+ });
1019
+
1020
+ // src/analysis/agent.ts
1021
+ async function analyze(model, backend) {
1022
+ const agent = createDeepAgent({
1023
+ model,
1024
+ systemPrompt: SYSTEM_PROMPT,
1025
+ backend,
1026
+ responseFormat: providerStrategy(FindingsSchema)
1027
+ });
1028
+ const userMessage = [
1029
+ "Analyze the frontend performance data in this workspace.",
1030
+ "",
1031
+ "The workspace contains heap snapshot data, page-load trace data, and Chrome runtime trace data.",
1032
+ "",
1033
+ "Start by reading /heap/summary.json, /trace/summary.json, and /trace/runtime/summary.json",
1034
+ "to understand the overall picture. Then explore:",
1035
+ "",
1036
+ "- /trace/network-waterfall.json for request timing",
1037
+ "- /trace/runtime/blocking-functions.json for function-level main thread blocking",
1038
+ "- /trace/runtime/event-listeners.json for listener add/remove imbalances",
1039
+ "- /trace/runtime/frame-breakdown.json for frame breakdown (scripting vs layout vs paint vs GC)",
1040
+ "- /scripts/ for the actual JavaScript source code",
1041
+ "- /styles/ for CSS source",
1042
+ "- /html/document.html for the page markup",
1043
+ "",
1044
+ "Look for memory issues (from the heap data), page-load issues (from the network trace),",
1045
+ "and runtime issues (from the Chrome trace — blocking functions, listener leaks, GC pressure).",
1046
+ "When you find a problem, read the actual source file to provide a specific, code-level fix."
1047
+ ].join(`
1048
+ `);
1049
+ const result = await agent.invoke({
1050
+ messages: [{ role: "user", content: userMessage }]
1051
+ });
1052
+ return result.structuredResponse.findings;
1053
+ }
1054
+ async function analyzeTestPerformance(model, backend) {
1055
+ const agent = createDeepAgent({
1056
+ model,
1057
+ systemPrompt: VITEST_SYSTEM_PROMPT,
1058
+ backend,
1059
+ responseFormat: providerStrategy(FindingsSchema)
1060
+ });
1061
+ const userMessage = [
1062
+ "Analyze the performance of the APPLICATION CODE being tested in this Vitest workspace.",
1063
+ "",
1064
+ "Follow this order:",
1065
+ "1. Read /hot-functions/application.json — these are the hotspots IN the user's own code",
1066
+ "2. Read /scripts/application.json — per-file CPU time for application source files",
1067
+ "3. Read /hot-functions/dependencies.json — expensive dependency calls",
1068
+ "4. If present, read /heap-profiles/index.json and /heap-profiles/<file>.json for allocation hotspots",
1069
+ "5. Read /summary.json and /timing/overview.json for the big picture",
1070
+ "6. Read CPU profiles in /profiles/ for detailed call trees",
1071
+ "7. Read source files in /src/ and /tests/ to understand root causes and propose code-level fixes",
1072
+ "",
1073
+ "Focus findings on the APPLICATION code — what can the developer change in their own codebase",
1074
+ "to improve performance? Dependency bottlenecks are worth reporting if the developer can",
1075
+ "reduce how they call the dependency or choose an alternative."
1076
+ ].join(`
1077
+ `);
1078
+ const result = await agent.invoke({
1079
+ messages: [{ role: "user", content: userMessage }]
1080
+ });
1081
+ return result.structuredResponse.findings;
1082
+ }
1083
+
1084
+ // src/sandbox/workspace.ts
1085
+ import { FilesystemBackend } from "deepagents";
1086
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
1087
+ import { join } from "node:path";
1088
+ import { tmpdir } from "node:os";
1089
+ async function createWorkspace(options) {
1090
+ const { heapSummary, traceResult, url, maxAssetSize = 10 * 1024 * 1024 } = options;
1091
+ const files = {};
1092
+ files["/heap/summary.json"] = JSON.stringify(heapSummary, null, 2);
1093
+ files["/trace/summary.json"] = JSON.stringify({
1094
+ url,
1095
+ timing: traceResult.metrics,
1096
+ requestCount: traceResult.networkRequests.length,
1097
+ totalTransferSize: traceResult.networkRequests.reduce((s, r) => s + r.encodedSize, 0),
1098
+ totalDecodedSize: traceResult.networkRequests.reduce((s, r) => s + r.decodedSize, 0),
1099
+ renderBlockingResources: traceResult.networkRequests.filter((r) => r.isRenderBlocking).map((r) => ({
1100
+ url: r.url,
1101
+ type: r.resourceType,
1102
+ size: r.decodedSize,
1103
+ duration: r.duration,
1104
+ path: r.responseBody ? getAssetPath(r) : null
1105
+ })),
1106
+ longTasks: traceResult.metrics.longTasks,
1107
+ resourceBreakdown: buildResourceBreakdown(traceResult.networkRequests)
1108
+ }, null, 2);
1109
+ files["/trace/network-waterfall.json"] = JSON.stringify(traceResult.networkRequests.sort((a, b) => a.startTime - b.startTime).map((r) => ({
1110
+ url: r.url,
1111
+ type: r.resourceType,
1112
+ status: r.status,
1113
+ size: r.decodedSize,
1114
+ startTime: Math.round(r.startTime),
1115
+ endTime: Math.round(r.endTime),
1116
+ duration: Math.round(r.duration),
1117
+ isRenderBlocking: r.isRenderBlocking,
1118
+ priority: r.priority,
1119
+ path: r.responseBody ? getAssetPath(r) : null
1120
+ })), null, 2);
1121
+ let totalStored = 0;
1122
+ for (const req of traceResult.networkRequests) {
1123
+ if (!req.responseBody)
1124
+ continue;
1125
+ if (totalStored + req.responseBody.length > maxAssetSize)
1126
+ continue;
1127
+ files[getAssetPath(req)] = req.responseBody;
1128
+ totalStored += req.responseBody.length;
1129
+ }
1130
+ files["/trace/asset-manifest.json"] = JSON.stringify(traceResult.networkRequests.map((r) => ({
1131
+ url: r.url,
1132
+ type: r.resourceType,
1133
+ size: r.decodedSize,
1134
+ duration: r.duration,
1135
+ isRenderBlocking: r.isRenderBlocking,
1136
+ stored: !!r.responseBody,
1137
+ path: r.responseBody ? getAssetPath(r) : null
1138
+ })), null, 2);
1139
+ if (traceResult.runtimeTrace) {
1140
+ const rt = traceResult.runtimeTrace;
1141
+ files["/trace/runtime/summary.json"] = JSON.stringify({
1142
+ totalEvents: rt.totalEvents,
1143
+ traceDuration: rt.traceDuration,
1144
+ mainThreadId: rt.mainThreadId,
1145
+ frameBreakdown: rt.frameBreakdown,
1146
+ blockingFunctionCount: rt.blockingFunctions.length,
1147
+ listenerImbalances: rt.eventListeners.filter((l) => l.activeCount > l.removeCount + 10).length,
1148
+ gcPauseCount: rt.gcEvents.length,
1149
+ gcTotalDuration: rt.gcEvents.reduce((s, e) => s + e.duration, 0),
1150
+ frequentEventTypes: rt.frequentEvents.map((e) => e.eventType)
1151
+ }, null, 2);
1152
+ files["/trace/runtime/blocking-functions.json"] = JSON.stringify(rt.blockingFunctions.slice(0, 50), null, 2);
1153
+ files["/trace/runtime/event-listeners.json"] = JSON.stringify(rt.eventListeners.filter((l) => l.addCount > 0), null, 2);
1154
+ files["/trace/runtime/frame-breakdown.json"] = JSON.stringify(rt.frameBreakdown, null, 2);
1155
+ }
1156
+ if (traceResult.rawTraceEvents && traceResult.rawTraceEvents.length > 0) {
1157
+ const rawJson = JSON.stringify(traceResult.rawTraceEvents);
1158
+ if (rawJson.length < 5 * 1024 * 1024) {
1159
+ files["/trace/runtime/raw-events.json"] = rawJson;
1160
+ } else {
1161
+ const mainTid = traceResult.runtimeTrace?.mainThreadId;
1162
+ const filtered = traceResult.rawTraceEvents.filter((e) => e.tid === mainTid && e.dur && e.dur > 0);
1163
+ const filteredJson = JSON.stringify(filtered);
1164
+ if (filteredJson.length < 5 * 1024 * 1024) {
1165
+ files["/trace/runtime/raw-events.json"] = filteredJson;
1166
+ } else {
1167
+ const important = filtered.filter((e) => (e.dur ?? 0) > 1000);
1168
+ files["/trace/runtime/raw-events.json"] = JSON.stringify(important);
1169
+ }
1170
+ }
1171
+ }
1172
+ const tempDir = mkdtempSync(join(tmpdir(), "zeitzeuge-workspace-"));
1173
+ for (const [filePath, content] of Object.entries(files)) {
1174
+ const relPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
1175
+ const fullPath = join(tempDir, relPath);
1176
+ mkdirSync(join(fullPath, ".."), { recursive: true });
1177
+ writeFileSync(fullPath, content, "utf-8");
1178
+ }
1179
+ const backend = new FilesystemBackend({
1180
+ rootDir: tempDir,
1181
+ virtualMode: true
1182
+ });
1183
+ const cleanup = () => {
1184
+ console.log(111, tempDir);
1185
+ };
1186
+ return { backend, cleanup };
1187
+ }
1188
+ function getAssetPath(req) {
1189
+ let filename;
1190
+ try {
1191
+ const pathname = new URL(req.url).pathname;
1192
+ filename = pathname.split("/").pop() || "index";
1193
+ } catch {
1194
+ filename = "unknown";
1195
+ }
1196
+ switch (req.resourceType) {
1197
+ case "Script":
1198
+ return `/scripts/${filename}`;
1199
+ case "Stylesheet":
1200
+ return `/styles/${filename}`;
1201
+ case "Font":
1202
+ return `/fonts/${filename}`;
1203
+ case "Document":
1204
+ return `/html/${filename}`;
1205
+ default:
1206
+ return `/other/${filename}`;
1207
+ }
1208
+ }
1209
+ function buildResourceBreakdown(requests) {
1210
+ const groups = {
1211
+ scripts: { count: 0, totalSize: 0 },
1212
+ stylesheets: { count: 0, totalSize: 0 },
1213
+ fonts: { count: 0, totalSize: 0 },
1214
+ images: { count: 0, totalSize: 0 },
1215
+ other: { count: 0, totalSize: 0 }
1216
+ };
1217
+ for (const r of requests) {
1218
+ const key = r.resourceType === "Script" ? "scripts" : r.resourceType === "Stylesheet" ? "stylesheets" : r.resourceType === "Font" ? "fonts" : r.resourceType === "Image" ? "images" : "other";
1219
+ groups[key].count++;
1220
+ groups[key].totalSize += r.decodedSize;
1221
+ }
1222
+ return groups;
1223
+ }
1224
+
1225
+ // src/output/terminal.ts
1226
+ import chalk from "chalk";
1227
+ import ora from "ora";
1228
+ var SEVERITY_ICONS = {
1229
+ critical: chalk.red("\uD83D\uDD34 CRITICAL"),
1230
+ warning: chalk.yellow("\uD83D\uDFE1 WARNING"),
1231
+ info: chalk.green("\uD83D\uDFE2 INFO")
1232
+ };
1233
+ var CATEGORY_LABELS = {
1234
+ "memory-leak": "Memory Leak",
1235
+ "large-retained-object": "Large Retained Object",
1236
+ "detached-dom": "Detached DOM",
1237
+ "render-blocking": "Render-Blocking",
1238
+ "long-task": "Long Task",
1239
+ "unused-code": "Unused Code",
1240
+ "waterfall-bottleneck": "Waterfall Bottleneck",
1241
+ "large-asset": "Large Asset",
1242
+ "frame-blocking-function": "Frame-Blocking Function",
1243
+ "listener-leak": "Listener Leak",
1244
+ "gc-pressure": "GC Pressure",
1245
+ "slow-test": "Slow Test",
1246
+ "expensive-setup": "Expensive Setup",
1247
+ "hot-function": "Hot Function",
1248
+ "unnecessary-computation": "Unnecessary Computation",
1249
+ "import-overhead": "Import Overhead",
1250
+ "dependency-bottleneck": "Dependency Bottleneck",
1251
+ algorithm: "Inefficient Algorithm",
1252
+ serialization: "Serialization Overhead",
1253
+ allocation: "Excessive Allocation",
1254
+ "event-handling": "Event Handling",
1255
+ "blocking-io": "Blocking I/O",
1256
+ other: "Other"
1257
+ };
1258
+ function printHeader(url, version) {
1259
+ const urlDisplay = url.length > 44 ? url.slice(0, 41) + "..." : url;
1260
+ console.log(chalk.cyan(`
1261
+ ┌${"─".repeat(57)}┐
1262
+ ` + `│ zeitzeuge v${version.padEnd(44)}│
1263
+ ` + `│ Analyzing: ${urlDisplay.padEnd(44)}│
1264
+ ` + `└${"─".repeat(57)}┘
1265
+ `));
1266
+ }
1267
+ function createSpinner(text) {
1268
+ return ora({ text, color: "cyan" }).start();
1269
+ }
1270
+ function printFindings(findings) {
1271
+ console.log(chalk.dim(`
1272
+ ` + "━".repeat(58) + `
1273
+ `));
1274
+ if (findings.length === 0) {
1275
+ console.log(chalk.green(` ✔ No significant performance issues found. Page looks healthy!
1276
+ `));
1277
+ console.log(chalk.dim("━".repeat(58)));
1278
+ return;
1279
+ }
1280
+ for (const finding of findings) {
1281
+ const icon = SEVERITY_ICONS[finding.severity];
1282
+ const categoryLabel = CATEGORY_LABELS[finding.category] ?? finding.category;
1283
+ console.log(`${icon} [${categoryLabel}]: ${chalk.bold(finding.title)}`);
1284
+ if (finding.retainedSize != null) {
1285
+ console.log(chalk.dim(` Retained size: ${formatBytes(finding.retainedSize)}`));
1286
+ }
1287
+ if (finding.impactMs != null) {
1288
+ console.log(chalk.dim(` Impact: ${finding.impactMs.toFixed(0)}ms`));
1289
+ }
1290
+ if (finding.resourceUrl) {
1291
+ console.log(chalk.dim(` Resource: ${finding.resourceUrl}`));
1292
+ }
1293
+ if (finding.retainerPath && finding.retainerPath.length > 0) {
1294
+ console.log(chalk.dim(` Path: ${finding.retainerPath.join(" → ")}`));
1295
+ }
1296
+ if (finding.testFile) {
1297
+ console.log(chalk.dim(` Test file: ${finding.testFile}`));
1298
+ }
1299
+ if (finding.hotFunction) {
1300
+ const hf = finding.hotFunction;
1301
+ console.log(chalk.dim(` Function: ${hf.name} at ${hf.scriptUrl}:${hf.lineNumber} (selfTime: ${hf.selfTime.toFixed(0)}ms, ${hf.selfPercent.toFixed(1)}%)`));
1302
+ }
1303
+ console.log(`
1304
+ ${finding.description}
1305
+ `);
1306
+ if (finding.suggestedFix) {
1307
+ console.log(chalk.dim(" Suggested fix:"));
1308
+ const lines = finding.suggestedFix.split(`
1309
+ `);
1310
+ const boxWidth = Math.max(...lines.map((l) => l.length), 20) + 4;
1311
+ console.log(chalk.dim(` ┌${"─".repeat(boxWidth)}┐`));
1312
+ for (const line of lines) {
1313
+ console.log(chalk.dim(" │ ") + chalk.white(line.padEnd(boxWidth - 2)) + chalk.dim(" │"));
1314
+ }
1315
+ console.log(chalk.dim(` └${"─".repeat(boxWidth)}┘`));
1316
+ }
1317
+ console.log();
1318
+ }
1319
+ console.log(chalk.dim("━".repeat(58)));
1320
+ const counts = {
1321
+ critical: findings.filter((f) => f.severity === "critical").length,
1322
+ warning: findings.filter((f) => f.severity === "warning").length,
1323
+ info: findings.filter((f) => f.severity === "info").length
1324
+ };
1325
+ console.log(`
1326
+ Summary: ${chalk.red(`${counts.critical} critical`)}, ` + `${chalk.yellow(`${counts.warning} warning`)}, ` + `${chalk.green(`${counts.info} info`)}
1327
+ `);
1328
+ }
1329
+ function printCaptureInfo(heapSummary, trace) {
1330
+ console.log(chalk.dim(`Heap: ${formatBytes(heapSummary.metadata.totalSize)} | ` + `Nodes: ${heapSummary.metadata.nodeCount.toLocaleString()} | ` + `Requests: ${trace.networkRequests.length} | ` + `Long tasks: ${trace.metrics.longTasks.length}`));
1331
+ }
1332
+ function printError(err) {
1333
+ const message = err instanceof Error ? err.message : String(err);
1334
+ console.error(chalk.red(`
1335
+ ✖ Error: ${message}
1336
+ `));
1337
+ }
1338
+ function formatBytes(bytes) {
1339
+ if (bytes < 1024)
1340
+ return `${bytes} B`;
1341
+ if (bytes < 1024 * 1024)
1342
+ return `${(bytes / 1024).toFixed(1)} KB`;
1343
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1344
+ }
1345
+
1346
+ // src/output/report.ts
1347
+ import { writeFileSync as writeFileSync2 } from "node:fs";
1348
+ var SEVERITY_EMOJI = {
1349
+ critical: "\uD83D\uDD34",
1350
+ warning: "\uD83D\uDFE1",
1351
+ info: "ℹ️"
1352
+ };
1353
+ var CATEGORY_LABELS2 = {
1354
+ "memory-leak": "Memory Leak",
1355
+ "large-retained-object": "Large Retained Object",
1356
+ "detached-dom": "Detached DOM",
1357
+ "render-blocking": "Render-Blocking",
1358
+ "long-task": "Long Task",
1359
+ "unused-code": "Unused Code",
1360
+ "waterfall-bottleneck": "Waterfall Bottleneck",
1361
+ "large-asset": "Large Asset",
1362
+ "frame-blocking-function": "Frame-Blocking Function",
1363
+ "listener-leak": "Listener Leak",
1364
+ "gc-pressure": "GC Pressure",
1365
+ "slow-test": "Slow Test",
1366
+ "expensive-setup": "Expensive Setup",
1367
+ "hot-function": "Hot Function",
1368
+ "unnecessary-computation": "Unnecessary Computation",
1369
+ "import-overhead": "Import Overhead",
1370
+ "dependency-bottleneck": "Dependency Bottleneck",
1371
+ algorithm: "Inefficient Algorithm",
1372
+ serialization: "Serialization Overhead",
1373
+ allocation: "Excessive Allocation",
1374
+ "event-handling": "Event Handling",
1375
+ "blocking-io": "Blocking I/O",
1376
+ other: "Other"
1377
+ };
1378
+ function writeReport(outputPath, options) {
1379
+ const md = generateMarkdown(options);
1380
+ writeFileSync2(outputPath, md, "utf-8");
1381
+ return outputPath;
1382
+ }
1383
+ function generateMarkdown(options) {
1384
+ const { url, version, findings, heapSummary, trace } = options;
1385
+ const now = new Date;
1386
+ const sections = [];
1387
+ sections.push(`# Performance Report`);
1388
+ sections.push("");
1389
+ sections.push(`> **${url}** — analyzed ${now.toISOString().replace("T", " ").slice(0, 16)} UTC by zeitzeuge v${version}`);
1390
+ sections.push("");
1391
+ const totalTransfer = trace.networkRequests.reduce((s, r) => s + r.encodedSize, 0);
1392
+ const loadSec = (trace.metrics.loadComplete / 1000).toFixed(1);
1393
+ const fcpSec = (trace.metrics.firstContentfulPaint / 1000).toFixed(2);
1394
+ const tbt = trace.metrics.totalBlockingTime.toFixed(0);
1395
+ const heapSize = formatBytes(heapSummary.metadata.totalSize);
1396
+ const reqCount = trace.networkRequests.length;
1397
+ sections.push(`**Page load** ${loadSec}s · **FCP** ${fcpSec}s · **TBT** ${tbt}ms · ` + `**Heap** ${heapSize} · **${reqCount} requests** (${formatBytes(totalTransfer)} transferred)`);
1398
+ sections.push("");
1399
+ const counts = {
1400
+ critical: findings.filter((f) => f.severity === "critical").length,
1401
+ warning: findings.filter((f) => f.severity === "warning").length,
1402
+ info: findings.filter((f) => f.severity === "info").length
1403
+ };
1404
+ if (findings.length === 0) {
1405
+ sections.push(`## ✅ No issues found`);
1406
+ sections.push("");
1407
+ sections.push(`No significant performance problems were detected. ` + `The page loads in ${loadSec}s with ${tbt}ms of total blocking time — looking healthy.`);
1408
+ sections.push("");
1409
+ } else {
1410
+ sections.push(`**${findings.length} issues found** — ` + `${counts.critical} critical, ${counts.warning} warning, ${counts.info} info`);
1411
+ sections.push("");
1412
+ for (let i = 0;i < findings.length; i++) {
1413
+ const f = findings[i];
1414
+ if (!f)
1415
+ continue;
1416
+ const emoji = SEVERITY_EMOJI[f.severity];
1417
+ const categoryLabel = CATEGORY_LABELS2[f.category] ?? f.category;
1418
+ sections.push(`---`);
1419
+ sections.push("");
1420
+ sections.push(`## ${emoji} ${f.title}`);
1421
+ sections.push("");
1422
+ const context = [`**${categoryLabel}**`];
1423
+ if (f.impactMs != null)
1424
+ context.push(`${f.impactMs.toFixed(0)}ms impact`);
1425
+ if (f.retainedSize != null)
1426
+ context.push(`${formatBytes(f.retainedSize)} retained`);
1427
+ if (f.resourceUrl)
1428
+ context.push(`\`${f.resourceUrl}\``);
1429
+ sections.push(context.join(" · "));
1430
+ sections.push("");
1431
+ sections.push(f.description);
1432
+ sections.push("");
1433
+ if (f.suggestedFix) {
1434
+ sections.push(`### How to fix`);
1435
+ sections.push("");
1436
+ const looksLikeCode = f.suggestedFix.includes("{") || f.suggestedFix.includes(";") || f.suggestedFix.includes("=>") || f.suggestedFix.includes("import ") || f.suggestedFix.includes("function ");
1437
+ if (looksLikeCode) {
1438
+ sections.push("```js");
1439
+ sections.push(f.suggestedFix);
1440
+ sections.push("```");
1441
+ } else {
1442
+ sections.push(f.suggestedFix);
1443
+ }
1444
+ sections.push("");
1445
+ }
1446
+ if (f.retainerPath && f.retainerPath.length > 0) {
1447
+ sections.push(`*Retention path:* ${f.retainerPath.map((p) => `\`${p}\``).join(" → ")}`);
1448
+ sections.push("");
1449
+ }
1450
+ }
1451
+ }
1452
+ sections.push(`---`);
1453
+ sections.push("");
1454
+ sections.push(`*Generated by zeitzeuge v${version}*`);
1455
+ sections.push("");
1456
+ return sections.join(`
1457
+ `);
1458
+ }
1459
+ function writeTestReport(outputPath, options) {
1460
+ const md = generateTestMarkdown(options);
1461
+ writeFileSync2(outputPath, md, "utf-8");
1462
+ return outputPath;
1463
+ }
1464
+ function generateTestMarkdown(options) {
1465
+ const { version, findings, testTiming, profiles } = options;
1466
+ const now = new Date;
1467
+ const sections = [];
1468
+ sections.push(`# Vitest Performance Report`);
1469
+ sections.push("");
1470
+ sections.push(`> Analyzed ${now.toISOString().replace("T", " ").slice(0, 16)} UTC by zeitzeuge v${version}`);
1471
+ sections.push("");
1472
+ const totalTests = testTiming.reduce((s, t) => s + t.testCount, 0);
1473
+ const totalFiles = testTiming.length;
1474
+ const totalDuration = testTiming.reduce((s, t) => s + t.duration, 0);
1475
+ const slowest = testTiming.length > 0 ? testTiming.reduce((a, b) => a.duration > b.duration ? a : b) : null;
1476
+ const totalGcTime = profiles.reduce((s, p) => s + p.summary.duration * p.summary.gcPercentage / 100, 0);
1477
+ const gcPercentage = totalDuration > 0 ? (totalGcTime / totalDuration * 100).toFixed(2) : "0";
1478
+ sections.push(`**Test run** ${totalTests} tests across ${totalFiles} files · ` + `**Total duration** ${(totalDuration / 1000).toFixed(2)}s · ` + `**Slowest file** ${slowest ? `${slowest.file} (${(slowest.duration / 1000).toFixed(2)}s)` : "—"} · ` + `**GC overhead** ${gcPercentage}% (${totalGcTime.toFixed(0)}ms)`);
1479
+ sections.push("");
1480
+ const counts = {
1481
+ critical: findings.filter((f) => f.severity === "critical").length,
1482
+ warning: findings.filter((f) => f.severity === "warning").length,
1483
+ info: findings.filter((f) => f.severity === "info").length
1484
+ };
1485
+ if (findings.length === 0) {
1486
+ sections.push(`## ✅ No issues found`);
1487
+ sections.push("");
1488
+ sections.push(`No significant performance problems were detected. ` + `Tests complete in ${(totalDuration / 1000).toFixed(2)}s — looking healthy.`);
1489
+ sections.push("");
1490
+ } else {
1491
+ sections.push(`**${findings.length} issues found** — ` + `${counts.critical} critical, ${counts.warning} warning, ${counts.info} info`);
1492
+ sections.push("");
1493
+ for (const f of findings) {
1494
+ const emoji = SEVERITY_EMOJI[f.severity];
1495
+ const categoryLabel = CATEGORY_LABELS2[f.category] ?? f.category;
1496
+ sections.push(`---`);
1497
+ sections.push("");
1498
+ sections.push(`## ${emoji} ${f.title}`);
1499
+ sections.push("");
1500
+ const context = [`**${categoryLabel}**`];
1501
+ if (f.impactMs != null)
1502
+ context.push(`${f.impactMs.toFixed(0)}ms impact`);
1503
+ if (f.testFile)
1504
+ context.push(`\`${f.testFile}\``);
1505
+ if (f.hotFunction) {
1506
+ context.push(`\`${f.hotFunction.name}\` (${f.hotFunction.selfTime.toFixed(0)}ms, ${f.hotFunction.selfPercent.toFixed(1)}%)`);
1507
+ }
1508
+ if (f.resourceUrl)
1509
+ context.push(`\`${f.resourceUrl}\``);
1510
+ sections.push(context.join(" · "));
1511
+ sections.push("");
1512
+ sections.push(f.description);
1513
+ sections.push("");
1514
+ if (f.suggestedFix) {
1515
+ sections.push(`### How to fix`);
1516
+ sections.push("");
1517
+ const looksLikeCode = f.suggestedFix.includes("{") || f.suggestedFix.includes(";") || f.suggestedFix.includes("=>") || f.suggestedFix.includes("import ") || f.suggestedFix.includes("function ");
1518
+ if (looksLikeCode) {
1519
+ sections.push("```ts");
1520
+ sections.push(f.suggestedFix);
1521
+ sections.push("```");
1522
+ } else {
1523
+ sections.push(f.suggestedFix);
1524
+ }
1525
+ sections.push("");
1526
+ }
1527
+ }
1528
+ }
1529
+ sections.push(`---`);
1530
+ sections.push("");
1531
+ sections.push(`*Generated by zeitzeuge v${version}*`);
1532
+ sections.push("");
1533
+ return sections.join(`
1534
+ `);
1535
+ }
1536
+
1537
+ // src/cli.ts
1538
+ import { readFileSync } from "node:fs";
1539
+ import { resolve, dirname } from "node:path";
1540
+ import { fileURLToPath } from "node:url";
1541
+ function getVersion() {
1542
+ try {
1543
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
1544
+ const pkgPath = resolve(__dirname2, "..", "package.json");
1545
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1546
+ return pkg.version ?? "0.3.0";
1547
+ } catch {
1548
+ return "0.3.0";
1549
+ }
1550
+ }
1551
+ var VERSION = getVersion();
1552
+ var argv = yargs(hideBin(process.argv)).scriptName("zeitzeuge").usage("Usage: $0 <url> [options]").command("$0 <url>", "Analyze frontend performance of a URL", (yargs2) => {
1553
+ return yargs2.positional("url", {
1554
+ describe: "Target URL to analyze (e.g. http://localhost:3000)",
1555
+ type: "string",
1556
+ demandOption: true
1557
+ });
1558
+ }).option("verbose", {
1559
+ alias: "v",
1560
+ type: "boolean",
1561
+ default: false,
1562
+ describe: "Enable verbose/debug logging"
1563
+ }).option("headless", {
1564
+ type: "boolean",
1565
+ default: true,
1566
+ describe: "Run Chrome in headless mode"
1567
+ }).option("timeout", {
1568
+ type: "number",
1569
+ default: 30000,
1570
+ describe: "Page load timeout in milliseconds"
1571
+ }).option("output", {
1572
+ alias: "o",
1573
+ type: "string",
1574
+ default: "zeitzeuge-report.md",
1575
+ describe: "Output path for the Markdown report"
1576
+ }).help("help", "Show help").alias("h", "help").version(VERSION).strict().parseSync();
1577
+ function validateUrl(url) {
1578
+ try {
1579
+ const parsed = new URL(url);
1580
+ if (!["http:", "https:"].includes(parsed.protocol)) {
1581
+ throw new Error("URL must use http:// or https:// protocol");
1582
+ }
1583
+ } catch (err) {
1584
+ if (err instanceof Error && err.message.includes("protocol")) {
1585
+ throw err;
1586
+ }
1587
+ throw new Error(`Invalid URL: "${url}". Please provide a valid URL (e.g. http://localhost:3000)`);
1588
+ }
1589
+ }
1590
+ async function main() {
1591
+ const url = argv.url;
1592
+ const verbose = argv.verbose;
1593
+ let browser;
1594
+ try {
1595
+ validateUrl(url);
1596
+ printHeader(url, VERSION);
1597
+ if (verbose)
1598
+ console.log("[verbose] Detecting API key and initializing model...");
1599
+ const model = initModel();
1600
+ if (verbose)
1601
+ console.log(`[verbose] Model initialized: ${model.constructor.name}`);
1602
+ const browserSpinner = createSpinner("Launching browser...");
1603
+ try {
1604
+ browser = activeBrowser = await launchBrowser({ headless: argv.headless });
1605
+ browserSpinner.succeed(`Browser launched (${argv.headless ? "headless" : "headed"})`);
1606
+ } catch (err) {
1607
+ browserSpinner.fail("Failed to launch browser");
1608
+ throw new Error(`Could not launch Chrome. Make sure Chrome/Chromium is installed.
1609
+ ` + ` Install: https://www.google.com/chrome/
1610
+ ` + (err instanceof Error ? ` Details: ${err.message}` : ""));
1611
+ }
1612
+ const captureSpinner = createSpinner(`Loading ${url} & capturing data...`);
1613
+ let captureResult;
1614
+ try {
1615
+ captureResult = await capturePage(browser, url, {
1616
+ timeout: argv.timeout
1617
+ });
1618
+ const heapSizeMB = (captureResult.heapSnapshot.data.length / (1024 * 1024)).toFixed(1);
1619
+ const reqCount = captureResult.trace.networkRequests.length;
1620
+ const longTaskCount = captureResult.trace.metrics.longTasks.length;
1621
+ const runtimeTraceInfo = captureResult.trace.runtimeTrace ? `
1622
+ Runtime trace: ${captureResult.trace.runtimeTrace.totalEvents.toLocaleString()} events captured` : "";
1623
+ captureSpinner.succeed(`Page loaded in ${(captureResult.trace.metrics.loadComplete / 1000).toFixed(1)}s
1624
+ ` + ` Heap snapshot: ${heapSizeMB} MB
1625
+ ` + ` Network requests: ${reqCount} captured
1626
+ ` + ` Long tasks: ${longTaskCount} detected` + runtimeTraceInfo);
1627
+ } catch (err) {
1628
+ captureSpinner.fail("Failed to capture page data");
1629
+ throw new Error(`Failed to capture data from ${url}.
1630
+ ` + ` Try running with --no-headless if the page requires interaction.
1631
+ ` + (err instanceof Error ? ` Details: ${err.message}` : ""));
1632
+ }
1633
+ const parseSpinner = createSpinner("Parsing heap snapshot...");
1634
+ const heapSummary = parseSnapshot(captureResult.heapSnapshot);
1635
+ parseSpinner.succeed(`Parsed: ${heapSummary.metadata.nodeCount.toLocaleString()} nodes, ${heapSummary.metadata.edgeCount.toLocaleString()} edges`);
1636
+ const workspaceSpinner = createSpinner("Building workspace...");
1637
+ let workspace;
1638
+ try {
1639
+ workspace = await createWorkspace({
1640
+ heapSummary,
1641
+ traceResult: captureResult.trace,
1642
+ url
1643
+ });
1644
+ const storedCount = captureResult.trace.networkRequests.filter((r) => r.responseBody).length;
1645
+ const totalSize = captureResult.trace.networkRequests.filter((r) => r.responseBody).reduce((sum, r) => sum + (r.responseBody?.length ?? 0), 0);
1646
+ const runtimeWorkspaceInfo = captureResult.trace.runtimeTrace ? `
1647
+ Runtime trace: summaries + raw events` : "";
1648
+ workspaceSpinner.succeed(`${storedCount} assets stored in workspace (${formatBytes(totalSize)} total)` + runtimeWorkspaceInfo);
1649
+ } catch (err) {
1650
+ workspaceSpinner.fail("Failed to build workspace");
1651
+ throw new Error(`Failed to create workspace.
1652
+ ` + (err instanceof Error ? ` Details: ${err.message}` : ""));
1653
+ }
1654
+ const agentSpinner = createSpinner("Deep Agent analyzing...");
1655
+ let findings;
1656
+ try {
1657
+ findings = await analyze(model, workspace.backend);
1658
+ agentSpinner.succeed(`Analysis complete — ${findings.length} findings`);
1659
+ } catch (err) {
1660
+ agentSpinner.fail(`Analysis failed: ${err instanceof Error ? err.message : "Unknown error"}`);
1661
+ throw new Error(`LLM analysis failed. Check your API key and network connection.
1662
+ ` + (err instanceof Error ? ` Details: ${err.message}` : ""));
1663
+ } finally {
1664
+ workspace.cleanup();
1665
+ }
1666
+ printFindings(findings);
1667
+ printCaptureInfo(heapSummary, captureResult.trace);
1668
+ const outputPath = argv.output;
1669
+ const reportPath = writeReport(resolve(outputPath), {
1670
+ url,
1671
+ version: VERSION,
1672
+ findings,
1673
+ heapSummary,
1674
+ trace: captureResult.trace
1675
+ });
1676
+ console.log(`
1677
+ \uD83D\uDCC4 Report written to ${reportPath}
1678
+ `);
1679
+ } catch (err) {
1680
+ printError(err);
1681
+ process.exit(1);
1682
+ } finally {
1683
+ if (browser) {
1684
+ await closeBrowser(browser);
1685
+ }
1686
+ }
1687
+ }
1688
+ var activeBrowser;
1689
+ process.on("SIGINT", async () => {
1690
+ if (activeBrowser) {
1691
+ await closeBrowser(activeBrowser);
1692
+ }
1693
+ process.exit(130);
1694
+ });
1695
+ main();