aipeek 0.2.3 → 0.2.5

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.
@@ -830,61 +830,29 @@ JS in the page and returns the result \u2014 for anything the typed endpoints ca
830
830
  aipeek auto-detects errors after HMR and prints them to the terminal \u2014 watch for \`[aipeek]\` messages.
831
831
  `;
832
832
  }
833
- function norm(line) {
834
- const t = line.trim();
835
- const i = t.indexOf("localhost:");
836
- if (i === -1)
837
- return t;
838
- let j = i + "localhost:".length;
839
- while (j < t.length && t[j] >= "0" && t[j] <= "9") j++;
840
- return `${t.slice(0, i)}localhost:PORT${t.slice(j)}`;
841
- }
842
- function stripBlocks(content, snippet) {
843
- const known = new Set(snippet.split("\n").map(norm).filter((l) => l.length > 3));
844
- const lines = content.split("\n");
845
- const keep = [];
846
- let inside = false;
847
- let buf = [];
848
- let hits = 0;
849
- const flush = () => {
850
- if (buf.length && hits / buf.length <= 0.5)
851
- keep.push(...buf);
852
- buf = [];
853
- hits = 0;
854
- inside = false;
855
- };
856
- for (const line of lines) {
857
- const isKnown = known.has(norm(line));
858
- if (!inside) {
859
- if (isKnown) {
860
- inside = true;
861
- buf = [line];
862
- hits = 1;
863
- } else {
864
- keep.push(line);
865
- }
866
- continue;
867
- }
868
- buf.push(line);
869
- if (isKnown)
870
- hits++;
871
- else if (buf.slice(-3).every((l) => !known.has(norm(l))))
872
- flush();
873
- }
874
- flush();
875
- return keep.join("\n");
876
- }
833
+ var START_TAG = "<!-- AIPEEK:START -->";
834
+ var END_TAG = "<!-- AIPEEK:END -->";
877
835
  function injectClaudeMd(root, port) {
878
836
  const path = _path.resolve.call(void 0, root, "CLAUDE.md");
879
- const snippet = aipeekSnippet(port);
837
+ const block = `${START_TAG}
838
+ ${aipeekSnippet(port).trim()}
839
+ ${END_TAG}
840
+ `;
880
841
  try {
881
842
  if (!_fs.existsSync.call(void 0, path)) {
882
- _fs.writeFileSync.call(void 0, path, snippet.trimStart());
843
+ _fs.writeFileSync.call(void 0, path, block);
844
+ return;
845
+ }
846
+ const content = _fs.readFileSync.call(void 0, path, "utf-8");
847
+ const si = content.indexOf(START_TAG);
848
+ const ei = content.indexOf(END_TAG);
849
+ if (si !== -1 && ei !== -1) {
850
+ _fs.writeFileSync.call(void 0, path, content.slice(0, si) + block.trimEnd() + content.slice(ei + END_TAG.length));
883
851
  return;
884
852
  }
885
- const stripped = stripBlocks(_fs.readFileSync.call(void 0, path, "utf-8"), snippet).trimEnd();
886
- _fs.writeFileSync.call(void 0, path, `${stripped}
887
- ${snippet}`);
853
+ const sep = content.endsWith("\n") ? "" : "\n";
854
+ _fs.writeFileSync.call(void 0, path, `${content}${sep}
855
+ ${block}`);
888
856
  } catch (e6) {
889
857
  }
890
858
  }
@@ -1201,4 +1169,7 @@ ${result.ui}` : head);
1201
1169
 
1202
1170
 
1203
1171
 
1204
- exports.check = check; exports.diffState = diffState; exports.emitSummary = emitSummary; exports.emitCheck = emitCheck; exports.emitDiff = emitDiff; exports.aipeekPlugin = aipeekPlugin;
1172
+
1173
+
1174
+
1175
+ exports.check = check; exports.diffState = diffState; exports.emitSummary = emitSummary; exports.emitCheck = emitCheck; exports.emitDiff = emitDiff; exports.START_TAG = START_TAG; exports.END_TAG = END_TAG; exports.injectClaudeMd = injectClaudeMd; exports.aipeekPlugin = aipeekPlugin;
@@ -826,61 +826,29 @@ JS in the page and returns the result \u2014 for anything the typed endpoints ca
826
826
  aipeek auto-detects errors after HMR and prints them to the terminal \u2014 watch for \`[aipeek]\` messages.
827
827
  `;
828
828
  }
829
- function norm(line) {
830
- const t = line.trim();
831
- const i = t.indexOf("localhost:");
832
- if (i === -1)
833
- return t;
834
- let j = i + "localhost:".length;
835
- while (j < t.length && t[j] >= "0" && t[j] <= "9") j++;
836
- return `${t.slice(0, i)}localhost:PORT${t.slice(j)}`;
837
- }
838
- function stripBlocks(content, snippet) {
839
- const known = new Set(snippet.split("\n").map(norm).filter((l) => l.length > 3));
840
- const lines = content.split("\n");
841
- const keep = [];
842
- let inside = false;
843
- let buf = [];
844
- let hits = 0;
845
- const flush = () => {
846
- if (buf.length && hits / buf.length <= 0.5)
847
- keep.push(...buf);
848
- buf = [];
849
- hits = 0;
850
- inside = false;
851
- };
852
- for (const line of lines) {
853
- const isKnown = known.has(norm(line));
854
- if (!inside) {
855
- if (isKnown) {
856
- inside = true;
857
- buf = [line];
858
- hits = 1;
859
- } else {
860
- keep.push(line);
861
- }
862
- continue;
863
- }
864
- buf.push(line);
865
- if (isKnown)
866
- hits++;
867
- else if (buf.slice(-3).every((l) => !known.has(norm(l))))
868
- flush();
869
- }
870
- flush();
871
- return keep.join("\n");
872
- }
829
+ var START_TAG = "<!-- AIPEEK:START -->";
830
+ var END_TAG = "<!-- AIPEEK:END -->";
873
831
  function injectClaudeMd(root, port) {
874
832
  const path = resolve(root, "CLAUDE.md");
875
- const snippet = aipeekSnippet(port);
833
+ const block = `${START_TAG}
834
+ ${aipeekSnippet(port).trim()}
835
+ ${END_TAG}
836
+ `;
876
837
  try {
877
838
  if (!existsSync(path)) {
878
- writeFileSync(path, snippet.trimStart());
839
+ writeFileSync(path, block);
840
+ return;
841
+ }
842
+ const content = readFileSync(path, "utf-8");
843
+ const si = content.indexOf(START_TAG);
844
+ const ei = content.indexOf(END_TAG);
845
+ if (si !== -1 && ei !== -1) {
846
+ writeFileSync(path, content.slice(0, si) + block.trimEnd() + content.slice(ei + END_TAG.length));
879
847
  return;
880
848
  }
881
- const stripped = stripBlocks(readFileSync(path, "utf-8"), snippet).trimEnd();
882
- writeFileSync(path, `${stripped}
883
- ${snippet}`);
849
+ const sep = content.endsWith("\n") ? "" : "\n";
850
+ writeFileSync(path, `${content}${sep}
851
+ ${block}`);
884
852
  } catch {
885
853
  }
886
854
  }
@@ -1196,5 +1164,8 @@ export {
1196
1164
  emitSummary,
1197
1165
  emitCheck,
1198
1166
  emitDiff,
1167
+ START_TAG,
1168
+ END_TAG,
1169
+ injectClaudeMd,
1199
1170
  aipeekPlugin
1200
1171
  };
package/dist/index.cjs CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
 
7
7
 
8
- var _chunk3NVB3GGEcjs = require('./chunk-3NVB3GGE.cjs');
8
+ var _chunk6EZKMGRDcjs = require('./chunk-6EZKMGRD.cjs');
9
9
  require('./chunk-Z2Y65YOY.cjs');
10
10
 
11
11
 
@@ -14,4 +14,4 @@ require('./chunk-Z2Y65YOY.cjs');
14
14
 
15
15
 
16
16
 
17
- exports.aipeekPlugin = _chunk3NVB3GGEcjs.aipeekPlugin; exports.check = _chunk3NVB3GGEcjs.check; exports.diffState = _chunk3NVB3GGEcjs.diffState; exports.emitCheck = _chunk3NVB3GGEcjs.emitCheck; exports.emitDiff = _chunk3NVB3GGEcjs.emitDiff; exports.emitSummary = _chunk3NVB3GGEcjs.emitSummary;
17
+ exports.aipeekPlugin = _chunk6EZKMGRDcjs.aipeekPlugin; exports.check = _chunk6EZKMGRDcjs.check; exports.diffState = _chunk6EZKMGRDcjs.diffState; exports.emitCheck = _chunk6EZKMGRDcjs.emitCheck; exports.emitDiff = _chunk6EZKMGRDcjs.emitDiff; exports.emitSummary = _chunk6EZKMGRDcjs.emitSummary;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  emitCheck,
6
6
  emitDiff,
7
7
  emitSummary
8
- } from "./chunk-72ZKZ42D.js";
8
+ } from "./chunk-X3HAXWFJ.js";
9
9
  export {
10
10
  aipeekPlugin,
11
11
  check,
package/dist/plugin.cjs CHANGED
@@ -1,7 +1,13 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunk3NVB3GGEcjs = require('./chunk-3NVB3GGE.cjs');
3
+
4
+
5
+
6
+ var _chunk6EZKMGRDcjs = require('./chunk-6EZKMGRD.cjs');
4
7
  require('./chunk-Z2Y65YOY.cjs');
5
8
 
6
9
 
7
- exports.aipeekPlugin = _chunk3NVB3GGEcjs.aipeekPlugin;
10
+
11
+
12
+
13
+ exports.END_TAG = _chunk6EZKMGRDcjs.END_TAG; exports.START_TAG = _chunk6EZKMGRDcjs.START_TAG; exports.aipeekPlugin = _chunk6EZKMGRDcjs.aipeekPlugin; exports.injectClaudeMd = _chunk6EZKMGRDcjs.injectClaudeMd;
package/dist/plugin.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import {
2
- aipeekPlugin
3
- } from "./chunk-72ZKZ42D.js";
2
+ END_TAG,
3
+ START_TAG,
4
+ aipeekPlugin,
5
+ injectClaudeMd
6
+ } from "./chunk-X3HAXWFJ.js";
4
7
  export {
5
- aipeekPlugin
8
+ END_TAG,
9
+ START_TAG,
10
+ aipeekPlugin,
11
+ injectClaudeMd
6
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipeek",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Gives AI a peek into your running browser app — UI tree, console, network, errors, state",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,9 +19,7 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
- "src/client",
23
- "src/core/action.ts",
24
- "src/core/types.ts"
22
+ "src"
25
23
  ],
26
24
  "scripts": {
27
25
  "build": "tsup",
@@ -0,0 +1,34 @@
1
+ import type { CheckResult, RawState } from './types'
2
+
3
+ export function check(raw: RawState): CheckResult {
4
+ const consoleErrors = raw.console.filter(l => l.level === 'error')
5
+ const failedRequests = raw.network.filter(r => r.status >= 400 || r.failed)
6
+
7
+ const assertions = [
8
+ {
9
+ name: 'no-console-errors',
10
+ pass: consoleErrors.length === 0,
11
+ ...consoleErrors.length && { detail: consoleErrors[0].text },
12
+ },
13
+ {
14
+ name: 'no-uncaught-errors',
15
+ pass: raw.errors.length === 0,
16
+ ...raw.errors.length && { detail: raw.errors[0].message },
17
+ },
18
+ {
19
+ name: 'no-failed-requests',
20
+ pass: failedRequests.length === 0,
21
+ ...failedRequests.length && { detail: `${failedRequests[0].method} ${failedRequests[0].url} ${failedRequests[0].status}` },
22
+ },
23
+ {
24
+ name: 'ui-not-empty',
25
+ pass: raw.ui.trim().length > 0,
26
+ ...!raw.ui.trim().length && { detail: 'UI tree is empty' },
27
+ },
28
+ ]
29
+
30
+ return {
31
+ pass: assertions.every(a => a.pass),
32
+ assertions,
33
+ }
34
+ }
@@ -0,0 +1,338 @@
1
+ import type { CompactState, ErrorEntry, LogEntry, NetworkRequest, RawState } from './types'
2
+ import { compactUrl, truncate } from './util'
3
+
4
+ const SLOW_THRESHOLD = 1000
5
+
6
+ // --- UI (merged component tree + DOM semantics) ---
7
+
8
+ const MAX_UI_DEPTH = 6
9
+ const UI_PRIMITIVES = 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
+
46
+ // component name = leading run before the first space, '[', or '—' separator
47
+ export function nameOf(line: string): string {
48
+ let end = 0
49
+ while (end < line.length) {
50
+ const c = line[end]
51
+ if (c === ' ' || c === '\t' || c === '[' || c === '—')
52
+ break
53
+ end++
54
+ }
55
+ return line.slice(0, end)
56
+ }
57
+
58
+ export function compactUI(tree: string): string {
59
+ if (!tree)
60
+ return ''
61
+
62
+ const lines = tree.split('\n')
63
+ const result: string[] = []
64
+ const repeatTracker = new Map<string, { count: number, lastIndex: number }>()
65
+
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i]
68
+ const trimmed = line.trimStart()
69
+ if (!trimmed)
70
+ continue
71
+
72
+ const indent = line.length - trimmed.length
73
+ const depth = Math.floor(indent / 2)
74
+ if (depth > MAX_UI_DEPTH)
75
+ continue
76
+
77
+ const componentName = nameOf(trimmed)
78
+ if (UI_PRIMITIVES.has(componentName))
79
+ continue
80
+
81
+ // fold repeated siblings
82
+ const key = `${depth}:${componentName}`
83
+ const tracker = repeatTracker.get(key)
84
+ if (tracker && i - tracker.lastIndex <= 2) {
85
+ tracker.count++
86
+ tracker.lastIndex = i
87
+ continue
88
+ }
89
+
90
+ // flush previous repeat groups
91
+ for (const [k, t] of repeatTracker) {
92
+ if (t.count > 1) {
93
+ const d = Number.parseInt(k.split(':')[0])
94
+ const name = k.split(':').slice(1).join(':')
95
+ result.push(`${' '.repeat(d)}${name} ×${t.count}`)
96
+ }
97
+ if (t.count > 1 || i - t.lastIndex > 2) {
98
+ repeatTracker.delete(k)
99
+ }
100
+ }
101
+
102
+ repeatTracker.set(key, { count: 1, lastIndex: i })
103
+ result.push(line)
104
+ }
105
+
106
+ // flush remaining
107
+ for (const [k, t] of repeatTracker) {
108
+ if (t.count > 1) {
109
+ const d = Number.parseInt(k.split(':')[0])
110
+ const name = k.split(':').slice(1).join(':')
111
+ result.push(`${' '.repeat(d)}${name} ×${t.count}`)
112
+ }
113
+ }
114
+
115
+ return result.join('\n')
116
+ }
117
+
118
+ // --- Console ---
119
+
120
+ // case-insensitive substrings that mark a log line as dev-tooling noise
121
+ const NOISE_SUBSTRINGS = [
122
+ '[hmr]',
123
+ '[vite]',
124
+ 'hot module',
125
+ 'react-devtools',
126
+ 'download the react devtools',
127
+ 'warning: react does not recognize',
128
+ 'source map',
129
+ 'favicon.ico',
130
+ 'webpack',
131
+ ]
132
+
133
+ export function compactConsole(logs: LogEntry[]): string {
134
+ if (!logs.length)
135
+ return ''
136
+
137
+ // filter noise
138
+ const filtered = logs.filter((l) => {
139
+ const lower = l.text.toLowerCase()
140
+ return !NOISE_SUBSTRINGS.some(s => lower.includes(s))
141
+ })
142
+ if (!filtered.length)
143
+ return ''
144
+
145
+ // dedup consecutive same messages
146
+ const deduped: { entry: LogEntry, count: number }[] = []
147
+ for (const log of filtered) {
148
+ const last = deduped[deduped.length - 1]
149
+ if (last && last.entry.text === log.text && last.entry.level === log.level) {
150
+ last.count++
151
+ }
152
+ else {
153
+ deduped.push({ entry: log, count: 1 })
154
+ }
155
+ }
156
+
157
+ // prioritize: errors first, then warns, then recent info/debug
158
+ const errors = deduped.filter(d => d.entry.level === 'error')
159
+ const warns = deduped.filter(d => d.entry.level === 'warn')
160
+ const rest = deduped.filter(d => d.entry.level !== 'error' && d.entry.level !== 'warn')
161
+
162
+ // keep last N info/debug entries
163
+ const recentRest = rest.slice(-10)
164
+
165
+ const lines: string[] = []
166
+ for (const group of [...errors, ...warns, ...recentRest]) {
167
+ const prefix = `[${group.entry.level}]`
168
+ const count = group.count > 1 ? ` ×${group.count}` : ''
169
+ const source = group.entry.source ? ` (${group.entry.source})` : ''
170
+ const text = truncate(group.entry.text, 200)
171
+ lines.push(`${prefix}${count} ${text}${source}`)
172
+ }
173
+
174
+ return lines.join('\n')
175
+ }
176
+
177
+ // --- Network ---
178
+
179
+ export function compactNetwork(requests: NetworkRequest[]): string {
180
+ if (!requests.length)
181
+ return ''
182
+
183
+ // only fetch/XHR
184
+ const relevant = requests.filter(r =>
185
+ r.resourceType === 'fetch' || r.resourceType === 'xhr' || r.resourceType === 'websocket'
186
+ || r.resourceType === 'eventsource' || isApiUrl(r.url),
187
+ )
188
+ if (!relevant.length)
189
+ return ''
190
+
191
+ const lines: string[] = []
192
+ for (const req of relevant) {
193
+ const duration = req.duration > 0 ? ` ${formatDuration(req.duration)}` : ''
194
+ const slow = req.duration >= SLOW_THRESHOLD ? ' [SLOW]' : ''
195
+ const url = compactUrl(req.url, 50)
196
+ const headers = diagnosticHeaders(req)
197
+
198
+ if (req.failed || req.status >= 400) {
199
+ // failed: show more detail
200
+ const body = req.responseBody ? ` "${truncate(req.responseBody, 100)}"` : ''
201
+ const failure = req.failureText ? ` (${req.failureText})` : ''
202
+ lines.push(`${req.method} ${url} ${req.status}${failure}${body}${headers}${duration}${slow}`)
203
+ }
204
+ else {
205
+ lines.push(`${req.method} ${url} ${req.status}${headers}${duration}${slow}`)
206
+ }
207
+ }
208
+
209
+ return lines.join('\n')
210
+ }
211
+
212
+ function isApiUrl(url: string): boolean {
213
+ try {
214
+ const u = new URL(url)
215
+ return u.pathname.startsWith('/api') || u.pathname.includes('/graphql')
216
+ }
217
+ catch {
218
+ return false
219
+ }
220
+ }
221
+
222
+ const DIAGNOSTIC_HEADERS = ['content-type', 'x-error', 'www-authenticate', 'access-control-allow-origin']
223
+
224
+ function diagnosticHeaders(req: NetworkRequest): string {
225
+ const h = req.responseHeaders
226
+ if (!h)
227
+ return ''
228
+ const parts: string[] = []
229
+ for (const key of DIAGNOSTIC_HEADERS) {
230
+ const val = h[key]
231
+ if (!val)
232
+ continue
233
+ // skip common json content-type on success — not diagnostic
234
+ if (key === 'content-type' && req.status < 400 && val.includes('application/json'))
235
+ continue
236
+ parts.push(`${key}: ${truncate(val, 60)}`)
237
+ }
238
+ if (!parts.length)
239
+ return ''
240
+ return ` [${parts.join(', ')}]`
241
+ }
242
+
243
+ function formatDuration(ms: number): string {
244
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`
245
+ }
246
+
247
+ // --- Errors ---
248
+
249
+ export function compactErrors(errors: ErrorEntry[]): string {
250
+ if (!errors.length)
251
+ return ''
252
+
253
+ // dedup by message
254
+ const seen = new Map<string, ErrorEntry>()
255
+ for (const err of errors) {
256
+ if (!seen.has(err.message)) {
257
+ seen.set(err.message, err)
258
+ }
259
+ }
260
+
261
+ const lines: string[] = []
262
+ for (const err of seen.values()) {
263
+ lines.push(err.message)
264
+ if (err.stack) {
265
+ const frames = filterStack(err.stack)
266
+ for (const frame of frames) {
267
+ lines.push(` at ${frame}`)
268
+ }
269
+ }
270
+ }
271
+
272
+ return lines.join('\n')
273
+ }
274
+
275
+ function filterStack(stack: string): string[] {
276
+ return stack
277
+ .split('\n')
278
+ .map(l => l.trim())
279
+ .filter(l => l.startsWith('at '))
280
+ .map(l => l.slice(3))
281
+ .filter(l => !l.includes('node_modules') && !l.includes('<anonymous>'))
282
+ .slice(0, 5)
283
+ }
284
+
285
+ // --- State ---
286
+
287
+ export function compactState(state: Record<string, unknown>): string {
288
+ if (!state || !Object.keys(state).length)
289
+ return ''
290
+
291
+ const lines: string[] = []
292
+ for (const [name, value] of Object.entries(state)) {
293
+ lines.push(`${name}:`)
294
+ if (typeof value === 'object' && value !== null) {
295
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
296
+ lines.push(` ${k}: ${formatValue(v)}`)
297
+ }
298
+ }
299
+ else {
300
+ lines.push(` ${String(value)}`)
301
+ }
302
+ }
303
+ return lines.join('\n')
304
+ }
305
+
306
+ function formatValue(v: unknown): string {
307
+ if (v === null || v === undefined)
308
+ return String(v)
309
+ if (typeof v === 'string')
310
+ return v
311
+ if (typeof v === 'number' || typeof v === 'boolean')
312
+ return String(v)
313
+ if (typeof v === 'object') {
314
+ const s = JSON.stringify(v)
315
+ return s.length > 120 ? `${s.slice(0, 120)}…` : s
316
+ }
317
+ return String(v)
318
+ }
319
+
320
+ // --- Main ---
321
+
322
+ export function compact(raw: RawState): CompactState {
323
+ return {
324
+ url: raw.url,
325
+ ui: compactUI(raw.ui),
326
+ console: compactConsole(raw.console),
327
+ network: compactNetwork(raw.network),
328
+ errors: compactErrors(raw.errors),
329
+ state: compactState(raw.state),
330
+ timestamp: raw.timestamp,
331
+ counts: {
332
+ console: raw.console.length,
333
+ network: raw.network.length,
334
+ errors: raw.errors.length,
335
+ state: Object.keys(raw.state).length,
336
+ },
337
+ }
338
+ }