browsertrack 0.0.1 → 0.1.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 (101) hide show
  1. package/README.md +143 -0
  2. package/dist/chunk-6A7FFDIB.js +221 -0
  3. package/dist/chunk-6A7FFDIB.js.map +1 -0
  4. package/dist/chunk-ANMR5WBH.js +659 -0
  5. package/dist/chunk-ANMR5WBH.js.map +1 -0
  6. package/dist/chunk-BSKNG4V7.js +1199 -0
  7. package/dist/chunk-BSKNG4V7.js.map +1 -0
  8. package/dist/chunk-X7C5CHBO.js +2080 -0
  9. package/dist/chunk-X7C5CHBO.js.map +1 -0
  10. package/dist/chunk-ZLNGOOH2.js +554 -0
  11. package/dist/chunk-ZLNGOOH2.js.map +1 -0
  12. package/dist/cli/index.js +2829 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/client/index.cjs +2218 -0
  15. package/dist/client/index.d.ts +197 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client.iife.js +309 -0
  19. package/dist/core/index.d.ts +78 -0
  20. package/dist/core/index.js +31 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/daemon/index.d.ts +58 -0
  23. package/dist/daemon/index.js +32 -0
  24. package/dist/daemon/index.js.map +1 -0
  25. package/dist/engine-DKloFjvs.d.ts +157 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +50 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mcp/index.d.ts +11 -0
  30. package/dist/mcp/index.js +12 -0
  31. package/dist/mcp/index.js.map +1 -0
  32. package/dist/notes-CBvN91Wf.d.ts +260 -0
  33. package/dist/projects-CY8ungMt.d.ts +99 -0
  34. package/dist/server-CN-se8td.d.ts +55 -0
  35. package/examples/test-app/index.html +299 -0
  36. package/package.json +52 -7
  37. package/packages/cli/src/index.ts +413 -0
  38. package/packages/client/package.json +28 -0
  39. package/packages/client/src/breadcrumbs.ts +30 -0
  40. package/packages/client/src/client.ts +278 -0
  41. package/packages/client/src/commands/handler.ts +301 -0
  42. package/packages/client/src/config.ts +37 -0
  43. package/packages/client/src/index.ts +50 -0
  44. package/packages/client/src/interceptors/console.ts +68 -0
  45. package/packages/client/src/interceptors/interaction.ts +119 -0
  46. package/packages/client/src/interceptors/navigation.ts +93 -0
  47. package/packages/client/src/interceptors/network.ts +165 -0
  48. package/packages/client/src/interceptors/runtime.ts +65 -0
  49. package/packages/client/src/notes/inspector.ts +943 -0
  50. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  51. package/packages/client/src/screenshot/driver.ts +14 -0
  52. package/packages/client/src/transport/websocket.ts +179 -0
  53. package/packages/client/tsconfig.json +8 -0
  54. package/packages/core/dist/index.d.ts +317 -0
  55. package/packages/core/dist/index.js +221 -0
  56. package/packages/core/package.json +23 -0
  57. package/packages/core/src/fingerprint.ts +120 -0
  58. package/packages/core/src/index.ts +9 -0
  59. package/packages/core/src/redaction.ts +139 -0
  60. package/packages/core/src/selector.ts +77 -0
  61. package/packages/core/src/types/commands.ts +101 -0
  62. package/packages/core/src/types/events.ts +108 -0
  63. package/packages/core/src/types/incidents.ts +54 -0
  64. package/packages/core/src/types/notes.ts +106 -0
  65. package/packages/core/src/types/probes.ts +44 -0
  66. package/packages/core/src/types/projects.ts +20 -0
  67. package/packages/core/tsconfig.json +8 -0
  68. package/packages/daemon/src/config.ts +29 -0
  69. package/packages/daemon/src/incidents/engine.ts +211 -0
  70. package/packages/daemon/src/index.ts +18 -0
  71. package/packages/daemon/src/notes/engine.ts +92 -0
  72. package/packages/daemon/src/notes/verification.ts +191 -0
  73. package/packages/daemon/src/server/daemon.ts +112 -0
  74. package/packages/daemon/src/server/http.ts +165 -0
  75. package/packages/daemon/src/server/ws.ts +175 -0
  76. package/packages/daemon/src/session/manager.ts +126 -0
  77. package/packages/daemon/src/storage/db.ts +733 -0
  78. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  79. package/packages/daemon/src/verification/engine.ts +264 -0
  80. package/packages/mcp/src/handlers.ts +398 -0
  81. package/packages/mcp/src/index.ts +3 -0
  82. package/packages/mcp/src/server.ts +86 -0
  83. package/packages/mcp/src/tools.ts +236 -0
  84. package/src/index.ts +4 -0
  85. package/test/client/interceptors.test.ts +69 -0
  86. package/test/core/fingerprint.test.ts +53 -0
  87. package/test/core/notes.test.ts +66 -0
  88. package/test/core/redaction.test.ts +48 -0
  89. package/test/daemon/incident-engine.test.ts +98 -0
  90. package/test/daemon/notes-storage.test.ts +130 -0
  91. package/test/daemon/notes-verification.test.ts +135 -0
  92. package/test/daemon/storage.test.ts +123 -0
  93. package/test/daemon/verification-engine.test.ts +88 -0
  94. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  95. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  96. package/test/mcp/handlers.test.ts +116 -0
  97. package/test/mcp/notes.test.ts +107 -0
  98. package/tsconfig.base.json +17 -0
  99. package/tsconfig.json +26 -0
  100. package/tsup.config.ts +54 -0
  101. package/vitest.config.ts +9 -0
@@ -0,0 +1,413 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getDaemonConfig } from '../../daemon/src/config.js';
5
+ import { createDaemon } from '../../daemon/src/index.js';
6
+ import { StorageDB } from '../../daemon/src/storage/db.js';
7
+ import { createMcpServer } from '../../mcp/src/server.js';
8
+
9
+ const program = new Command();
10
+ program.name('browsertrack').description('Local browser diagnostics + MCP bridge for coding agents').version('0.1.0');
11
+
12
+ function getPidFilePath(): string {
13
+ const config = getDaemonConfig();
14
+ return path.join(config.dataDir, 'daemon.pid');
15
+ }
16
+
17
+ function savePid(pid: number): void {
18
+ const pidFile = getPidFilePath();
19
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
20
+ fs.writeFileSync(pidFile, String(pid), 'utf-8');
21
+ }
22
+
23
+ function readPid(): number | null {
24
+ try {
25
+ const pidFile = getPidFilePath();
26
+ if (fs.existsSync(pidFile)) {
27
+ const pidStr = fs.readFileSync(pidFile, 'utf-8').trim();
28
+ const pid = parseInt(pidStr, 10);
29
+ if (!isNaN(pid)) return pid;
30
+ }
31
+ } catch {}
32
+ return null;
33
+ }
34
+
35
+ function removePid(): void {
36
+ try {
37
+ const pidFile = getPidFilePath();
38
+ if (fs.existsSync(pidFile)) {
39
+ fs.unlinkSync(pidFile);
40
+ }
41
+ } catch {}
42
+ }
43
+
44
+ function isProcessRunning(pid: number): boolean {
45
+ try {
46
+ process.kill(pid, 0);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ // 1. START
54
+ program
55
+ .command('start')
56
+ .description('Start the local BrowserTrack daemon and HTTP/WS server')
57
+ .option('-p, --port <number>', 'Server port', '7331')
58
+ .option('-h, --host <host>', 'Server host', '127.0.0.1')
59
+ .option('-v, --verbose', 'Enable verbose logging', false)
60
+ .action(async (options) => {
61
+ const existingPid = readPid();
62
+ if (existingPid && isProcessRunning(existingPid)) {
63
+ console.log(`[BrowserTrack] Daemon is already running (PID: ${existingPid})`);
64
+ return;
65
+ }
66
+
67
+ const port = parseInt(options.port, 10);
68
+ const daemon = createDaemon({
69
+ port,
70
+ host: options.host,
71
+ verbose: options.verbose,
72
+ });
73
+
74
+ try {
75
+ await daemon.start();
76
+ savePid(process.pid);
77
+
78
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
79
+ console.log(' šŸ” BrowserTrack Daemon Active');
80
+ console.log(` 🌐 Server: http://${daemon.config.host}:${daemon.config.port}`);
81
+ console.log(` šŸ”Œ WebSocket: ws://${daemon.config.host}:${daemon.config.port}`);
82
+ console.log(` šŸ“¦ Script: <script src="http://${daemon.config.host}:${daemon.config.port}/client.js"></script>`);
83
+ console.log(` šŸ’¾ Database: ${daemon.config.dbPath}`);
84
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
85
+
86
+ // Graceful shutdown handling
87
+ let isShuttingDown = false;
88
+ const shutdown = async () => {
89
+ if (isShuttingDown) {
90
+ process.exit(0);
91
+ }
92
+ isShuttingDown = true;
93
+ console.log('\n[BrowserTrack] Shutting down daemon...');
94
+ removePid();
95
+
96
+ // Safety fallback timer to prevent terminal freeze
97
+ const forceExitTimer = setTimeout(() => {
98
+ process.exit(0);
99
+ }, 800);
100
+ forceExitTimer.unref();
101
+
102
+ try {
103
+ await daemon.stop();
104
+ } catch {}
105
+ process.exit(0);
106
+ };
107
+
108
+ process.on('SIGINT', shutdown);
109
+ process.on('SIGTERM', shutdown);
110
+ } catch (err: any) {
111
+ console.error('[BrowserTrack] Failed to start daemon:', err.message);
112
+ process.exit(1);
113
+ }
114
+ });
115
+
116
+ // 2. STOP
117
+ program
118
+ .command('stop')
119
+ .description('Stop the running BrowserTrack daemon')
120
+ .action(async () => {
121
+ const pid = readPid();
122
+ if (!pid || !isProcessRunning(pid)) {
123
+ console.log('[BrowserTrack] Daemon is not currently running.');
124
+ removePid();
125
+ return;
126
+ }
127
+
128
+ try {
129
+ process.kill(pid, 'SIGTERM');
130
+ removePid();
131
+ console.log(`[BrowserTrack] Stopped daemon (PID: ${pid}).`);
132
+ } catch (err: any) {
133
+ console.error(`[BrowserTrack] Failed to stop daemon:`, err.message);
134
+ }
135
+ });
136
+
137
+ // 3. STATUS
138
+ program
139
+ .command('status')
140
+ .description('Check if the BrowserTrack daemon is running and view active sessions')
141
+ .action(async () => {
142
+ const pid = readPid();
143
+ const isRunning = pid ? isProcessRunning(pid) : false;
144
+ const config = getDaemonConfig();
145
+
146
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
147
+ console.log(` Status: ${isRunning ? '🟢 RUNNING' : '⚪ STOPPED'}${isRunning ? ` (PID: ${pid})` : ''}`);
148
+ console.log(` Endpoint: http://${config.host}:${config.port}`);
149
+ console.log(` Database: ${config.dbPath}`);
150
+
151
+ if (fs.existsSync(config.dbPath)) {
152
+ try {
153
+ const db = new StorageDB(config.dbPath);
154
+ const projects = db.listProjects();
155
+ const sessions = db.listSessions(undefined, true);
156
+ const incidents = db.listIncidents({ limit: 100 });
157
+ const openIncidents = incidents.filter((i) => i.status === 'OPEN');
158
+
159
+ console.log(` Projects: ${projects.length}`);
160
+ console.log(` Live Sessions: ${sessions.length}`);
161
+ console.log(` Open Errors: ${openIncidents.length} (${incidents.length} total recorded)`);
162
+ db.close();
163
+ } catch {}
164
+ }
165
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
166
+ });
167
+
168
+ // 4. PROJECTS
169
+ const projectCommand = program.command('project').description('Manage tracked project mappings');
170
+
171
+ projectCommand
172
+ .command('add <name>')
173
+ .description('Register a project with origin and filesystem path')
174
+ .requiredOption('-o, --origin <origin>', 'Project origin (e.g. http://localhost:5173)')
175
+ .option('-p, --path <path>', 'Filesystem path (e.g. /path/to/project)')
176
+ .action((name, options) => {
177
+ const config = getDaemonConfig();
178
+ const db = new StorageDB(config.dbPath);
179
+ const resolvedPath = options.path ? path.resolve(options.path) : undefined;
180
+
181
+ const proj = db.upsertProject({
182
+ id: `proj_${name}`,
183
+ name,
184
+ origin: options.origin,
185
+ path: resolvedPath,
186
+ });
187
+
188
+ console.log(`[BrowserTrack] Registered project '${proj.name}':`);
189
+ console.log(` ID: ${proj.id}`);
190
+ console.log(` Origin: ${proj.origin}`);
191
+ console.log(` Path: ${proj.path || '(none)'}`);
192
+ db.close();
193
+ });
194
+
195
+ program
196
+ .command('projects')
197
+ .description('List all tracked projects')
198
+ .action(() => {
199
+ const config = getDaemonConfig();
200
+ const db = new StorageDB(config.dbPath);
201
+ const projects = db.listProjects();
202
+
203
+ if (projects.length === 0) {
204
+ console.log('[BrowserTrack] No projects registered yet. Projects will be auto-detected upon browser connection.');
205
+ } else {
206
+ console.log('\nTracked Projects:');
207
+ for (const p of projects) {
208
+ console.log(` • ${p.name.padEnd(16)} | ${p.origin.padEnd(26)} | ${p.path || '(auto-detected)'}`);
209
+ }
210
+ console.log('');
211
+ }
212
+ db.close();
213
+ });
214
+
215
+ // 5. ERRORS / INCIDENTS
216
+ program
217
+ .command('errors')
218
+ .description('List recorded runtime errors and incidents')
219
+ .option('-p, --project <project>', 'Filter by project name or ID')
220
+ .option('-s, --status <status>', 'Filter by status (OPEN, VERIFIED, FAILED, etc.)')
221
+ .option('-l, --limit <number>', 'Limit result count', '20')
222
+ .action((options) => {
223
+ const config = getDaemonConfig();
224
+ const db = new StorageDB(config.dbPath);
225
+ const limit = parseInt(options.limit, 10);
226
+ const incidents = db.listIncidents({
227
+ projectId: options.project,
228
+ status: options.status,
229
+ limit,
230
+ });
231
+
232
+ if (incidents.length === 0) {
233
+ console.log('[BrowserTrack] No incidents found matching the filter.');
234
+ } else {
235
+ console.log(`\nIncidents (${incidents.length}):`);
236
+ for (const inc of incidents) {
237
+ const statusBadge =
238
+ inc.status === 'OPEN'
239
+ ? 'šŸ”“ OPEN'
240
+ : inc.status === 'VERIFIED'
241
+ ? '🟢 VERIFIED'
242
+ : inc.status === 'FAILED'
243
+ ? 'āŒ FAILED'
244
+ : `⚪ ${inc.status}`;
245
+ console.log(` ${inc.id.padEnd(12)} [${statusBadge}] (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
246
+ console.log(` Source: ${inc.source.file}:${inc.source.line} | Route: ${inc.route}`);
247
+ }
248
+ console.log('');
249
+ }
250
+ db.close();
251
+ });
252
+
253
+ // 6. VISUAL NOTES
254
+ const noteCmd = program.command('note').description('Inspect or manage visual development notes');
255
+
256
+ noteCmd
257
+ .command('show <noteId>')
258
+ .description('Show full context for a specific visual note')
259
+ .action((noteId) => {
260
+ const config = getDaemonConfig();
261
+ const db = new StorageDB(config.dbPath);
262
+ const note = db.getNote(noteId);
263
+
264
+ if (!note) {
265
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
266
+ } else {
267
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
268
+ console.log(` šŸ“ Visual Note: ${note.id} [${note.status}]`);
269
+ console.log(` šŸ“ Route: ${note.route} (${note.url})`);
270
+ console.log(` šŸ“ Viewport: ${note.viewport.width} Ɨ ${note.viewport.height} (dpr: ${note.viewport.devicePixelRatio})`);
271
+ if (note.target) {
272
+ console.log(` šŸŽÆ Target: ${note.target.selector}`);
273
+ console.log(` Bounds: x:${note.target.boundingRect.x}, y:${note.target.boundingRect.y}, ${note.target.boundingRect.width}Ɨ${note.target.boundingRect.height}`);
274
+ }
275
+ console.log(` šŸ’¬ Note: "${note.message}"`);
276
+ if (note.screenshots?.original) {
277
+ console.log(` šŸ–¼ļø Screenshot: ${note.screenshots.original}`);
278
+ }
279
+ console.log(` šŸ•’ Created: ${note.createdAt}`);
280
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
281
+ }
282
+ db.close();
283
+ });
284
+
285
+ noteCmd
286
+ .command('resolve <noteId>')
287
+ .description('Mark a visual note as resolved')
288
+ .action((noteId) => {
289
+ const config = getDaemonConfig();
290
+ const db = new StorageDB(config.dbPath);
291
+ const note = db.getNote(noteId);
292
+
293
+ if (!note) {
294
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
295
+ } else {
296
+ db.updateNoteStatus(noteId, 'RESOLVED');
297
+ console.log(`[BrowserTrack] Marked note '${noteId}' as RESOLVED.`);
298
+ }
299
+ db.close();
300
+ });
301
+
302
+ noteCmd
303
+ .command('reopen <noteId>')
304
+ .description('Reopen a visual note')
305
+ .action((noteId) => {
306
+ const config = getDaemonConfig();
307
+ const db = new StorageDB(config.dbPath);
308
+ const note = db.getNote(noteId);
309
+
310
+ if (!note) {
311
+ console.log(`[BrowserTrack] Note '${noteId}' not found.`);
312
+ } else {
313
+ db.updateNoteStatus(noteId, 'OPEN');
314
+ console.log(`[BrowserTrack] Reopened note '${noteId}' (status: OPEN).`);
315
+ }
316
+ db.close();
317
+ });
318
+
319
+ program
320
+ .command('notes')
321
+ .description('List visual development notes')
322
+ .option('-p, --project <project>', 'Filter by project name or ID')
323
+ .option('-s, --status <status>', 'Filter by status (OPEN, RESOLVED, etc.)')
324
+ .option('-l, --limit <number>', 'Limit result count', '20')
325
+ .action((options) => {
326
+ const config = getDaemonConfig();
327
+ const db = new StorageDB(config.dbPath);
328
+ const limit = parseInt(options.limit, 10);
329
+ const notes = db.listNotes({
330
+ projectId: options.project,
331
+ status: options.status,
332
+ limit,
333
+ });
334
+
335
+ if (notes.length === 0) {
336
+ console.log('[BrowserTrack] No visual notes found.');
337
+ } else {
338
+ console.log(`\nVisual Notes (${notes.length}):`);
339
+ for (const n of notes) {
340
+ const badge = n.status === 'OPEN' ? '🟔 OPEN' : n.status === 'RESOLVED' ? '🟢 RESOLVED' : `⚪ ${n.status}`;
341
+ console.log(` ${n.id.padEnd(12)} [${badge}] Route: ${n.route.padEnd(16)} | Target: ${n.target?.selector || n.type}`);
342
+ console.log(` Note: "${n.message}" (${n.viewport.width}Ɨ${n.viewport.height})`);
343
+ }
344
+ console.log('');
345
+ }
346
+ db.close();
347
+ });
348
+
349
+ // 7. INBOX (Errors + Visual Notes)
350
+ program
351
+ .command('inbox')
352
+ .description('View combined developer inbox with active runtime errors and visual notes')
353
+ .option('-p, --project <project>', 'Filter by project name or ID')
354
+ .action((options) => {
355
+ const config = getDaemonConfig();
356
+ const db = new StorageDB(config.dbPath);
357
+ const incidents = db.listIncidents({ projectId: options.project, status: 'OPEN', limit: 20 });
358
+ const notes = db.listNotes({ projectId: options.project, status: 'OPEN', limit: 20 });
359
+
360
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
361
+ console.log(` šŸ“„ Browser Development Inbox ${options.project ? `(${options.project})` : ''}`);
362
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
363
+
364
+ if (incidents.length === 0 && notes.length === 0) {
365
+ console.log(' ✨ All clear! No open errors or visual notes.');
366
+ } else {
367
+ if (incidents.length > 0) {
368
+ console.log(`\n 🚨 Runtime Errors (${incidents.length}):`);
369
+ for (const inc of incidents) {
370
+ console.log(` • ${inc.id} (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
371
+ console.log(` Route: ${inc.route} | Source: ${inc.source.file}:${inc.source.line}`);
372
+ }
373
+ }
374
+
375
+ if (notes.length > 0) {
376
+ console.log(`\n šŸ“ Visual Notes (${notes.length}):`);
377
+ for (const n of notes) {
378
+ console.log(` • ${n.id} on ${n.route} (${n.viewport.width}Ɨ${n.viewport.height})`);
379
+ console.log(` Target: ${n.target?.selector || n.type} | Note: "${n.message}"`);
380
+ }
381
+ }
382
+ }
383
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
384
+ db.close();
385
+ });
386
+
387
+ // 6. CLEAR
388
+ program
389
+ .command('clear')
390
+ .description('Clear all stored incidents, events, and sessions from the database')
391
+ .action(() => {
392
+ const config = getDaemonConfig();
393
+ const db = new StorageDB(config.dbPath);
394
+ db.clearAll();
395
+ console.log('[BrowserTrack] Database cleared.');
396
+ db.close();
397
+ });
398
+
399
+ // 7. MCP SERVER
400
+ program
401
+ .command('mcp')
402
+ .description('Launch the Model Context Protocol (MCP) server over stdio')
403
+ .action(async () => {
404
+ try {
405
+ const server = createMcpServer();
406
+ await server.startStdio();
407
+ } catch (err: any) {
408
+ console.error('[BrowserTrack] MCP Server error:', err?.message);
409
+ process.exit(1);
410
+ }
411
+ });
412
+
413
+ program.parse(process.argv);
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@browserdiag/client",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight browser diagnostics client for BrowserDiag",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format esm,cjs,iife --global-name BrowserDiag --dts --clean",
18
+ "dev": "tsup src/index.ts --format esm,cjs,iife --global-name BrowserDiag --dts --watch",
19
+ "clean": "rm -rf dist"
20
+ },
21
+ "dependencies": {
22
+ "@browserdiag/core": "^0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.3.6",
26
+ "typescript": "^5.7.3"
27
+ }
28
+ }
@@ -0,0 +1,30 @@
1
+ import type { Breadcrumb } from '@browserdiag/core';
2
+
3
+ export class BreadcrumbBuffer {
4
+ private capacity: number;
5
+ private buffer: Breadcrumb[] = [];
6
+
7
+ constructor(capacity = 50) {
8
+ this.capacity = capacity;
9
+ }
10
+
11
+ public add(breadcrumb: Omit<Breadcrumb, 'timestamp'> & { timestamp?: number }): void {
12
+ const item: Breadcrumb = {
13
+ ...breadcrumb,
14
+ timestamp: breadcrumb.timestamp || Date.now(),
15
+ };
16
+
17
+ this.buffer.push(item);
18
+ if (this.buffer.length > this.capacity) {
19
+ this.buffer.shift();
20
+ }
21
+ }
22
+
23
+ public getRecent(): Breadcrumb[] {
24
+ return [...this.buffer];
25
+ }
26
+
27
+ public clear(): void {
28
+ this.buffer = [];
29
+ }
30
+ }