aipeek 0.2.1 → 0.2.2

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.
@@ -0,0 +1,1199 @@
1
+ // src/server/plugin.ts
2
+ import { Buffer } from "buffer";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { dirname, resolve } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { transformSync } from "esbuild";
7
+
8
+ // src/core/action.ts
9
+ var TYPES = ["click", "fill", "press", "wait", "screenshot"];
10
+ function resolveAction(type, args) {
11
+ if (!TYPES.includes(type))
12
+ return { valid: false, error: `unknown action: ${type}` };
13
+ const hasTarget = !!(args.sel || args.text);
14
+ switch (type) {
15
+ case "click":
16
+ return hasTarget ? { valid: true } : { valid: false, error: "click needs sel= or text=" };
17
+ case "fill":
18
+ if (!hasTarget)
19
+ return { valid: false, error: "fill needs sel= or text=" };
20
+ if (args.value === void 0)
21
+ return { valid: false, error: "fill needs value=" };
22
+ return { valid: true };
23
+ case "press":
24
+ return args.key ? { valid: true } : { valid: false, error: "press needs key=" };
25
+ case "wait":
26
+ return hasTarget ? { valid: true } : { valid: false, error: "wait needs sel= or text=" };
27
+ case "screenshot":
28
+ return { valid: true };
29
+ default:
30
+ return { valid: false, error: `unknown action: ${type}` };
31
+ }
32
+ }
33
+
34
+ // src/core/check.ts
35
+ function check(raw) {
36
+ const consoleErrors = raw.console.filter((l) => l.level === "error");
37
+ const failedRequests = raw.network.filter((r) => r.status >= 400 || r.failed);
38
+ const assertions = [
39
+ {
40
+ name: "no-console-errors",
41
+ pass: consoleErrors.length === 0,
42
+ ...consoleErrors.length && { detail: consoleErrors[0].text }
43
+ },
44
+ {
45
+ name: "no-uncaught-errors",
46
+ pass: raw.errors.length === 0,
47
+ ...raw.errors.length && { detail: raw.errors[0].message }
48
+ },
49
+ {
50
+ name: "no-failed-requests",
51
+ pass: failedRequests.length === 0,
52
+ ...failedRequests.length && { detail: `${failedRequests[0].method} ${failedRequests[0].url} ${failedRequests[0].status}` }
53
+ },
54
+ {
55
+ name: "ui-not-empty",
56
+ pass: raw.ui.trim().length > 0,
57
+ ...!raw.ui.trim().length && { detail: "UI tree is empty" }
58
+ }
59
+ ];
60
+ return {
61
+ pass: assertions.every((a) => a.pass),
62
+ assertions
63
+ };
64
+ }
65
+
66
+ // src/core/util.ts
67
+ function truncate(s, max) {
68
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
69
+ }
70
+ function compactUrl(url, search) {
71
+ try {
72
+ const u = new URL(url);
73
+ if (search && u.search)
74
+ return `${u.pathname}?${truncate(u.search.slice(1), search)}`;
75
+ return u.pathname;
76
+ } catch {
77
+ return truncate(url, 80);
78
+ }
79
+ }
80
+
81
+ // src/core/compact.ts
82
+ var SLOW_THRESHOLD = 1e3;
83
+ var MAX_UI_DEPTH = 6;
84
+ var UI_PRIMITIVES = /* @__PURE__ */ new Set([
85
+ "Button",
86
+ "Input",
87
+ "Label",
88
+ "Badge",
89
+ "Checkbox",
90
+ "Skeleton",
91
+ "Spinner",
92
+ "Switch",
93
+ "Tabs",
94
+ "Tooltip",
95
+ "Popover",
96
+ "Dialog",
97
+ "Select",
98
+ "Card",
99
+ "Table",
100
+ "Slider",
101
+ "Progress",
102
+ "RadioGroup",
103
+ "HoverCard",
104
+ "DropdownMenu",
105
+ "ContextMenu",
106
+ "Command",
107
+ "Form",
108
+ "Alert",
109
+ "Pagination",
110
+ "Textarea",
111
+ "TooltipProvider",
112
+ "DialogPortal",
113
+ "Router",
114
+ "RenderErrorBoundary",
115
+ "RouterProvider",
116
+ "RouterProvider2",
117
+ "PanelGroup",
118
+ "Panel"
119
+ ]);
120
+ function nameOf(line) {
121
+ let end = 0;
122
+ while (end < line.length) {
123
+ const c = line[end];
124
+ if (c === " " || c === " " || c === "[" || c === "\u2014")
125
+ break;
126
+ end++;
127
+ }
128
+ return line.slice(0, end);
129
+ }
130
+ function compactUI(tree) {
131
+ if (!tree)
132
+ return "";
133
+ const lines = tree.split("\n");
134
+ const result = [];
135
+ const repeatTracker = /* @__PURE__ */ new Map();
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const line = lines[i];
138
+ const trimmed = line.trimStart();
139
+ if (!trimmed)
140
+ continue;
141
+ const indent = line.length - trimmed.length;
142
+ const depth = Math.floor(indent / 2);
143
+ if (depth > MAX_UI_DEPTH)
144
+ continue;
145
+ const componentName = nameOf(trimmed);
146
+ if (UI_PRIMITIVES.has(componentName))
147
+ continue;
148
+ const key = `${depth}:${componentName}`;
149
+ const tracker = repeatTracker.get(key);
150
+ if (tracker && i - tracker.lastIndex <= 2) {
151
+ tracker.count++;
152
+ tracker.lastIndex = i;
153
+ continue;
154
+ }
155
+ for (const [k, t] of repeatTracker) {
156
+ if (t.count > 1) {
157
+ const d = Number.parseInt(k.split(":")[0]);
158
+ const name = k.split(":").slice(1).join(":");
159
+ result.push(`${" ".repeat(d)}${name} \xD7${t.count}`);
160
+ }
161
+ if (t.count > 1 || i - t.lastIndex > 2) {
162
+ repeatTracker.delete(k);
163
+ }
164
+ }
165
+ repeatTracker.set(key, { count: 1, lastIndex: i });
166
+ result.push(line);
167
+ }
168
+ for (const [k, t] of repeatTracker) {
169
+ if (t.count > 1) {
170
+ const d = Number.parseInt(k.split(":")[0]);
171
+ const name = k.split(":").slice(1).join(":");
172
+ result.push(`${" ".repeat(d)}${name} \xD7${t.count}`);
173
+ }
174
+ }
175
+ return result.join("\n");
176
+ }
177
+ var NOISE_SUBSTRINGS = [
178
+ "[hmr]",
179
+ "[vite]",
180
+ "hot module",
181
+ "react-devtools",
182
+ "download the react devtools",
183
+ "warning: react does not recognize",
184
+ "source map",
185
+ "favicon.ico",
186
+ "webpack"
187
+ ];
188
+ function compactConsole(logs) {
189
+ if (!logs.length)
190
+ return "";
191
+ const filtered = logs.filter((l) => {
192
+ const lower = l.text.toLowerCase();
193
+ return !NOISE_SUBSTRINGS.some((s) => lower.includes(s));
194
+ });
195
+ if (!filtered.length)
196
+ return "";
197
+ const deduped = [];
198
+ for (const log of filtered) {
199
+ const last = deduped[deduped.length - 1];
200
+ if (last && last.entry.text === log.text && last.entry.level === log.level) {
201
+ last.count++;
202
+ } else {
203
+ deduped.push({ entry: log, count: 1 });
204
+ }
205
+ }
206
+ const errors = deduped.filter((d) => d.entry.level === "error");
207
+ const warns = deduped.filter((d) => d.entry.level === "warn");
208
+ const rest = deduped.filter((d) => d.entry.level !== "error" && d.entry.level !== "warn");
209
+ const recentRest = rest.slice(-10);
210
+ const lines = [];
211
+ for (const group of [...errors, ...warns, ...recentRest]) {
212
+ const prefix = `[${group.entry.level}]`;
213
+ const count = group.count > 1 ? ` \xD7${group.count}` : "";
214
+ const source = group.entry.source ? ` (${group.entry.source})` : "";
215
+ const text = truncate(group.entry.text, 200);
216
+ lines.push(`${prefix}${count} ${text}${source}`);
217
+ }
218
+ return lines.join("\n");
219
+ }
220
+ function compactNetwork(requests) {
221
+ if (!requests.length)
222
+ return "";
223
+ const relevant = requests.filter(
224
+ (r) => r.resourceType === "fetch" || r.resourceType === "xhr" || r.resourceType === "websocket" || r.resourceType === "eventsource" || isApiUrl(r.url)
225
+ );
226
+ if (!relevant.length)
227
+ return "";
228
+ const lines = [];
229
+ for (const req of relevant) {
230
+ const duration = req.duration > 0 ? ` ${formatDuration(req.duration)}` : "";
231
+ const slow = req.duration >= SLOW_THRESHOLD ? " [SLOW]" : "";
232
+ const url = compactUrl(req.url, 50);
233
+ const headers = diagnosticHeaders(req);
234
+ if (req.failed || req.status >= 400) {
235
+ const body = req.responseBody ? ` "${truncate(req.responseBody, 100)}"` : "";
236
+ const failure = req.failureText ? ` (${req.failureText})` : "";
237
+ lines.push(`${req.method} ${url} ${req.status}${failure}${body}${headers}${duration}${slow}`);
238
+ } else {
239
+ lines.push(`${req.method} ${url} ${req.status}${headers}${duration}${slow}`);
240
+ }
241
+ }
242
+ return lines.join("\n");
243
+ }
244
+ function isApiUrl(url) {
245
+ try {
246
+ const u = new URL(url);
247
+ return u.pathname.startsWith("/api") || u.pathname.includes("/graphql");
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+ var DIAGNOSTIC_HEADERS = ["content-type", "x-error", "www-authenticate", "access-control-allow-origin"];
253
+ function diagnosticHeaders(req) {
254
+ const h = req.responseHeaders;
255
+ if (!h)
256
+ return "";
257
+ const parts = [];
258
+ for (const key of DIAGNOSTIC_HEADERS) {
259
+ const val = h[key];
260
+ if (!val)
261
+ continue;
262
+ if (key === "content-type" && req.status < 400 && val.includes("application/json"))
263
+ continue;
264
+ parts.push(`${key}: ${truncate(val, 60)}`);
265
+ }
266
+ if (!parts.length)
267
+ return "";
268
+ return ` [${parts.join(", ")}]`;
269
+ }
270
+ function formatDuration(ms) {
271
+ return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
272
+ }
273
+ function compactErrors(errors) {
274
+ if (!errors.length)
275
+ return "";
276
+ const seen = /* @__PURE__ */ new Map();
277
+ for (const err of errors) {
278
+ if (!seen.has(err.message)) {
279
+ seen.set(err.message, err);
280
+ }
281
+ }
282
+ const lines = [];
283
+ for (const err of seen.values()) {
284
+ lines.push(err.message);
285
+ if (err.stack) {
286
+ const frames = filterStack(err.stack);
287
+ for (const frame of frames) {
288
+ lines.push(` at ${frame}`);
289
+ }
290
+ }
291
+ }
292
+ return lines.join("\n");
293
+ }
294
+ function filterStack(stack) {
295
+ return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).map((l) => l.slice(3)).filter((l) => !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, 5);
296
+ }
297
+ function compactState(state) {
298
+ if (!state || !Object.keys(state).length)
299
+ return "";
300
+ const lines = [];
301
+ for (const [name, value] of Object.entries(state)) {
302
+ lines.push(`${name}:`);
303
+ if (typeof value === "object" && value !== null) {
304
+ for (const [k, v] of Object.entries(value)) {
305
+ lines.push(` ${k}: ${formatValue(v)}`);
306
+ }
307
+ } else {
308
+ lines.push(` ${String(value)}`);
309
+ }
310
+ }
311
+ return lines.join("\n");
312
+ }
313
+ function formatValue(v) {
314
+ if (v === null || v === void 0)
315
+ return String(v);
316
+ if (typeof v === "string")
317
+ return v;
318
+ if (typeof v === "number" || typeof v === "boolean")
319
+ return String(v);
320
+ if (typeof v === "object") {
321
+ const s = JSON.stringify(v);
322
+ return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
323
+ }
324
+ return String(v);
325
+ }
326
+ function compact(raw) {
327
+ return {
328
+ url: raw.url,
329
+ ui: compactUI(raw.ui),
330
+ console: compactConsole(raw.console),
331
+ network: compactNetwork(raw.network),
332
+ errors: compactErrors(raw.errors),
333
+ state: compactState(raw.state),
334
+ timestamp: raw.timestamp,
335
+ counts: {
336
+ console: raw.console.length,
337
+ network: raw.network.length,
338
+ errors: raw.errors.length,
339
+ state: Object.keys(raw.state).length
340
+ }
341
+ };
342
+ }
343
+
344
+ // src/core/detail.ts
345
+ function detail(raw, section, index, full) {
346
+ switch (section) {
347
+ case "ui":
348
+ return full ? raw.ui || null : compactUI(raw.ui) || null;
349
+ case "console":
350
+ return detailConsole(raw.console, index, full);
351
+ case "network":
352
+ return detailNetwork(raw.network, index, full);
353
+ case "errors":
354
+ return detailError(raw.errors, index, full);
355
+ case "state":
356
+ return detailState(raw.state, index, full);
357
+ default:
358
+ return null;
359
+ }
360
+ }
361
+ function detailConsole(logs, index, full) {
362
+ if (index === void 0) {
363
+ if (!logs.length)
364
+ return "(empty)";
365
+ return logs.map((log2, i2) => `[${i2}] [${log2.level}] ${truncate(log2.text, 120)}`).join("\n");
366
+ }
367
+ const i = Number.parseInt(index);
368
+ if (Number.isNaN(i) || i < 0 || i >= logs.length)
369
+ return null;
370
+ const log = logs[i];
371
+ if (full) {
372
+ const parts = [`[${log.level}] ${log.text}`];
373
+ if (log.timestamp)
374
+ parts.push(`timestamp: ${new Date(log.timestamp).toISOString()}`);
375
+ if (log.source)
376
+ parts.push(`source: ${log.source}`);
377
+ return parts.join("\n");
378
+ }
379
+ return `[${log.level}] ${truncate(log.text, 200)}`;
380
+ }
381
+ function detailNetwork(requests, index, full) {
382
+ if (index === void 0) {
383
+ if (!requests.length)
384
+ return "(empty)";
385
+ return requests.map((req2, i2) => `[${i2}] ${req2.method} ${req2.status} ${truncate(req2.url, 80)} ${req2.duration}ms${req2.failed ? " FAILED" : ""}`).join("\n");
386
+ }
387
+ const i = Number.parseInt(index);
388
+ if (Number.isNaN(i) || i < 0 || i >= requests.length)
389
+ return null;
390
+ const req = requests[i];
391
+ const lines = [
392
+ `${req.method} ${req.url}`,
393
+ `status: ${req.status}`,
394
+ `duration: ${req.duration}ms`,
395
+ `type: ${req.resourceType}`
396
+ ];
397
+ if (req.failed)
398
+ lines.push(`failed: ${req.failureText || "true"}`);
399
+ if (full) {
400
+ if (req.requestHeaders && Object.keys(req.requestHeaders).length) {
401
+ lines.push("request-headers:");
402
+ for (const [k, v] of Object.entries(req.requestHeaders)) lines.push(` ${k}: ${v}`);
403
+ }
404
+ if (req.requestBody)
405
+ lines.push(`request-body:
406
+ ${req.requestBody}`);
407
+ if (req.responseHeaders && Object.keys(req.responseHeaders).length) {
408
+ lines.push("response-headers:");
409
+ for (const [k, v] of Object.entries(req.responseHeaders)) lines.push(` ${k}: ${v}`);
410
+ }
411
+ if (req.responseBody)
412
+ lines.push(`response-body:
413
+ ${req.responseBody}`);
414
+ } else {
415
+ if (req.requestBody) {
416
+ lines.push(`request-body: ${byteSize(req.requestBody)}`);
417
+ if (req.requestSample) {
418
+ const schema = jsonSchema(req.requestSample);
419
+ if (schema)
420
+ lines.push(schema);
421
+ }
422
+ }
423
+ if (req.responseBody) {
424
+ if (req.status >= 400) {
425
+ lines.push(`response-body: ${byteSize(req.responseBody)} "${truncate(req.responseBody, 100)}"`);
426
+ } else {
427
+ if (req.responseSample) {
428
+ lines.push(`response-body: ${byteSize(req.responseBody)}`);
429
+ const schema = jsonSchema(req.responseSample);
430
+ if (schema)
431
+ lines.push(schema);
432
+ } else {
433
+ lines.push(`response-body: ${byteSize(req.responseBody)} ${truncate(req.responseBody, 100)}`);
434
+ }
435
+ }
436
+ }
437
+ }
438
+ return lines.join("\n");
439
+ }
440
+ function detailError(errors, index, full) {
441
+ if (index === void 0) {
442
+ if (!errors.length)
443
+ return "(empty)";
444
+ return errors.map((err2, i2) => `[${i2}] ${truncate(err2.message, 120)}`).join("\n");
445
+ }
446
+ const i = Number.parseInt(index);
447
+ if (Number.isNaN(i) || i < 0 || i >= errors.length)
448
+ return null;
449
+ const err = errors[i];
450
+ if (full) {
451
+ const lines2 = [err.message];
452
+ if (err.stack)
453
+ lines2.push(err.stack);
454
+ if (err.source)
455
+ lines2.push(`source: ${err.source}`);
456
+ if (err.line != null)
457
+ lines2.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
458
+ return lines2.join("\n");
459
+ }
460
+ const lines = [err.message];
461
+ if (err.stack) {
462
+ const appFrames = err.stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules")).slice(0, 3);
463
+ if (appFrames.length)
464
+ lines.push(...appFrames);
465
+ const totalApp = err.stack.split("\n").filter((l) => l.trim().startsWith("at ") && !l.includes("node_modules")).length;
466
+ if (totalApp > 3)
467
+ lines.push(` ... ${totalApp - 3} more app frames`);
468
+ }
469
+ if (err.line != null)
470
+ lines.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
471
+ return lines.join("\n");
472
+ }
473
+ function detailState(state, name, full) {
474
+ if (!name) {
475
+ const keys = Object.keys(state);
476
+ if (!keys.length)
477
+ return "(empty)";
478
+ return keys.map((k) => `${k}: ${formatSummaryValue(state[k])}`).join("\n");
479
+ }
480
+ if (!(name in state))
481
+ return null;
482
+ const value = state[name];
483
+ if (full) {
484
+ try {
485
+ return JSON.stringify(value, null, 2);
486
+ } catch {
487
+ return String(value);
488
+ }
489
+ }
490
+ if (typeof value !== "object" || value === null)
491
+ return `${name}: ${typeof value}`;
492
+ const lines = [];
493
+ for (const [k, v] of Object.entries(value)) {
494
+ lines.push(`${k}: ${formatSummaryValue(v)}`);
495
+ }
496
+ return lines.join("\n");
497
+ }
498
+ function isArraySentinel(v) {
499
+ if (!v.startsWith("Array(") || !v.endsWith(")"))
500
+ return false;
501
+ const digits = v.slice(6, -1);
502
+ return digits.length > 0 && [...digits].every((c) => c >= "0" && c <= "9");
503
+ }
504
+ function formatSummaryValue(v) {
505
+ if (v === null || v === void 0)
506
+ return String(v);
507
+ if (typeof v === "string") {
508
+ if (isArraySentinel(v))
509
+ return v;
510
+ return v.length > 80 ? `${v.slice(0, 80)}\u2026` : v;
511
+ }
512
+ if (typeof v === "number" || typeof v === "boolean")
513
+ return String(v);
514
+ if (typeof v === "object") {
515
+ const s = JSON.stringify(v);
516
+ return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
517
+ }
518
+ return String(v);
519
+ }
520
+ function jsonSchema(sample) {
521
+ try {
522
+ return schemaOf(JSON.parse(sample), 0);
523
+ } catch {
524
+ return null;
525
+ }
526
+ }
527
+ function schemaOf(v, d) {
528
+ if (v === null)
529
+ return "null";
530
+ if (typeof v === "string")
531
+ return "string";
532
+ if (typeof v === "number")
533
+ return "number";
534
+ if (typeof v === "boolean")
535
+ return "boolean";
536
+ if (Array.isArray(v)) {
537
+ if (!v.length)
538
+ return "[]";
539
+ if (d >= 3)
540
+ return "[\u2026]";
541
+ return `${schemaOf(v[0], d + 1)}[]`;
542
+ }
543
+ if (typeof v === "object") {
544
+ if (d >= 3)
545
+ return "{\u2026}";
546
+ const entries = Object.entries(v);
547
+ if (!entries.length)
548
+ return "{}";
549
+ const max = d === 0 ? 12 : 6;
550
+ const fields = entries.slice(0, max).map(([k, val]) => `${k}: ${schemaOf(val, d + 1)}`);
551
+ if (entries.length > max)
552
+ fields.push(`\u2026 ${entries.length - max} more`);
553
+ return `{ ${fields.join(", ")} }`;
554
+ }
555
+ return typeof v;
556
+ }
557
+ function byteSize(s) {
558
+ const bytes = new TextEncoder().encode(s).length;
559
+ if (bytes < 1024)
560
+ return `${bytes}B`;
561
+ if (bytes < 1024 * 1024)
562
+ return `${(bytes / 1024).toFixed(1)}KB`;
563
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
564
+ }
565
+
566
+ // src/core/diff.ts
567
+ function diffState(prev, curr) {
568
+ if (!prev) {
569
+ return {
570
+ newErrors: curr.console.filter((l) => l.level === "error"),
571
+ newExceptions: [...curr.errors],
572
+ newFailedRequests: curr.network.filter((r) => r.status >= 400 || r.failed),
573
+ uiGone: false,
574
+ clean: curr.console.filter((l) => l.level === "error").length === 0 && curr.errors.length === 0 && curr.network.filter((r) => r.status >= 400 || r.failed).length === 0
575
+ };
576
+ }
577
+ const prevErrorTexts = new Set(prev.console.filter((l) => l.level === "error").map((l) => l.text));
578
+ const newErrors = curr.console.filter((l) => l.level === "error" && !prevErrorTexts.has(l.text));
579
+ const prevExceptionMsgs = new Set(prev.errors.map((e) => e.message));
580
+ const newExceptions = curr.errors.filter((e) => !prevExceptionMsgs.has(e.message));
581
+ const prevFailedKeys = new Set(
582
+ prev.network.filter((r) => r.status >= 400 || r.failed).map((r) => `${r.method}|${r.url}|${r.status}`)
583
+ );
584
+ const newFailedRequests = curr.network.filter((r) => r.status >= 400 || r.failed).filter((r) => !prevFailedKeys.has(`${r.method}|${r.url}|${r.status}`));
585
+ const uiGone = prev.ui.trim().length > 0 && curr.ui.trim().length === 0;
586
+ return {
587
+ newErrors,
588
+ newExceptions,
589
+ newFailedRequests,
590
+ uiGone,
591
+ clean: newErrors.length === 0 && newExceptions.length === 0 && newFailedRequests.length === 0 && !uiGone
592
+ };
593
+ }
594
+
595
+ // src/core/emit.ts
596
+ import pc from "picocolors";
597
+ var SECTIONS = ["ui", "console", "network", "errors", "state"];
598
+ var COUNTED_SECTIONS = {
599
+ console: "console",
600
+ network: "network",
601
+ errors: "errors",
602
+ state: "state"
603
+ };
604
+ function emit(state) {
605
+ const sections = [];
606
+ for (const key of SECTIONS) {
607
+ if (state[key]) {
608
+ const countKey = COUNTED_SECTIONS[key];
609
+ const count = countKey ? state.counts?.[countKey] ?? 0 : 0;
610
+ const attr = count ? ` count="${count}"` : "";
611
+ sections.push(`<${key}${attr}>
612
+ ${state[key]}
613
+ </${key}>`);
614
+ }
615
+ }
616
+ if (!sections.length) {
617
+ sections.push("<empty/>");
618
+ }
619
+ return `<aipeek url="${state.url}">
620
+
621
+ ${sections.join("\n\n")}
622
+
623
+ </aipeek>
624
+
625
+ detail: GET /__aipeek/{section}/{index}?full`;
626
+ }
627
+ function emitSummary(raw) {
628
+ const consoleErrors = raw.console.filter((l) => l.level === "error");
629
+ const consoleWarns = raw.console.filter((l) => l.level === "warn");
630
+ const failedReqs = raw.network.filter((r) => r.status >= 400 || r.failed);
631
+ const hasIssues = consoleErrors.length > 0 || raw.errors.length > 0 || failedReqs.length > 0;
632
+ const lines = [];
633
+ if (raw.ui.trim()) {
634
+ lines.push(`ui: ${summarizeUI(raw.ui)}`);
635
+ }
636
+ if (raw.console.length) {
637
+ if (consoleErrors.length || consoleWarns.length) {
638
+ const parts = [];
639
+ for (const e of consoleErrors) parts.push(` [error] ${truncate(e.text, 150)}`);
640
+ for (const w of consoleWarns) parts.push(` [warn] ${truncate(w.text, 150)}`);
641
+ const rest = raw.console.length - consoleErrors.length - consoleWarns.length;
642
+ if (rest > 0)
643
+ parts.push(` \u2026 ${rest} more`);
644
+ lines.push(`console (${raw.console.length}):`);
645
+ lines.push(...parts);
646
+ } else {
647
+ lines.push(`console: ${raw.console.length} logs`);
648
+ }
649
+ }
650
+ if (raw.network.length) {
651
+ if (failedReqs.length) {
652
+ lines.push(`network (${raw.network.length}):`);
653
+ for (const r of failedReqs) {
654
+ const body = r.responseBody ? ` "${truncate(r.responseBody, 80)}"` : "";
655
+ lines.push(` ${r.method} ${compactUrl(r.url)} ${r.status}${body}`);
656
+ }
657
+ const ok = raw.network.length - failedReqs.length;
658
+ if (ok > 0)
659
+ lines.push(` \u2026 ${ok} ok`);
660
+ } else {
661
+ lines.push(`network: ${raw.network.length} ok`);
662
+ }
663
+ }
664
+ if (raw.errors.length) {
665
+ lines.push(`errors (${raw.errors.length}):`);
666
+ for (const e of raw.errors) lines.push(` ${truncate(e.message, 150)}`);
667
+ }
668
+ const storeNames = Object.keys(raw.state);
669
+ if (storeNames.length) {
670
+ const parts = storeNames.map((n) => {
671
+ const v = raw.state[n];
672
+ const keys = typeof v === "object" && v !== null ? Object.keys(v).length : 0;
673
+ return keys > 0 ? `${n}(${keys})` : n;
674
+ });
675
+ lines.push(`state: ${parts.join(", ")}`);
676
+ }
677
+ if (!lines.length)
678
+ return "<aipeek>empty</aipeek>";
679
+ const status = hasIssues ? `${consoleErrors.length + raw.errors.length + failedReqs.length} issues` : "ok";
680
+ return `<aipeek url="${raw.url}" status="${status}">
681
+ ${lines.join("\n")}
682
+ </aipeek>${hasIssues ? "\n\ndetail: GET /__aipeek/{section}/{index}" : ""}`;
683
+ }
684
+ function summarizeUI(tree) {
685
+ const components = [];
686
+ const counts = /* @__PURE__ */ new Map();
687
+ for (const line of tree.split("\n")) {
688
+ const trimmed = line.trimStart();
689
+ if (!trimmed)
690
+ continue;
691
+ const indent = line.length - trimmed.length;
692
+ if (indent > 2)
693
+ continue;
694
+ const name = nameOf(trimmed);
695
+ if (!name)
696
+ continue;
697
+ const focused = trimmed.includes("[focused]");
698
+ const generating = trimmed.includes("[generating]");
699
+ const loading = trimmed.includes("[loading]");
700
+ const existing = counts.get(name) || 0;
701
+ if (existing > 0) {
702
+ counts.set(name, existing + 1);
703
+ continue;
704
+ }
705
+ counts.set(name, 1);
706
+ let label = name;
707
+ if (focused)
708
+ label += "[focused]";
709
+ if (generating)
710
+ label += "[generating]";
711
+ if (loading)
712
+ label += "[loading]";
713
+ components.push(label);
714
+ }
715
+ const result = components.map((c) => {
716
+ const name = c.split("[")[0];
717
+ const count = counts.get(name) || 1;
718
+ return count > 1 ? `${c}(\xD7${count})` : c;
719
+ });
720
+ return result.join(", ") || "empty";
721
+ }
722
+ function emitCheck(result) {
723
+ const lines = result.assertions.map(
724
+ (a) => a.pass ? `\u2713 ${a.name}` : `\u2717 ${a.name}${a.detail ? `: ${a.detail}` : ""}`
725
+ );
726
+ return `<aipeek-check pass="${result.pass}">
727
+ ${lines.join("\n")}
728
+ </aipeek-check>`;
729
+ }
730
+ function emitDiff(diff) {
731
+ const issues = [];
732
+ for (const e of diff.newErrors) issues.push(pc.red(` [error] ${e.text}`));
733
+ for (const e of diff.newExceptions) issues.push(pc.red(` [exception] ${e.message}`));
734
+ for (const r of diff.newFailedRequests) issues.push(pc.yellow(` [network] ${r.method} ${r.url} ${r.status}`));
735
+ if (diff.uiGone)
736
+ issues.push(pc.magenta(" [ui] component tree disappeared"));
737
+ if (!issues.length)
738
+ return "";
739
+ const count = issues.length;
740
+ return `${pc.bold("[aipeek]")} ${pc.red(`\u2717 ${count} issue${count > 1 ? "s" : ""} after HMR`)}
741
+ ${issues.join("\n")}`;
742
+ }
743
+
744
+ // src/server/plugin.ts
745
+ function readBody(req) {
746
+ return new Promise((resolve2) => {
747
+ let s = "";
748
+ req.on("data", (c) => s += c);
749
+ req.on("end", () => resolve2(s));
750
+ });
751
+ }
752
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
753
+ var clientPath = resolve(__dirname2, "../client/client.ts");
754
+ var patchPath = resolve(__dirname2, "../client/client-patch.ts");
755
+ function compilePatch() {
756
+ const source = readFileSync(patchPath, "utf-8");
757
+ const result = transformSync(source, {
758
+ loader: "ts",
759
+ target: "es2020",
760
+ format: "iife",
761
+ minify: false
762
+ });
763
+ return result.code;
764
+ }
765
+ function aipeekSnippet(port) {
766
+ const base = `http://localhost:${port}/__aipeek`;
767
+ return `
768
+ # aipeek \u2014 Runtime Browser Inspector
769
+
770
+ IMPORTANT: Before debugging any UI issue, visual bug, or runtime error, ALWAYS fetch the live app state first. Do NOT guess \u2014 look at the actual browser state.
771
+
772
+ ## Read state \u2014 cheapest first
773
+
774
+ \`\`\`bash
775
+ curl ${base}/screen # state-machine projection {view, modal, focus, knobs} \u2014 START HERE
776
+ curl ${base}/ui # React component tree \u2014 deep-dive when /screen isn't enough
777
+ curl '${base}/dom?scope=ChatInput' # semantic DOM scoped to a component \u2014 UI as text, src locations
778
+ curl ${base} # high-density summary (ok sections \u2192 1 line, issues \u2192 expanded)
779
+ curl ${base}?full # full dump: UI tree + console + network + errors + state
780
+ curl ${base}/check # pass/fail health check \u2014 use after code changes
781
+ curl ${base}/console # console logs (errors, warnings, info)
782
+ curl ${base}/network # fetch/XHR requests with status and timing
783
+ curl ${base}/errors # uncaught errors and unhandled rejections
784
+ curl ${base}/state # registered store snapshots
785
+ \`\`\`
786
+
787
+ \`/screen\` projects the whole UI to a few state variables \u2014 start there, not \`/ui\`. Append
788
+ \`?full\` for untruncated output. Append \`/{index}\` for a specific item's detail.
789
+
790
+ To inspect or edit a component, work top-down \u2014 the full DOM is huge, a scoped view is
791
+ accurate: \`/screen\` or \`/ui\` to find the component, then \`/dom?scope=<Name>\` (matches the
792
+ source path) or \`/dom?sel=<css>\` for just that subtree. Each line carries its source
793
+ location (\`@File.tsx:line\`), so the DOM view tells you exactly where to edit.
794
+
795
+ ## Drive the page (acts on the currently-open tab \u2014 no separate browser)
796
+
797
+ \`\`\`bash
798
+ curl '${base}/click?text=New' # click by visible text (or sel= for CSS)
799
+ curl '${base}/fill?sel=textarea&value=hi' # set an input/textarea/select value
800
+ curl '${base}/press?key=Enter' # key on focused element (e.g. Control+a)
801
+ curl '${base}/wait?text=Done&timeout=8000' # poll until text/sel appears (add gone=1 to wait until it disappears)
802
+ curl '${base}/screenshot?out=shot.png' # DOM\u2192PNG into .aipeek/ (html-to-image; lossy)
803
+ \`\`\`
804
+
805
+ \`click\`/\`fill\`/\`press\` settle the DOM and append the resulting UI tree (\`--- ui after ---\`)
806
+ to the response \u2014 no follow-up read needed. On a miss, the response lists the reachable
807
+ clickable elements so you can re-target. URL-encode any \`sel=\` with non-ASCII or quotes:
808
+ \`curl -G ${base}/click --data-urlencode 'sel=button[title="\u77E5\u8BC6\u5E93"]'\`.
809
+
810
+ **Chain \u2014 a whole interaction in one round-trip.** POST a JSON array; runs in sequence,
811
+ each step settles before the next, stops on first failure:
812
+
813
+ \`\`\`bash
814
+ curl -X POST ${base}/chain -d '[
815
+ {"type":"click","sel":"button[title=\\"\u77E5\u8BC6\u5E93\\"]"},
816
+ {"type":"wait","text":"Done"},
817
+ {"type":"fill","sel":"textarea","value":"hi"},
818
+ {"type":"press","key":"Enter"}
819
+ ]'
820
+ \`\`\`
821
+
822
+ **Escape hatch.** \`curl '${base}/eval?code=...'\` (or POST the code as the body) runs arbitrary
823
+ JS in the page and returns the result \u2014 for anything the typed endpoints can't do.
824
+
825
+ aipeek auto-detects errors after HMR and prints them to the terminal \u2014 watch for \`[aipeek]\` messages.
826
+ `;
827
+ }
828
+ function norm(line) {
829
+ const t = line.trim();
830
+ const i = t.indexOf("localhost:");
831
+ if (i === -1)
832
+ return t;
833
+ let j = i + "localhost:".length;
834
+ while (j < t.length && t[j] >= "0" && t[j] <= "9") j++;
835
+ return `${t.slice(0, i)}localhost:PORT${t.slice(j)}`;
836
+ }
837
+ function stripBlocks(content, snippet) {
838
+ const known = new Set(snippet.split("\n").map(norm).filter((l) => l.length > 3));
839
+ const lines = content.split("\n");
840
+ const keep = [];
841
+ let inside = false;
842
+ let buf = [];
843
+ let hits = 0;
844
+ const flush = () => {
845
+ if (buf.length && hits / buf.length <= 0.5)
846
+ keep.push(...buf);
847
+ buf = [];
848
+ hits = 0;
849
+ inside = false;
850
+ };
851
+ for (const line of lines) {
852
+ const isKnown = known.has(norm(line));
853
+ if (!inside) {
854
+ if (isKnown) {
855
+ inside = true;
856
+ buf = [line];
857
+ hits = 1;
858
+ } else {
859
+ keep.push(line);
860
+ }
861
+ continue;
862
+ }
863
+ buf.push(line);
864
+ if (isKnown)
865
+ hits++;
866
+ else if (buf.slice(-3).every((l) => !known.has(norm(l))))
867
+ flush();
868
+ }
869
+ flush();
870
+ return keep.join("\n");
871
+ }
872
+ function injectClaudeMd(root, port) {
873
+ const path = resolve(root, "CLAUDE.md");
874
+ const snippet = aipeekSnippet(port);
875
+ try {
876
+ if (!existsSync(path)) {
877
+ writeFileSync(path, snippet.trimStart());
878
+ return;
879
+ }
880
+ const stripped = stripBlocks(readFileSync(path, "utf-8"), snippet).trimEnd();
881
+ writeFileSync(path, `${stripped}
882
+ ${snippet}`);
883
+ } catch {
884
+ }
885
+ }
886
+ function aipeekPlugin() {
887
+ let pendingResolve = null;
888
+ let server;
889
+ let lastRaw = null;
890
+ let pushTimer;
891
+ const pendingActions = /* @__PURE__ */ new Map();
892
+ let actionId = 0;
893
+ let pendingDom = null;
894
+ let pendingScreen = null;
895
+ const pendingEvals = /* @__PURE__ */ new Map();
896
+ let evalId = 0;
897
+ const VISIBLE_MS = 400;
898
+ function twoPhase(event, payload, arm, fullMs = 3e3) {
899
+ return new Promise((resolve2, reject) => {
900
+ let settled = false;
901
+ const clear = arm((v) => {
902
+ settled = true;
903
+ resolve2(v);
904
+ });
905
+ server.hot.send(event, { ...payload, requireVisible: true });
906
+ setTimeout(() => {
907
+ if (settled)
908
+ return;
909
+ server.hot.send(event, { ...payload, requireVisible: false });
910
+ setTimeout(() => {
911
+ if (settled)
912
+ return;
913
+ clear();
914
+ reject(new Error(`timeout: no client response within ${VISIBLE_MS + fullMs}ms`));
915
+ }, fullMs);
916
+ }, VISIBLE_MS);
917
+ });
918
+ }
919
+ function collectFromClient() {
920
+ return twoPhase("aipeek:collect", {}, (resolve2) => {
921
+ pendingResolve = resolve2;
922
+ return () => {
923
+ pendingResolve = null;
924
+ };
925
+ });
926
+ }
927
+ function collectDomFromClient(scope, sel) {
928
+ return twoPhase("aipeek:collect-dom", { scope, sel }, (resolve2) => {
929
+ pendingDom = resolve2;
930
+ return () => {
931
+ pendingDom = null;
932
+ };
933
+ });
934
+ }
935
+ function collectScreenFromClient() {
936
+ return twoPhase("aipeek:collect-screen", {}, (resolve2) => {
937
+ pendingScreen = resolve2;
938
+ return () => {
939
+ pendingScreen = null;
940
+ };
941
+ });
942
+ }
943
+ function sendAction(type, args) {
944
+ const id = ++actionId;
945
+ const fullMs = Math.max(args.timeout ?? 0, 3e3) + 2e3;
946
+ return twoPhase("aipeek:action", { id, type, args }, (resolve2) => {
947
+ pendingActions.set(id, resolve2);
948
+ return () => {
949
+ pendingActions.delete(id);
950
+ };
951
+ }, fullMs);
952
+ }
953
+ function evalInClient(code) {
954
+ const id = ++evalId;
955
+ return twoPhase("aipeek:eval", { id, code }, (resolve2) => {
956
+ pendingEvals.set(id, resolve2);
957
+ return () => {
958
+ pendingEvals.delete(id);
959
+ };
960
+ }, 8e3);
961
+ }
962
+ return {
963
+ name: "aipeek",
964
+ apply: "serve",
965
+ transformIndexHtml() {
966
+ const patchCode = compilePatch();
967
+ return [
968
+ // Synchronous inline script — patches console/fetch/XHR/errors
969
+ // BEFORE any ES modules execute
970
+ {
971
+ tag: "script",
972
+ children: patchCode,
973
+ injectTo: "head-prepend"
974
+ },
975
+ // Module script — collectors + HMR channel (can be deferred)
976
+ {
977
+ tag: "script",
978
+ attrs: { type: "module" },
979
+ children: `import '/@fs/${clientPath}'`,
980
+ injectTo: "body"
981
+ }
982
+ ];
983
+ },
984
+ configureServer(_server) {
985
+ server = _server;
986
+ injectClaudeMd(server.config.root, server.config.server.port || 5173);
987
+ server.hot.on("aipeek:state", (data) => {
988
+ if (pendingResolve) {
989
+ pendingResolve(data);
990
+ pendingResolve = null;
991
+ }
992
+ });
993
+ server.hot.on("aipeek:result", (data) => {
994
+ const resolve2 = pendingActions.get(data.id);
995
+ if (resolve2) {
996
+ pendingActions.delete(data.id);
997
+ resolve2(data);
998
+ }
999
+ });
1000
+ server.hot.on("aipeek:eval-result", (data) => {
1001
+ const resolve2 = pendingEvals.get(data.id);
1002
+ if (resolve2) {
1003
+ pendingEvals.delete(data.id);
1004
+ resolve2(data);
1005
+ }
1006
+ });
1007
+ server.hot.on("aipeek:dom", (data) => {
1008
+ if (pendingDom) {
1009
+ pendingDom(data.dom);
1010
+ pendingDom = null;
1011
+ }
1012
+ });
1013
+ server.hot.on("aipeek:screen", (data) => {
1014
+ if (pendingScreen) {
1015
+ pendingScreen(data.screen);
1016
+ pendingScreen = null;
1017
+ }
1018
+ });
1019
+ server.hot.on("vite:afterUpdate", () => {
1020
+ clearTimeout(pushTimer);
1021
+ pushTimer = setTimeout(async () => {
1022
+ try {
1023
+ const raw = await collectFromClient();
1024
+ const diff = diffState(lastRaw, raw);
1025
+ lastRaw = raw;
1026
+ if (!diff.clean) {
1027
+ const msg = emitDiff(diff);
1028
+ if (msg)
1029
+ server.config.logger.warn(msg);
1030
+ }
1031
+ } catch {
1032
+ }
1033
+ }, 500);
1034
+ });
1035
+ server.middlewares.use("/__aipeek", async (req, res) => {
1036
+ const url = new URL(req.url || "/", "http://localhost");
1037
+ const parts = url.pathname.split("/").filter(Boolean);
1038
+ const full = url.searchParams.has("full");
1039
+ try {
1040
+ if (parts[0] === "eval") {
1041
+ let code = url.searchParams.get("code") || "";
1042
+ if (!code && req.method === "POST")
1043
+ code = await readBody(req);
1044
+ if (!code) {
1045
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1046
+ res.end("eval needs ?code= or a POST body");
1047
+ return;
1048
+ }
1049
+ const r = await evalInClient(code);
1050
+ res.writeHead(r.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1051
+ res.end(r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
1052
+ return;
1053
+ }
1054
+ if (parts[0] === "dom") {
1055
+ const dom = await collectDomFromClient(
1056
+ url.searchParams.get("scope") || void 0,
1057
+ url.searchParams.get("sel") || void 0
1058
+ );
1059
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1060
+ res.end(dom || "(empty)");
1061
+ return;
1062
+ }
1063
+ if (parts[0] === "screen") {
1064
+ const screen = await collectScreenFromClient();
1065
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1066
+ res.end(screen || "(empty)");
1067
+ return;
1068
+ }
1069
+ if (parts[0] === "chain") {
1070
+ const body = await readBody(req);
1071
+ let steps;
1072
+ try {
1073
+ steps = JSON.parse(body);
1074
+ if (!Array.isArray(steps))
1075
+ throw new Error("body must be a JSON array");
1076
+ } catch (e) {
1077
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1078
+ res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
1079
+ return;
1080
+ }
1081
+ lastRaw = null;
1082
+ const lines = [];
1083
+ let lastUi = "";
1084
+ let allOk = true;
1085
+ for (let i = 0; i < steps.length; i++) {
1086
+ const { type, ...args } = steps[i];
1087
+ const check2 = resolveAction(type, args);
1088
+ if (!check2.valid) {
1089
+ lines.push(`[${i}] \u2717 ${type}: ${check2.error}`);
1090
+ allOk = false;
1091
+ break;
1092
+ }
1093
+ const r = await sendAction(type, args);
1094
+ lines.push(`[${i}] ${r.ok ? "\u2713" : "\u2717"} ${type}: ${r.ok ? r.detail || "ok" : r.error}`);
1095
+ if (r.screen)
1096
+ lines.push(r.screen.split("\n").map((l) => ` ${l}`).join("\n"));
1097
+ if (r.ui)
1098
+ lastUi = r.ui;
1099
+ if (!r.ok) {
1100
+ allOk = false;
1101
+ break;
1102
+ }
1103
+ }
1104
+ res.writeHead(allOk ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1105
+ res.end(lastUi ? `${lines.join("\n")}
1106
+
1107
+ --- ui after ---
1108
+ ${lastUi}` : lines.join("\n"));
1109
+ return;
1110
+ }
1111
+ if (["click", "fill", "press", "wait", "screenshot"].includes(parts[0])) {
1112
+ const q = url.searchParams;
1113
+ const args = {
1114
+ sel: q.get("sel") || void 0,
1115
+ text: q.get("text") || void 0,
1116
+ value: q.has("value") ? q.get("value") : void 0,
1117
+ key: q.get("key") || void 0,
1118
+ timeout: q.has("timeout") ? Number(q.get("timeout")) : void 0,
1119
+ gone: q.has("gone") ? q.get("gone") !== "false" : void 0
1120
+ };
1121
+ const check2 = resolveAction(parts[0], args);
1122
+ if (!check2.valid) {
1123
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1124
+ res.end(check2.error);
1125
+ return;
1126
+ }
1127
+ const result = await sendAction(parts[0], args);
1128
+ lastRaw = null;
1129
+ if (parts[0] === "screenshot" && result.dataUrl) {
1130
+ const dir = resolve(server.config.root, ".aipeek");
1131
+ mkdirSync(dir, { recursive: true });
1132
+ const name = q.get("out") || `shot-${result.dataUrl.length}.png`;
1133
+ const file = resolve(dir, name);
1134
+ writeFileSync(file, Buffer.from(result.dataUrl.split(",")[1], "base64"));
1135
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1136
+ res.end(`saved: ${file}`);
1137
+ return;
1138
+ }
1139
+ res.writeHead(result.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1140
+ const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
1141
+
1142
+ clickable: ${result.detail}` : ""}`;
1143
+ res.end(result.ui ? `${head}
1144
+
1145
+ --- ui after ---
1146
+ ${result.ui}` : head);
1147
+ return;
1148
+ }
1149
+ if (parts[0] === "check") {
1150
+ const raw2 = await collectFromClient();
1151
+ lastRaw = raw2;
1152
+ const result = check(raw2);
1153
+ const output = emitCheck(result);
1154
+ res.writeHead(result.pass ? 200 : 417, { "Content-Type": "text/plain; charset=utf-8" });
1155
+ res.end(output);
1156
+ return;
1157
+ }
1158
+ if (parts.length >= 1) {
1159
+ if (!lastRaw)
1160
+ lastRaw = await collectFromClient();
1161
+ const result = detail(lastRaw, parts[0], parts[1], full);
1162
+ if (result !== null) {
1163
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1164
+ res.end(result);
1165
+ return;
1166
+ }
1167
+ res.writeHead(404, { "Content-Type": "text/plain" });
1168
+ res.end(`not found: ${parts.join("/")}`);
1169
+ return;
1170
+ }
1171
+ const raw = await collectFromClient();
1172
+ lastRaw = raw;
1173
+ if (full) {
1174
+ const compacted = compact(raw);
1175
+ const output = emit(compacted);
1176
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1177
+ res.end(output);
1178
+ } else {
1179
+ const output = emitSummary(raw);
1180
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1181
+ res.end(output);
1182
+ }
1183
+ } catch (err) {
1184
+ res.writeHead(504, { "Content-Type": "text/plain" });
1185
+ res.end(err instanceof Error ? err.message : "unknown error");
1186
+ }
1187
+ });
1188
+ }
1189
+ };
1190
+ }
1191
+
1192
+ export {
1193
+ check,
1194
+ diffState,
1195
+ emitSummary,
1196
+ emitCheck,
1197
+ emitDiff,
1198
+ aipeekPlugin
1199
+ };