browsertrack 0.2.1 → 0.2.3
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.
- package/AGENTS.md +9 -6
- package/README.md +2 -0
- package/dist/{chunk-464D4U2U.js → chunk-5NR5K3ER.js} +299 -44
- package/dist/chunk-5NR5K3ER.js.map +1 -0
- package/dist/{chunk-3HOXPTM2.js → chunk-G5CIZSQM.js} +808 -53
- package/dist/chunk-G5CIZSQM.js.map +1 -0
- package/dist/{chunk-6VA7GBAO.js → chunk-ONPW7AYL.js} +144 -12
- package/dist/chunk-ONPW7AYL.js.map +1 -0
- package/dist/{chunk-INXDWPJW.js → chunk-PG4JJDCV.js} +353 -119
- package/dist/chunk-PG4JJDCV.js.map +1 -0
- package/dist/cli/index.js +1058 -546
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +449 -128
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -2
- package/dist/client.iife.js +121 -15
- package/dist/core/index.d.ts +32 -2
- package/dist/core/index.js +11 -1
- package/dist/daemon/index.d.ts +2 -2
- package/dist/daemon/index.js +6 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -7
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +7 -4
- package/dist/{server-DiVmTrIR.d.ts → server-BRG-RQQP.d.ts} +10 -1
- package/docs/cli.md +4 -1
- package/docs/getting-started.md +60 -6
- package/docs/mcp-reference.md +42 -0
- package/package.json +1 -1
- package/packages/cli/src/index.ts +247 -151
- package/packages/client/src/interceptors/navigation.ts +38 -26
- package/packages/client/src/interceptors/network.ts +22 -17
- package/packages/client/src/notes/inspector.ts +289 -65
- package/packages/client/src/source/resolver.ts +9 -3
- package/packages/client/src/transport/websocket.ts +23 -18
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/safety.ts +86 -0
- package/packages/core/src/selector.ts +110 -15
- package/packages/daemon/src/server/daemon.ts +7 -1
- package/packages/daemon/src/server/http.ts +125 -5
- package/packages/daemon/src/server/ws.ts +33 -29
- package/packages/daemon/src/storage/db.ts +57 -35
- package/packages/mcp/src/handlers.ts +121 -45
- package/packages/mcp/src/server.ts +221 -3
- package/test/core/safety.test.ts +106 -0
- package/test/core/selector.test.ts +133 -0
- package/test/daemon/storage.test.ts +36 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
- package/test/mcp/auto-start.test.ts +87 -0
- package/dist/chunk-3HOXPTM2.js.map +0 -1
- package/dist/chunk-464D4U2U.js.map +0 -1
- package/dist/chunk-6VA7GBAO.js.map +0 -1
- package/dist/chunk-7OCOQGDN.js +0 -635
- package/dist/chunk-7OCOQGDN.js.map +0 -1
- package/dist/chunk-INXDWPJW.js.map +0 -1
|
@@ -4,7 +4,16 @@ import path from 'node:path';
|
|
|
4
4
|
import { getDaemonConfig } from '../../daemon/src/config.js';
|
|
5
5
|
import { createDaemon } from '../../daemon/src/index.js';
|
|
6
6
|
import { StorageDB } from '../../daemon/src/storage/db.js';
|
|
7
|
-
import { createMcpServer } from '../../mcp/src/server.js';
|
|
7
|
+
import { createMcpServer, isDaemonRunning } from '../../mcp/src/server.js';
|
|
8
|
+
|
|
9
|
+
// Global process safety handlers to prevent fatal crashes
|
|
10
|
+
process.on('uncaughtException', (err: any) => {
|
|
11
|
+
console.error('[BrowserTrack] Uncaught Exception:', err?.message || err);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
process.on('unhandledRejection', (reason: any) => {
|
|
15
|
+
console.error('[BrowserTrack] Unhandled Rejection:', reason?.message || reason);
|
|
16
|
+
});
|
|
8
17
|
|
|
9
18
|
const program = new Command();
|
|
10
19
|
program.name('browsertrack').description('Local browser diagnostics + MCP bridge for coding agents').version('0.1.0');
|
|
@@ -65,6 +74,11 @@ program
|
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
const port = parseInt(options.port, 10);
|
|
77
|
+
const isRunningHttp = await isDaemonRunning(options.host, port);
|
|
78
|
+
if (isRunningHttp) {
|
|
79
|
+
console.log(`[BrowserTrack] Daemon is already running on http://${options.host}:${port} (active via MCP Server).`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
68
82
|
const daemon = createDaemon({
|
|
69
83
|
port,
|
|
70
84
|
host: options.host,
|
|
@@ -120,6 +134,13 @@ program
|
|
|
120
134
|
.action(async () => {
|
|
121
135
|
const pid = readPid();
|
|
122
136
|
if (!pid || !isProcessRunning(pid)) {
|
|
137
|
+
const config = getDaemonConfig();
|
|
138
|
+
const isRunningHttp = await isDaemonRunning(config.host, config.port);
|
|
139
|
+
if (isRunningHttp) {
|
|
140
|
+
console.log('[BrowserTrack] Daemon is actively maintained by an MCP Server process in your IDE and will shut down when your editor session ends.');
|
|
141
|
+
removePid();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
123
144
|
console.log('[BrowserTrack] Daemon is not currently running.');
|
|
124
145
|
removePid();
|
|
125
146
|
return;
|
|
@@ -140,11 +161,20 @@ program
|
|
|
140
161
|
.description('Check if the BrowserTrack daemon is running and view active sessions')
|
|
141
162
|
.action(async () => {
|
|
142
163
|
const pid = readPid();
|
|
143
|
-
const isRunning = pid ? isProcessRunning(pid) : false;
|
|
144
164
|
const config = getDaemonConfig();
|
|
165
|
+
let isRunning = pid ? isProcessRunning(pid) : false;
|
|
166
|
+
let detail = isRunning ? ` (PID: ${pid})` : '';
|
|
167
|
+
|
|
168
|
+
if (!isRunning) {
|
|
169
|
+
const isRunningHttp = await isDaemonRunning(config.host, config.port);
|
|
170
|
+
if (isRunningHttp) {
|
|
171
|
+
isRunning = true;
|
|
172
|
+
detail = ' (Active via MCP Server)';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
145
175
|
|
|
146
176
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
147
|
-
console.log(` Status: ${isRunning ? '🟢 RUNNING' : '⚪ STOPPED'}${
|
|
177
|
+
console.log(` Status: ${isRunning ? '🟢 RUNNING' : '⚪ STOPPED'}${detail}`);
|
|
148
178
|
console.log(` Endpoint: http://${config.host}:${config.port}`);
|
|
149
179
|
console.log(` Database: ${config.dbPath}`);
|
|
150
180
|
|
|
@@ -174,42 +204,54 @@ projectCommand
|
|
|
174
204
|
.requiredOption('-o, --origin <origin>', 'Project origin (e.g. http://localhost:5173)')
|
|
175
205
|
.option('-p, --path <path>', 'Filesystem path (e.g. /path/to/project)')
|
|
176
206
|
.action((name, options) => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
try {
|
|
208
|
+
const config = getDaemonConfig();
|
|
209
|
+
const db = new StorageDB(config.dbPath);
|
|
210
|
+
try {
|
|
211
|
+
const resolvedPath = options.path ? path.resolve(options.path) : undefined;
|
|
212
|
+
const proj = db.upsertProject({
|
|
213
|
+
id: `proj_${name}`,
|
|
214
|
+
name,
|
|
215
|
+
origin: options.origin,
|
|
216
|
+
path: resolvedPath,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.log(`[BrowserTrack] Registered project '${proj.name}':`);
|
|
220
|
+
console.log(` ID: ${proj.id}`);
|
|
221
|
+
console.log(` Origin: ${proj.origin}`);
|
|
222
|
+
console.log(` Path: ${proj.path || '(none)'}`);
|
|
223
|
+
} finally {
|
|
224
|
+
db.close();
|
|
225
|
+
}
|
|
226
|
+
} catch (err: any) {
|
|
227
|
+
console.error('[BrowserTrack] Failed to register project:', err?.message || err);
|
|
228
|
+
}
|
|
193
229
|
});
|
|
194
230
|
|
|
195
231
|
program
|
|
196
232
|
.command('projects')
|
|
197
233
|
.description('List all tracked projects')
|
|
198
234
|
.action(() => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
235
|
+
try {
|
|
236
|
+
const config = getDaemonConfig();
|
|
237
|
+
const db = new StorageDB(config.dbPath);
|
|
238
|
+
try {
|
|
239
|
+
const projects = db.listProjects();
|
|
240
|
+
if (projects.length === 0) {
|
|
241
|
+
console.log('[BrowserTrack] No projects registered yet. Projects will be auto-detected upon browser connection.');
|
|
242
|
+
} else {
|
|
243
|
+
console.log('\nTracked Projects:');
|
|
244
|
+
for (const p of projects) {
|
|
245
|
+
console.log(` • ${p.name.padEnd(16)} | ${p.origin.padEnd(26)} | ${p.path || '(auto-detected)'}`);
|
|
246
|
+
}
|
|
247
|
+
console.log('');
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
db.close();
|
|
209
251
|
}
|
|
210
|
-
|
|
252
|
+
} catch (err: any) {
|
|
253
|
+
console.error('[BrowserTrack] Failed to list projects:', err?.message || err);
|
|
211
254
|
}
|
|
212
|
-
db.close();
|
|
213
255
|
});
|
|
214
256
|
|
|
215
257
|
// 5. ERRORS / INCIDENTS
|
|
@@ -220,34 +262,41 @@ program
|
|
|
220
262
|
.option('-s, --status <status>', 'Filter by status (OPEN, VERIFIED, FAILED, etc.)')
|
|
221
263
|
.option('-l, --limit <number>', 'Limit result count', '20')
|
|
222
264
|
.action((options) => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
inc
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
265
|
+
try {
|
|
266
|
+
const config = getDaemonConfig();
|
|
267
|
+
const db = new StorageDB(config.dbPath);
|
|
268
|
+
try {
|
|
269
|
+
const limit = parseInt(options.limit, 10);
|
|
270
|
+
const incidents = db.listIncidents({
|
|
271
|
+
projectId: options.project,
|
|
272
|
+
status: options.status,
|
|
273
|
+
limit,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (incidents.length === 0) {
|
|
277
|
+
console.log('[BrowserTrack] No incidents found matching the filter.');
|
|
278
|
+
} else {
|
|
279
|
+
console.log(`\nIncidents (${incidents.length}):`);
|
|
280
|
+
for (const inc of incidents) {
|
|
281
|
+
const statusBadge =
|
|
282
|
+
inc.status === 'OPEN'
|
|
283
|
+
? '🔴 OPEN'
|
|
284
|
+
: inc.status === 'VERIFIED'
|
|
285
|
+
? '🟢 VERIFIED'
|
|
286
|
+
: inc.status === 'FAILED'
|
|
287
|
+
? '❌ FAILED'
|
|
288
|
+
: `⚪ ${inc.status}`;
|
|
289
|
+
console.log(` ${inc.id.padEnd(12)} [${statusBadge}] (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
|
|
290
|
+
console.log(` Source: ${inc.source.file}:${inc.source.line} | Route: ${inc.route}`);
|
|
291
|
+
}
|
|
292
|
+
console.log('');
|
|
293
|
+
}
|
|
294
|
+
} finally {
|
|
295
|
+
db.close();
|
|
247
296
|
}
|
|
248
|
-
|
|
297
|
+
} catch (err: any) {
|
|
298
|
+
console.error('[BrowserTrack] Failed to list incidents:', err?.message || err);
|
|
249
299
|
}
|
|
250
|
-
db.close();
|
|
251
300
|
});
|
|
252
301
|
|
|
253
302
|
// 6. VISUAL NOTES
|
|
@@ -257,63 +306,81 @@ noteCmd
|
|
|
257
306
|
.command('show <noteId>')
|
|
258
307
|
.description('Show full context for a specific visual note')
|
|
259
308
|
.action((noteId) => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
309
|
+
try {
|
|
310
|
+
const config = getDaemonConfig();
|
|
311
|
+
const db = new StorageDB(config.dbPath);
|
|
312
|
+
try {
|
|
313
|
+
const note = db.getNote(noteId);
|
|
314
|
+
if (!note) {
|
|
315
|
+
console.log(`[BrowserTrack] Note '${noteId}' not found.`);
|
|
316
|
+
} else {
|
|
317
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
318
|
+
console.log(` 📝 Visual Note: ${note.id} [${note.status}]`);
|
|
319
|
+
console.log(` 📍 Route: ${note.route} (${note.url})`);
|
|
320
|
+
console.log(` 📐 Viewport: ${note.viewport.width} × ${note.viewport.height} (dpr: ${note.viewport.devicePixelRatio})`);
|
|
321
|
+
if (note.target) {
|
|
322
|
+
console.log(` 🎯 Target: ${note.target.selector}`);
|
|
323
|
+
console.log(` Bounds: x:${note.target.boundingRect.x}, y:${note.target.boundingRect.y}, ${note.target.boundingRect.width}×${note.target.boundingRect.height}`);
|
|
324
|
+
}
|
|
325
|
+
console.log(` 💬 Note: "${note.message}"`);
|
|
326
|
+
if (note.screenshots?.original) {
|
|
327
|
+
console.log(` 🖼️ Screenshot: ${note.screenshots.original}`);
|
|
328
|
+
}
|
|
329
|
+
console.log(` 🕒 Created: ${note.createdAt}`);
|
|
330
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
db.close();
|
|
278
334
|
}
|
|
279
|
-
|
|
280
|
-
console.
|
|
335
|
+
} catch (err: any) {
|
|
336
|
+
console.error('[BrowserTrack] Failed to retrieve note:', err?.message || err);
|
|
281
337
|
}
|
|
282
|
-
db.close();
|
|
283
338
|
});
|
|
284
339
|
|
|
285
340
|
noteCmd
|
|
286
341
|
.command('resolve <noteId>')
|
|
287
342
|
.description('Mark a visual note as resolved')
|
|
288
343
|
.action((noteId) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
344
|
+
try {
|
|
345
|
+
const config = getDaemonConfig();
|
|
346
|
+
const db = new StorageDB(config.dbPath);
|
|
347
|
+
try {
|
|
348
|
+
const note = db.getNote(noteId);
|
|
349
|
+
if (!note) {
|
|
350
|
+
console.log(`[BrowserTrack] Note '${noteId}' not found.`);
|
|
351
|
+
} else {
|
|
352
|
+
db.updateNoteStatus(noteId, 'RESOLVED');
|
|
353
|
+
console.log(`[BrowserTrack] Marked note '${noteId}' as RESOLVED.`);
|
|
354
|
+
}
|
|
355
|
+
} finally {
|
|
356
|
+
db.close();
|
|
357
|
+
}
|
|
358
|
+
} catch (err: any) {
|
|
359
|
+
console.error('[BrowserTrack] Failed to resolve note:', err?.message || err);
|
|
298
360
|
}
|
|
299
|
-
db.close();
|
|
300
361
|
});
|
|
301
362
|
|
|
302
363
|
noteCmd
|
|
303
364
|
.command('reopen <noteId>')
|
|
304
365
|
.description('Reopen a visual note')
|
|
305
366
|
.action((noteId) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
367
|
+
try {
|
|
368
|
+
const config = getDaemonConfig();
|
|
369
|
+
const db = new StorageDB(config.dbPath);
|
|
370
|
+
try {
|
|
371
|
+
const note = db.getNote(noteId);
|
|
372
|
+
if (!note) {
|
|
373
|
+
console.log(`[BrowserTrack] Note '${noteId}' not found.`);
|
|
374
|
+
} else {
|
|
375
|
+
db.updateNoteStatus(noteId, 'OPEN');
|
|
376
|
+
console.log(`[BrowserTrack] Reopened note '${noteId}' (status: OPEN).`);
|
|
377
|
+
}
|
|
378
|
+
} finally {
|
|
379
|
+
db.close();
|
|
380
|
+
}
|
|
381
|
+
} catch (err: any) {
|
|
382
|
+
console.error('[BrowserTrack] Failed to reopen note:', err?.message || err);
|
|
315
383
|
}
|
|
316
|
-
db.close();
|
|
317
384
|
});
|
|
318
385
|
|
|
319
386
|
program
|
|
@@ -323,27 +390,34 @@ program
|
|
|
323
390
|
.option('-s, --status <status>', 'Filter by status (OPEN, RESOLVED, etc.)')
|
|
324
391
|
.option('-l, --limit <number>', 'Limit result count', '20')
|
|
325
392
|
.action((options) => {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
393
|
+
try {
|
|
394
|
+
const config = getDaemonConfig();
|
|
395
|
+
const db = new StorageDB(config.dbPath);
|
|
396
|
+
try {
|
|
397
|
+
const limit = parseInt(options.limit, 10);
|
|
398
|
+
const notes = db.listNotes({
|
|
399
|
+
projectId: options.project,
|
|
400
|
+
status: options.status,
|
|
401
|
+
limit,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (notes.length === 0) {
|
|
405
|
+
console.log('[BrowserTrack] No visual notes found.');
|
|
406
|
+
} else {
|
|
407
|
+
console.log(`\nVisual Notes (${notes.length}):`);
|
|
408
|
+
for (const n of notes) {
|
|
409
|
+
const badge = n.status === 'OPEN' ? '🟡 OPEN' : n.status === 'RESOLVED' ? '🟢 RESOLVED' : `⚪ ${n.status}`;
|
|
410
|
+
console.log(` ${n.id.padEnd(12)} [${badge}] Route: ${n.route.padEnd(16)} | Target: ${n.target?.selector || n.type}`);
|
|
411
|
+
console.log(` Note: "${n.message}" (${n.viewport.width}×${n.viewport.height})`);
|
|
412
|
+
}
|
|
413
|
+
console.log('');
|
|
414
|
+
}
|
|
415
|
+
} finally {
|
|
416
|
+
db.close();
|
|
343
417
|
}
|
|
344
|
-
|
|
418
|
+
} catch (err: any) {
|
|
419
|
+
console.error('[BrowserTrack] Failed to list visual notes:', err?.message || err);
|
|
345
420
|
}
|
|
346
|
-
db.close();
|
|
347
421
|
});
|
|
348
422
|
|
|
349
423
|
// 7. INBOX (Errors + Visual Notes)
|
|
@@ -352,36 +426,43 @@ program
|
|
|
352
426
|
.description('View combined developer inbox with active runtime errors and visual notes')
|
|
353
427
|
.option('-p, --project <project>', 'Filter by project name or ID')
|
|
354
428
|
.action((options) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
429
|
+
try {
|
|
430
|
+
const config = getDaemonConfig();
|
|
431
|
+
const db = new StorageDB(config.dbPath);
|
|
432
|
+
try {
|
|
433
|
+
const incidents = db.listIncidents({ projectId: options.project, status: 'OPEN', limit: 20 });
|
|
434
|
+
const notes = db.listNotes({ projectId: options.project, status: 'OPEN', limit: 20 });
|
|
435
|
+
|
|
436
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
437
|
+
console.log(` 📥 Browser Development Inbox ${options.project ? `(${options.project})` : ''}`);
|
|
438
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
439
|
+
|
|
440
|
+
if (incidents.length === 0 && notes.length === 0) {
|
|
441
|
+
console.log(' ✨ All clear! No open errors or visual notes.');
|
|
442
|
+
} else {
|
|
443
|
+
if (incidents.length > 0) {
|
|
444
|
+
console.log(`\n 🚨 Runtime Errors (${incidents.length}):`);
|
|
445
|
+
for (const inc of incidents) {
|
|
446
|
+
console.log(` • ${inc.id} (${inc.occurrences}x) ${inc.type}: ${inc.message}`);
|
|
447
|
+
console.log(` Route: ${inc.route} | Source: ${inc.source.file}:${inc.source.line}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (notes.length > 0) {
|
|
452
|
+
console.log(`\n 📝 Visual Notes (${notes.length}):`);
|
|
453
|
+
for (const n of notes) {
|
|
454
|
+
console.log(` • ${n.id} on ${n.route} (${n.viewport.width}×${n.viewport.height})`);
|
|
455
|
+
console.log(` Target: ${n.target?.selector || n.type} | Note: "${n.message}"`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
380
458
|
}
|
|
459
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
460
|
+
} finally {
|
|
461
|
+
db.close();
|
|
381
462
|
}
|
|
463
|
+
} catch (err: any) {
|
|
464
|
+
console.error('[BrowserTrack] Failed to display inbox:', err?.message || err);
|
|
382
465
|
}
|
|
383
|
-
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
384
|
-
db.close();
|
|
385
466
|
});
|
|
386
467
|
|
|
387
468
|
// 6. CLEAR
|
|
@@ -389,20 +470,35 @@ program
|
|
|
389
470
|
.command('clear')
|
|
390
471
|
.description('Clear all stored incidents, events, and sessions from the database')
|
|
391
472
|
.action(() => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
473
|
+
try {
|
|
474
|
+
const config = getDaemonConfig();
|
|
475
|
+
const db = new StorageDB(config.dbPath);
|
|
476
|
+
try {
|
|
477
|
+
db.clearAll();
|
|
478
|
+
console.log('[BrowserTrack] Database cleared.');
|
|
479
|
+
} finally {
|
|
480
|
+
db.close();
|
|
481
|
+
}
|
|
482
|
+
} catch (err: any) {
|
|
483
|
+
console.error('[BrowserTrack] Failed to clear database:', err?.message || err);
|
|
484
|
+
}
|
|
397
485
|
});
|
|
398
486
|
|
|
399
487
|
// 7. MCP SERVER
|
|
400
488
|
program
|
|
401
489
|
.command('mcp')
|
|
402
490
|
.description('Launch the Model Context Protocol (MCP) server over stdio')
|
|
403
|
-
.
|
|
491
|
+
.option('--no-daemon', 'Do not auto-start background daemon if offline')
|
|
492
|
+
.action(async (options) => {
|
|
404
493
|
try {
|
|
405
|
-
|
|
494
|
+
// In MCP stdio mode, redirect console.log to console.error to preserve JSON-RPC protocol on stdout
|
|
495
|
+
console.log = (...args: any[]) => {
|
|
496
|
+
console.error(...args);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const server = createMcpServer({
|
|
500
|
+
autoStartDaemon: options.daemon !== false,
|
|
501
|
+
});
|
|
406
502
|
await server.startStdio();
|
|
407
503
|
} catch (err: any) {
|
|
408
504
|
console.error('[BrowserTrack] MCP Server error:', err?.message);
|
|
@@ -9,14 +9,18 @@ export type NavigationCallback = (event: NavigationEvent) => void;
|
|
|
9
9
|
export function setupNavigationInterceptors(onNavigation: NavigationCallback): () => void {
|
|
10
10
|
if (typeof window === 'undefined' || typeof history === 'undefined') return () => {};
|
|
11
11
|
|
|
12
|
-
let currentUrl =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
let currentUrl = '';
|
|
13
|
+
try {
|
|
14
|
+
currentUrl = redactUrl(window.location.href);
|
|
15
|
+
// Initial navigation
|
|
16
|
+
onNavigation({
|
|
17
|
+
to: currentUrl,
|
|
18
|
+
type: 'initial',
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
// Defensive
|
|
23
|
+
}
|
|
20
24
|
|
|
21
25
|
const originalPushState = history.pushState;
|
|
22
26
|
const originalReplaceState = history.replaceState;
|
|
@@ -58,27 +62,35 @@ export function setupNavigationInterceptors(onNavigation: NavigationCallback): (
|
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
const onPopState = () => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
try {
|
|
66
|
+
const from = currentUrl;
|
|
67
|
+
const to = redactUrl(window.location.href);
|
|
68
|
+
currentUrl = to;
|
|
69
|
+
onNavigation({
|
|
70
|
+
from,
|
|
71
|
+
to,
|
|
72
|
+
type: 'popstate',
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
// Defensive
|
|
77
|
+
}
|
|
70
78
|
};
|
|
71
79
|
|
|
72
80
|
const onHashChange = () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
try {
|
|
82
|
+
const from = currentUrl;
|
|
83
|
+
const to = redactUrl(window.location.href);
|
|
84
|
+
currentUrl = to;
|
|
85
|
+
onNavigation({
|
|
86
|
+
from,
|
|
87
|
+
to,
|
|
88
|
+
type: 'hashchange',
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
// Defensive
|
|
93
|
+
}
|
|
82
94
|
};
|
|
83
95
|
|
|
84
96
|
window.addEventListener('popstate', onPopState);
|
|
@@ -37,35 +37,40 @@ export function setupNetworkInterceptors(onNetwork: NetworkCallback): () => void
|
|
|
37
37
|
|
|
38
38
|
const safeUrl = redactUrl(urlStr);
|
|
39
39
|
|
|
40
|
+
let response: Response;
|
|
40
41
|
try {
|
|
41
|
-
|
|
42
|
-
const durationMs = Date.now() - startTime;
|
|
43
|
-
|
|
44
|
-
onNetwork({
|
|
45
|
-
url: safeUrl,
|
|
46
|
-
method,
|
|
47
|
-
status: response.status,
|
|
48
|
-
statusText: response.statusText,
|
|
49
|
-
durationMs,
|
|
50
|
-
timestamp: startTime,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
return response;
|
|
42
|
+
response = await originalFetch.apply(window, [input as any, init]);
|
|
54
43
|
} catch (err: any) {
|
|
55
44
|
const durationMs = Date.now() - startTime;
|
|
56
45
|
const isAbort = err?.name === 'AbortError';
|
|
57
46
|
|
|
47
|
+
try {
|
|
48
|
+
onNetwork({
|
|
49
|
+
url: safeUrl,
|
|
50
|
+
method,
|
|
51
|
+
durationMs,
|
|
52
|
+
error: err?.message || 'Network request failed',
|
|
53
|
+
aborted: isAbort,
|
|
54
|
+
timestamp: startTime,
|
|
55
|
+
});
|
|
56
|
+
} catch {}
|
|
57
|
+
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const durationMs = Date.now() - startTime;
|
|
62
|
+
try {
|
|
58
63
|
onNetwork({
|
|
59
64
|
url: safeUrl,
|
|
60
65
|
method,
|
|
66
|
+
status: response.status,
|
|
67
|
+
statusText: response.statusText,
|
|
61
68
|
durationMs,
|
|
62
|
-
error: err?.message || 'Network request failed',
|
|
63
|
-
aborted: isAbort,
|
|
64
69
|
timestamp: startTime,
|
|
65
70
|
});
|
|
71
|
+
} catch {}
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
}
|
|
73
|
+
return response;
|
|
69
74
|
};
|
|
70
75
|
|
|
71
76
|
cleanups.push(() => {
|