aipeek 0.1.4 → 0.2.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.
@@ -1,621 +0,0 @@
1
- // src/plugin.ts
2
- import { existsSync, readFileSync, writeFileSync } from "fs";
3
- import { dirname, resolve } from "path";
4
- import { fileURLToPath } from "url";
5
-
6
- // src/compact.ts
7
- var SLOW_THRESHOLD = 1e3;
8
- var MAX_UI_DEPTH = 6;
9
- var UI_PRIMITIVES = /* @__PURE__ */ new Set([
10
- "Button",
11
- "Input",
12
- "Label",
13
- "Badge",
14
- "Checkbox",
15
- "Skeleton",
16
- "Spinner",
17
- "Switch",
18
- "Tabs",
19
- "Tooltip",
20
- "Popover",
21
- "Dialog",
22
- "Select",
23
- "Card",
24
- "Table",
25
- "Slider",
26
- "Progress",
27
- "RadioGroup",
28
- "HoverCard",
29
- "DropdownMenu",
30
- "ContextMenu",
31
- "Command",
32
- "Form",
33
- "Alert",
34
- "Pagination",
35
- "Textarea",
36
- "TooltipProvider",
37
- "DialogPortal",
38
- "Router",
39
- "RenderErrorBoundary",
40
- "RouterProvider",
41
- "RouterProvider2",
42
- "PanelGroup",
43
- "Panel"
44
- ]);
45
- function compactUI(tree) {
46
- if (!tree)
47
- return "";
48
- const lines = tree.split("\n");
49
- const result = [];
50
- const repeatTracker = /* @__PURE__ */ new Map();
51
- for (let i = 0; i < lines.length; i++) {
52
- const line = lines[i];
53
- const trimmed = line.trimStart();
54
- if (!trimmed)
55
- continue;
56
- const indent = line.length - trimmed.length;
57
- const depth = Math.floor(indent / 2);
58
- if (depth > MAX_UI_DEPTH)
59
- continue;
60
- const componentName = trimmed.split(/[\s[—]/)[0];
61
- if (UI_PRIMITIVES.has(componentName))
62
- continue;
63
- const key = `${depth}:${componentName}`;
64
- const tracker = repeatTracker.get(key);
65
- if (tracker && i - tracker.lastIndex <= 2) {
66
- tracker.count++;
67
- tracker.lastIndex = i;
68
- continue;
69
- }
70
- for (const [k, t] of repeatTracker) {
71
- if (t.count > 1) {
72
- const d = Number.parseInt(k.split(":")[0]);
73
- const name = k.split(":").slice(1).join(":");
74
- result.push(`${" ".repeat(d)}${name} \xD7${t.count}`);
75
- }
76
- if (t.count > 1 || i - t.lastIndex > 2) {
77
- repeatTracker.delete(k);
78
- }
79
- }
80
- repeatTracker.set(key, { count: 1, lastIndex: i });
81
- result.push(line);
82
- }
83
- for (const [k, t] of repeatTracker) {
84
- if (t.count > 1) {
85
- const d = Number.parseInt(k.split(":")[0]);
86
- const name = k.split(":").slice(1).join(":");
87
- result.push(`${" ".repeat(d)}${name} \xD7${t.count}`);
88
- }
89
- }
90
- return result.join("\n");
91
- }
92
- var NOISE_PATTERNS = [
93
- /\[HMR\]/i,
94
- /\[vite\]/i,
95
- /hot module/i,
96
- /react-devtools/i,
97
- /download the react devtools/i,
98
- /warning: react does not recognize/i,
99
- /source map/i,
100
- /favicon\.ico/,
101
- /webpack/i,
102
- /\[webpack\]/i
103
- ];
104
- function compactConsole(logs) {
105
- if (!logs.length)
106
- return "";
107
- const filtered = logs.filter((l) => !NOISE_PATTERNS.some((p) => p.test(l.text)));
108
- if (!filtered.length)
109
- return "";
110
- const deduped = [];
111
- for (const log of filtered) {
112
- const last = deduped[deduped.length - 1];
113
- if (last && last.entry.text === log.text && last.entry.level === log.level) {
114
- last.count++;
115
- } else {
116
- deduped.push({ entry: log, count: 1 });
117
- }
118
- }
119
- const errors = deduped.filter((d) => d.entry.level === "error");
120
- const warns = deduped.filter((d) => d.entry.level === "warn");
121
- const rest = deduped.filter((d) => d.entry.level !== "error" && d.entry.level !== "warn");
122
- const recentRest = rest.slice(-10);
123
- const lines = [];
124
- for (const group of [...errors, ...warns, ...recentRest]) {
125
- const prefix = `[${group.entry.level}]`;
126
- const count = group.count > 1 ? ` \xD7${group.count}` : "";
127
- const source = group.entry.source ? ` (${group.entry.source})` : "";
128
- const text = truncate(group.entry.text, 200);
129
- lines.push(`${prefix}${count} ${text}${source}`);
130
- }
131
- return lines.join("\n");
132
- }
133
- function compactNetwork(requests) {
134
- if (!requests.length)
135
- return "";
136
- const relevant = requests.filter(
137
- (r) => r.resourceType === "fetch" || r.resourceType === "xhr" || r.resourceType === "websocket" || isApiUrl(r.url)
138
- );
139
- if (!relevant.length)
140
- return "";
141
- const lines = [];
142
- for (const req of relevant) {
143
- const duration = req.duration > 0 ? ` ${formatDuration(req.duration)}` : "";
144
- const slow = req.duration >= SLOW_THRESHOLD ? " [SLOW]" : "";
145
- const url = compactUrl(req.url);
146
- if (req.failed || req.status >= 400) {
147
- const body = req.responseBody ? ` "${truncate(req.responseBody, 100)}"` : "";
148
- const failure = req.failureText ? ` (${req.failureText})` : "";
149
- lines.push(`${req.method} ${url} ${req.status}${failure}${body}${duration}${slow}`);
150
- } else {
151
- lines.push(`${req.method} ${url} ${req.status}${duration}${slow}`);
152
- }
153
- }
154
- return lines.join("\n");
155
- }
156
- function isApiUrl(url) {
157
- try {
158
- const u = new URL(url);
159
- return u.pathname.startsWith("/api") || u.pathname.includes("/graphql");
160
- } catch {
161
- return false;
162
- }
163
- }
164
- function compactUrl(url) {
165
- try {
166
- const u = new URL(url);
167
- const path = u.pathname + (u.search ? `?${truncate(u.search.slice(1), 50)}` : "");
168
- return path;
169
- } catch {
170
- return truncate(url, 80);
171
- }
172
- }
173
- function formatDuration(ms) {
174
- return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
175
- }
176
- function compactErrors(errors) {
177
- if (!errors.length)
178
- return "";
179
- const seen = /* @__PURE__ */ new Map();
180
- for (const err of errors) {
181
- if (!seen.has(err.message)) {
182
- seen.set(err.message, err);
183
- }
184
- }
185
- const lines = [];
186
- for (const err of seen.values()) {
187
- lines.push(err.message);
188
- if (err.stack) {
189
- const frames = filterStack(err.stack);
190
- for (const frame of frames) {
191
- lines.push(` at ${frame}`);
192
- }
193
- }
194
- }
195
- return lines.join("\n");
196
- }
197
- function filterStack(stack) {
198
- 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);
199
- }
200
- function compactState(state) {
201
- if (!state || !Object.keys(state).length)
202
- return "";
203
- const lines = [];
204
- for (const [name, value] of Object.entries(state)) {
205
- lines.push(`${name}:`);
206
- if (typeof value === "object" && value !== null) {
207
- for (const [k, v] of Object.entries(value)) {
208
- lines.push(` ${k}: ${formatValue(v)}`);
209
- }
210
- } else {
211
- lines.push(` ${String(value)}`);
212
- }
213
- }
214
- return lines.join("\n");
215
- }
216
- function formatValue(v) {
217
- if (v === null || v === void 0)
218
- return String(v);
219
- if (typeof v === "string")
220
- return v;
221
- if (typeof v === "number" || typeof v === "boolean")
222
- return String(v);
223
- if (typeof v === "object") {
224
- const s = JSON.stringify(v);
225
- return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
226
- }
227
- return String(v);
228
- }
229
- function compact(raw) {
230
- return {
231
- url: raw.url,
232
- ui: compactUI(raw.ui),
233
- console: compactConsole(raw.console),
234
- network: compactNetwork(raw.network),
235
- errors: compactErrors(raw.errors),
236
- state: compactState(raw.state),
237
- timestamp: raw.timestamp,
238
- counts: {
239
- console: raw.console.length,
240
- network: raw.network.length,
241
- errors: raw.errors.length,
242
- state: Object.keys(raw.state).length
243
- }
244
- };
245
- }
246
- function truncate(s, max) {
247
- return s.length > max ? `${s.slice(0, max)}\u2026` : s;
248
- }
249
-
250
- // src/detail.ts
251
- async function detail(raw, section, index, full) {
252
- switch (section) {
253
- case "ui":
254
- return full ? raw.ui || null : compactUI(raw.ui) || null;
255
- case "console":
256
- return detailConsole(raw.console, index, full);
257
- case "network":
258
- return detailNetwork(raw.network, index, full);
259
- case "errors":
260
- return detailError(raw.errors, index, full);
261
- case "state":
262
- return detailState(raw.state, index, full);
263
- default:
264
- return null;
265
- }
266
- }
267
- function detailConsole(logs, index, full) {
268
- const i = Number.parseInt(index ?? "");
269
- if (isNaN(i) || i < 0 || i >= logs.length)
270
- return null;
271
- const log = logs[i];
272
- if (full) {
273
- const parts = [`[${log.level}] ${log.text}`];
274
- if (log.timestamp)
275
- parts.push(`timestamp: ${new Date(log.timestamp).toISOString()}`);
276
- if (log.source)
277
- parts.push(`source: ${log.source}`);
278
- return parts.join("\n");
279
- }
280
- return `[${log.level}] ${truncate2(log.text, 200)}`;
281
- }
282
- async function detailNetwork(requests, index, full) {
283
- const i = Number.parseInt(index ?? "");
284
- if (isNaN(i) || i < 0 || i >= requests.length)
285
- return null;
286
- const req = requests[i];
287
- const lines = [
288
- `${req.method} ${req.url}`,
289
- `status: ${req.status}`,
290
- `duration: ${req.duration}ms`,
291
- `type: ${req.resourceType}`
292
- ];
293
- if (req.failed)
294
- lines.push(`failed: ${req.failureText || "true"}`);
295
- if (full) {
296
- if (req.requestHeaders && Object.keys(req.requestHeaders).length) {
297
- lines.push("request-headers:");
298
- for (const [k, v] of Object.entries(req.requestHeaders)) lines.push(` ${k}: ${v}`);
299
- }
300
- if (req.requestBody)
301
- lines.push(`request-body:
302
- ${req.requestBody}`);
303
- if (req.responseHeaders && Object.keys(req.responseHeaders).length) {
304
- lines.push("response-headers:");
305
- for (const [k, v] of Object.entries(req.responseHeaders)) lines.push(` ${k}: ${v}`);
306
- }
307
- if (req.responseBody)
308
- lines.push(`response-body:
309
- ${req.responseBody}`);
310
- } else {
311
- if (req.requestBody) {
312
- lines.push(`request-body: ${byteSize(req.requestBody)}`);
313
- if (req.requestSample) {
314
- const ts = await quickTypeOrFallback(req.requestSample, "RequestBody");
315
- if (ts)
316
- lines.push(ts);
317
- }
318
- }
319
- if (req.responseBody) {
320
- if (req.status >= 400) {
321
- lines.push(`response-body: ${byteSize(req.responseBody)} "${truncate2(req.responseBody, 100)}"`);
322
- } else {
323
- if (req.responseSample) {
324
- const ts = await quickTypeOrFallback(req.responseSample, "ResponseBody");
325
- lines.push(`response-body: ${byteSize(req.responseBody)}`);
326
- if (ts)
327
- lines.push(ts);
328
- } else {
329
- lines.push(`response-body: ${byteSize(req.responseBody)} ${truncate2(req.responseBody, 100)}`);
330
- }
331
- }
332
- }
333
- }
334
- return lines.join("\n");
335
- }
336
- async function quickTypeOrFallback(sample, typeName) {
337
- try {
338
- const { InputData, jsonInputForTargetLanguage, quicktype } = await import("quicktype-core");
339
- const jsonInput = jsonInputForTargetLanguage("typescript");
340
- await jsonInput.addSource({ name: typeName, samples: [sample] });
341
- const inputData = new InputData();
342
- inputData.addInput(jsonInput);
343
- const { lines } = await quicktype({
344
- inputData,
345
- lang: "typescript",
346
- rendererOptions: { "just-types": "true", "acronym-style": "original" }
347
- });
348
- return lines.join("\n");
349
- } catch {
350
- return jsonSchema(sample);
351
- }
352
- }
353
- function detailError(errors, index, full) {
354
- const i = Number.parseInt(index ?? "");
355
- if (isNaN(i) || i < 0 || i >= errors.length)
356
- return null;
357
- const err = errors[i];
358
- if (full) {
359
- const lines2 = [err.message];
360
- if (err.stack)
361
- lines2.push(err.stack);
362
- if (err.source)
363
- lines2.push(`source: ${err.source}`);
364
- if (err.line != null)
365
- lines2.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
366
- return lines2.join("\n");
367
- }
368
- const lines = [err.message];
369
- if (err.stack) {
370
- const appFrames = err.stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules")).slice(0, 3);
371
- if (appFrames.length)
372
- lines.push(...appFrames);
373
- const totalApp = err.stack.split("\n").filter((l) => l.trim().startsWith("at ") && !l.includes("node_modules")).length;
374
- if (totalApp > 3)
375
- lines.push(` ... ${totalApp - 3} more app frames`);
376
- }
377
- if (err.line != null)
378
- lines.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
379
- return lines.join("\n");
380
- }
381
- function detailState(state, name, full) {
382
- if (!name || !(name in state))
383
- return null;
384
- const value = state[name];
385
- if (full) {
386
- try {
387
- return JSON.stringify(value, null, 2);
388
- } catch {
389
- return String(value);
390
- }
391
- }
392
- if (typeof value !== "object" || value === null)
393
- return `${name}: ${typeof value}`;
394
- const lines = [];
395
- for (const [k, v] of Object.entries(value)) {
396
- lines.push(`${k}: ${formatSummaryValue(v)}`);
397
- }
398
- return lines.join("\n");
399
- }
400
- function formatSummaryValue(v) {
401
- if (v === null || v === void 0)
402
- return String(v);
403
- if (typeof v === "string") {
404
- if (/^Array\(\d+\)$/.test(v))
405
- return v;
406
- return v.length > 80 ? `${v.slice(0, 80)}\u2026` : v;
407
- }
408
- if (typeof v === "number" || typeof v === "boolean")
409
- return String(v);
410
- if (typeof v === "object") {
411
- const s = JSON.stringify(v);
412
- return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
413
- }
414
- return String(v);
415
- }
416
- function jsonSchema(sample) {
417
- try {
418
- return schemaOf(JSON.parse(sample), 0);
419
- } catch {
420
- return null;
421
- }
422
- }
423
- function schemaOf(v, d) {
424
- if (v === null)
425
- return "null";
426
- if (typeof v === "string")
427
- return "string";
428
- if (typeof v === "number")
429
- return "number";
430
- if (typeof v === "boolean")
431
- return "boolean";
432
- if (Array.isArray(v)) {
433
- if (!v.length)
434
- return "[]";
435
- if (d >= 3)
436
- return "[\u2026]";
437
- return `${schemaOf(v[0], d + 1)}[]`;
438
- }
439
- if (typeof v === "object") {
440
- if (d >= 3)
441
- return "{\u2026}";
442
- const entries = Object.entries(v);
443
- if (!entries.length)
444
- return "{}";
445
- const max = d === 0 ? 12 : 6;
446
- const fields = entries.slice(0, max).map(([k, val]) => `${k}: ${schemaOf(val, d + 1)}`);
447
- if (entries.length > max)
448
- fields.push(`\u2026 ${entries.length - max} more`);
449
- return `{ ${fields.join(", ")} }`;
450
- }
451
- return typeof v;
452
- }
453
- function truncate2(s, max) {
454
- return s.length > max ? `${s.slice(0, max)}\u2026` : s;
455
- }
456
- function byteSize(s) {
457
- const bytes = new TextEncoder().encode(s).length;
458
- if (bytes < 1024)
459
- return `${bytes}B`;
460
- if (bytes < 1024 * 1024)
461
- return `${(bytes / 1024).toFixed(1)}KB`;
462
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
463
- }
464
-
465
- // src/emit.ts
466
- var SECTIONS = ["ui", "console", "network", "errors", "state"];
467
- var COUNTED_SECTIONS = {
468
- console: "console",
469
- network: "network",
470
- errors: "errors",
471
- state: "state"
472
- };
473
- function emit(state) {
474
- const sections = [];
475
- for (const key of SECTIONS) {
476
- if (state[key]) {
477
- const countKey = COUNTED_SECTIONS[key];
478
- const count = countKey ? state.counts?.[countKey] ?? 0 : 0;
479
- const attr = count ? ` count="${count}"` : "";
480
- sections.push(`<${key}${attr}>
481
- ${state[key]}
482
- </${key}>`);
483
- }
484
- }
485
- if (!sections.length) {
486
- sections.push("<empty/>");
487
- }
488
- return `<aipeek url="${state.url}">
489
-
490
- ${sections.join("\n\n")}
491
-
492
- </aipeek>
493
-
494
- detail: GET /__aipeek/{ui|console|network|errors|state}/{index}`;
495
- }
496
-
497
- // src/plugin.ts
498
- var __dirname = dirname(fileURLToPath(import.meta.url));
499
- var clientPath = resolve(__dirname, "client.ts");
500
- function aipeekSnippet(port) {
501
- const base = `http://localhost:${port}/__aipeek`;
502
- return `
503
- # aipeek \u2014 Runtime Browser Inspector
504
-
505
- 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.
506
-
507
- \`\`\`bash
508
- curl ${base} # full summary: UI tree + console + network + errors + state
509
- curl ${base}/ui # React component tree with props and DOM semantics
510
- curl ${base}/console # console logs (errors, warnings, info)
511
- curl ${base}/network # fetch/XHR requests with status and timing
512
- curl ${base}/errors # uncaught errors and unhandled rejections
513
- curl ${base}/state # registered store snapshots
514
- \`\`\`
515
-
516
- Append \`?full\` for untruncated output. Append \`/{index}\` for a specific item's detail.
517
- `;
518
- }
519
- var AIPEEK_HEADING = "# aipeek \u2014 Runtime Browser Inspector";
520
- function injectClaudeMd(root, port) {
521
- const path = resolve(root, "CLAUDE.md");
522
- const snippet = aipeekSnippet(port);
523
- try {
524
- if (!existsSync(path)) {
525
- writeFileSync(path, snippet.trimStart());
526
- return;
527
- }
528
- const content = readFileSync(path, "utf-8");
529
- const start = content.indexOf(AIPEEK_HEADING);
530
- if (start === -1) {
531
- writeFileSync(path, `${content.trimEnd()}
532
- ${snippet}`);
533
- return;
534
- }
535
- const afterHeading = content.indexOf("\n", start);
536
- const nextHeading = content.indexOf("\n# ", afterHeading);
537
- const end = nextHeading === -1 ? content.length : nextHeading;
538
- const before = content.slice(0, start).trimEnd();
539
- const after = content.slice(end);
540
- writeFileSync(path, before + (before ? "\n" : "") + snippet + after);
541
- } catch {
542
- }
543
- }
544
- function aipeekPlugin() {
545
- let pendingResolve = null;
546
- let server;
547
- let lastRaw = null;
548
- function collectFromClient() {
549
- return new Promise((resolve2, reject) => {
550
- pendingResolve = resolve2;
551
- setTimeout(() => {
552
- pendingResolve = null;
553
- reject(new Error("timeout: no client response within 3s"));
554
- }, 3e3);
555
- server.hot.send("aipeek:collect", {});
556
- });
557
- }
558
- return {
559
- name: "aipeek",
560
- apply: "serve",
561
- transformIndexHtml() {
562
- return [{
563
- tag: "script",
564
- attrs: { type: "module" },
565
- children: `import '/@fs/${clientPath}'`,
566
- injectTo: "body"
567
- }];
568
- },
569
- configureServer(_server) {
570
- server = _server;
571
- injectClaudeMd(server.config.root, server.config.server.port || 5173);
572
- server.hot.on("aipeek:state", (data) => {
573
- if (pendingResolve) {
574
- pendingResolve(data);
575
- pendingResolve = null;
576
- }
577
- });
578
- server.middlewares.use("/__aipeek", async (req, res) => {
579
- const url = new URL(req.url || "/", "http://localhost");
580
- const parts = url.pathname.split("/").filter(Boolean);
581
- const full = url.searchParams.has("full");
582
- try {
583
- if (parts.length >= 1) {
584
- if (!lastRaw)
585
- lastRaw = await collectFromClient();
586
- const result = await detail(lastRaw, parts[0], parts[1], full);
587
- if (result !== null) {
588
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
589
- res.end(result);
590
- return;
591
- }
592
- res.writeHead(404, { "Content-Type": "text/plain" });
593
- res.end(`not found: ${parts.join("/")}`);
594
- return;
595
- }
596
- const raw = await collectFromClient();
597
- lastRaw = raw;
598
- const compacted = compact(raw);
599
- const output = emit(compacted);
600
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
601
- res.end(output);
602
- } catch (err) {
603
- res.writeHead(504, { "Content-Type": "text/plain" });
604
- res.end(err instanceof Error ? err.message : "unknown error");
605
- }
606
- });
607
- }
608
- };
609
- }
610
-
611
- export {
612
- compactUI,
613
- compactConsole,
614
- compactNetwork,
615
- compactErrors,
616
- compactState,
617
- compact,
618
- detail,
619
- emit,
620
- aipeekPlugin
621
- };