browsertrack 0.0.1 → 0.1.1

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 (110) hide show
  1. package/.github/workflows/docs.yml +54 -0
  2. package/AGENTS.md +133 -0
  3. package/README.md +143 -0
  4. package/dist/chunk-6VA7GBAO.js +246 -0
  5. package/dist/chunk-6VA7GBAO.js.map +1 -0
  6. package/dist/chunk-G2Y3CXCY.js +659 -0
  7. package/dist/chunk-G2Y3CXCY.js.map +1 -0
  8. package/dist/chunk-ILRYKMME.js +1228 -0
  9. package/dist/chunk-ILRYKMME.js.map +1 -0
  10. package/dist/chunk-SKCMT2DE.js +2641 -0
  11. package/dist/chunk-SKCMT2DE.js.map +1 -0
  12. package/dist/chunk-SPCIROIU.js +616 -0
  13. package/dist/chunk-SPCIROIU.js.map +1 -0
  14. package/dist/cli/index.js +2942 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/client/index.cjs +2780 -0
  17. package/dist/client/index.d.ts +211 -0
  18. package/dist/client/index.js +20 -0
  19. package/dist/client/index.js.map +1 -0
  20. package/dist/client.iife.js +630 -0
  21. package/dist/core/index.d.ts +86 -0
  22. package/dist/core/index.js +37 -0
  23. package/dist/core/index.js.map +1 -0
  24. package/dist/daemon/index.d.ts +58 -0
  25. package/dist/daemon/index.js +32 -0
  26. package/dist/daemon/index.js.map +1 -0
  27. package/dist/engine-CeT9URuN.d.ts +161 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.js +56 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/mcp/index.d.ts +11 -0
  32. package/dist/mcp/index.js +12 -0
  33. package/dist/mcp/index.js.map +1 -0
  34. package/dist/notes-CBvN91Wf.d.ts +260 -0
  35. package/dist/projects-CY8ungMt.d.ts +99 -0
  36. package/dist/server-Dd8NX2Mk.d.ts +55 -0
  37. package/docboot.config.js +16 -0
  38. package/docs/cli.md +51 -0
  39. package/docs/closed-loop-verification.md +57 -0
  40. package/docs/getting-started.md +122 -0
  41. package/docs/incidents-diagnostics.md +61 -0
  42. package/docs/index.md +46 -0
  43. package/docs/mcp-reference.md +128 -0
  44. package/docs/security-privacy.md +32 -0
  45. package/docs/visual-notes.md +67 -0
  46. package/examples/test-app/index.html +299 -0
  47. package/package.json +56 -7
  48. package/packages/cli/src/index.ts +413 -0
  49. package/packages/client/package.json +28 -0
  50. package/packages/client/src/breadcrumbs.ts +30 -0
  51. package/packages/client/src/client.ts +278 -0
  52. package/packages/client/src/commands/handler.ts +301 -0
  53. package/packages/client/src/config.ts +37 -0
  54. package/packages/client/src/index.ts +50 -0
  55. package/packages/client/src/interceptors/console.ts +68 -0
  56. package/packages/client/src/interceptors/interaction.ts +119 -0
  57. package/packages/client/src/interceptors/navigation.ts +93 -0
  58. package/packages/client/src/interceptors/network.ts +165 -0
  59. package/packages/client/src/interceptors/runtime.ts +65 -0
  60. package/packages/client/src/notes/inspector.ts +1553 -0
  61. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  62. package/packages/client/src/screenshot/driver.ts +14 -0
  63. package/packages/client/src/transport/websocket.ts +194 -0
  64. package/packages/client/tsconfig.json +8 -0
  65. package/packages/core/package.json +26 -0
  66. package/packages/core/src/fingerprint.ts +120 -0
  67. package/packages/core/src/index.ts +9 -0
  68. package/packages/core/src/redaction.ts +174 -0
  69. package/packages/core/src/selector.ts +77 -0
  70. package/packages/core/src/types/commands.ts +101 -0
  71. package/packages/core/src/types/events.ts +108 -0
  72. package/packages/core/src/types/incidents.ts +54 -0
  73. package/packages/core/src/types/notes.ts +106 -0
  74. package/packages/core/src/types/probes.ts +44 -0
  75. package/packages/core/src/types/projects.ts +20 -0
  76. package/packages/core/tsconfig.json +8 -0
  77. package/packages/daemon/src/config.ts +29 -0
  78. package/packages/daemon/src/incidents/engine.ts +211 -0
  79. package/packages/daemon/src/index.ts +18 -0
  80. package/packages/daemon/src/notes/engine.ts +92 -0
  81. package/packages/daemon/src/notes/verification.ts +191 -0
  82. package/packages/daemon/src/server/daemon.ts +112 -0
  83. package/packages/daemon/src/server/http.ts +165 -0
  84. package/packages/daemon/src/server/ws.ts +246 -0
  85. package/packages/daemon/src/session/manager.ts +147 -0
  86. package/packages/daemon/src/storage/db.ts +742 -0
  87. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  88. package/packages/daemon/src/verification/engine.ts +264 -0
  89. package/packages/mcp/src/handlers.ts +398 -0
  90. package/packages/mcp/src/index.ts +3 -0
  91. package/packages/mcp/src/server.ts +86 -0
  92. package/packages/mcp/src/tools.ts +236 -0
  93. package/src/index.ts +4 -0
  94. package/test/client/interceptors.test.ts +130 -0
  95. package/test/core/fingerprint.test.ts +53 -0
  96. package/test/core/notes.test.ts +66 -0
  97. package/test/core/redaction.test.ts +60 -0
  98. package/test/daemon/incident-engine.test.ts +98 -0
  99. package/test/daemon/notes-storage.test.ts +130 -0
  100. package/test/daemon/notes-verification.test.ts +135 -0
  101. package/test/daemon/storage.test.ts +123 -0
  102. package/test/daemon/verification-engine.test.ts +88 -0
  103. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  104. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  105. package/test/mcp/handlers.test.ts +116 -0
  106. package/test/mcp/notes.test.ts +107 -0
  107. package/tsconfig.base.json +17 -0
  108. package/tsconfig.json +26 -0
  109. package/tsup.config.ts +54 -0
  110. package/vitest.config.ts +9 -0
@@ -0,0 +1,153 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import WebSocket from 'ws';
6
+ import { createDaemon } from '../../packages/daemon/src/index.js';
7
+ import { handleToolCall } from '../../packages/mcp/src/handlers.js';
8
+
9
+ describe('BrowserTrack Full E2E Lifecycle (Daemon + WebSocket Client + MCP)', () => {
10
+ let tempDir: string;
11
+ let daemon: ReturnType<typeof createDaemon>;
12
+ let wsClient: WebSocket | null = null;
13
+ const testPort = 7339;
14
+
15
+ beforeEach(async () => {
16
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bt_e2e_'));
17
+ const dbPath = path.join(tempDir, 'e2e.db');
18
+ const screenshotsDir = path.join(tempDir, 'screenshots');
19
+
20
+ daemon = createDaemon({
21
+ port: testPort,
22
+ host: '127.0.0.1',
23
+ dbPath,
24
+ screenshotsDir,
25
+ verbose: false,
26
+ });
27
+
28
+ await daemon.start();
29
+ });
30
+
31
+ afterEach(async () => {
32
+ if (wsClient) {
33
+ wsClient.close();
34
+ wsClient = null;
35
+ }
36
+ await daemon.stop();
37
+ fs.rmSync(tempDir, { recursive: true, force: true });
38
+ });
39
+
40
+ it('should complete full cycle: handshake -> error ingestion -> incident creation -> MCP tool query -> command dispatch', async () => {
41
+ let receivedSessionId = '';
42
+
43
+ // 1. Connect WebSocket client (simulating browser)
44
+ wsClient = new WebSocket(`ws://127.0.0.1:${testPort}`);
45
+
46
+ await new Promise<void>((resolve) => {
47
+ wsClient!.on('open', () => {
48
+ // Send hello
49
+ wsClient!.send(
50
+ JSON.stringify({
51
+ type: 'hello',
52
+ origin: 'http://localhost:5173',
53
+ url: 'http://localhost:5173/products/123',
54
+ title: 'Product Details',
55
+ userAgent: 'Mozilla/5.0 TestBrowser',
56
+ timestamp: Date.now(),
57
+ })
58
+ );
59
+ });
60
+
61
+ wsClient!.on('message', (raw: string | Buffer) => {
62
+ const msg = JSON.parse(raw.toString());
63
+ if (msg.type === 'hello_ack') {
64
+ receivedSessionId = msg.sessionId;
65
+ resolve();
66
+ }
67
+ });
68
+ });
69
+
70
+ expect(receivedSessionId.startsWith('sess_')).toBe(true);
71
+
72
+ // 2. Browser client triggers runtime error
73
+ wsClient.send(
74
+ JSON.stringify({
75
+ type: 'event',
76
+ sessionId: receivedSessionId,
77
+ eventType: 'runtime_error',
78
+ payload: {
79
+ errorType: 'TypeError',
80
+ message: 'Cannot read property price of undefined',
81
+ filename: 'http://localhost:5173/src/components/ProductCard.tsx',
82
+ lineno: 45,
83
+ colno: 12,
84
+ timestamp: Date.now(),
85
+ },
86
+ breadcrumbs: [
87
+ { type: 'navigation', message: 'navigate /products/123', timestamp: Date.now() - 200 },
88
+ { type: 'click', message: 'click button.add-to-cart', timestamp: Date.now() - 100 },
89
+ ],
90
+ lastElement: {
91
+ selector: 'button.add-to-cart',
92
+ tag: 'button',
93
+ visible: true,
94
+ innerText: 'Add to Cart',
95
+ },
96
+ route: '/products/123',
97
+ url: 'http://localhost:5173/products/123',
98
+ timestamp: Date.now(),
99
+ })
100
+ );
101
+
102
+ // Wait a brief tick for SQLite write
103
+ await new Promise((r) => setTimeout(r, 100));
104
+
105
+ // 3. MCP query: list_incidents
106
+ const listRes = await handleToolCall('list_incidents', {}, { db: daemon.db });
107
+ expect(listRes.total).toBe(1);
108
+ const incidentId = listRes.incidents[0].id;
109
+ expect(incidentId.startsWith('inc_')).toBe(true);
110
+ expect(listRes.incidents[0].message).toBe('Cannot read property price of undefined');
111
+
112
+ // 4. MCP query: get_incident
113
+ const getRes = await handleToolCall('get_incident', { incidentId }, { db: daemon.db });
114
+ expect(getRes.id).toBe(incidentId);
115
+ expect(getRes.source.file).toBe('/src/components/ProductCard.tsx');
116
+ expect(getRes.source.line).toBe(45);
117
+ expect(getRes.lastInteractedElement?.selector).toBe('button.add-to-cart');
118
+ expect(getRes.recentBreadcrumbs.length).toBe(2);
119
+
120
+ // 5. Test Live Command: get_page_state via daemon session manager
121
+ // Setup client listener to reply to get_page_state command
122
+ wsClient.on('message', (raw: string | Buffer) => {
123
+ const msg = JSON.parse(raw.toString());
124
+ if (msg.type === 'command' && msg.command.type === 'get_page_state') {
125
+ wsClient!.send(
126
+ JSON.stringify({
127
+ type: 'command_response',
128
+ sessionId: receivedSessionId,
129
+ response: {
130
+ id: msg.command.id,
131
+ ok: true,
132
+ result: {
133
+ url: 'http://localhost:5173/products/123',
134
+ route: '/products/123',
135
+ title: 'Product Details',
136
+ readyState: 'complete',
137
+ },
138
+ },
139
+ })
140
+ );
141
+ }
142
+ });
143
+
144
+ const pageState = await handleToolCall(
145
+ 'get_page_state',
146
+ { sessionId: receivedSessionId },
147
+ { db: daemon.db, sessionManager: daemon.sessionManager }
148
+ );
149
+
150
+ expect(pageState.url).toBe('http://localhost:5173/products/123');
151
+ expect(pageState.title).toBe('Product Details');
152
+ });
153
+ });
@@ -0,0 +1,205 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { WebSocket } from 'ws';
3
+ import { BrowserTrackDaemon } from '../../packages/daemon/src/server/daemon.js';
4
+ import { handleToolCall } from '../../packages/mcp/src/handlers.js';
5
+
6
+ describe('E2E Visual Notes Flow (Client -> Daemon -> MCP -> Verification -> Resolution)', () => {
7
+ let daemon: BrowserTrackDaemon;
8
+ const testPort = 7345;
9
+ let clientWs: WebSocket;
10
+ let assignedSessionId = '';
11
+
12
+ beforeAll(async () => {
13
+ daemon = new BrowserTrackDaemon({
14
+ port: testPort,
15
+ host: '127.0.0.1',
16
+ dbPath: ':memory:',
17
+ screenshotsDir: '/tmp/test-e2e-notes-screenshots',
18
+ verbose: false,
19
+ });
20
+ await daemon.start();
21
+
22
+ // Connect WebSocket simulating browser client
23
+ await new Promise<void>((resolve, reject) => {
24
+ clientWs = new WebSocket(`ws://127.0.0.1:${testPort}`);
25
+ clientWs.on('open', () => {
26
+ clientWs.send(
27
+ JSON.stringify({
28
+ type: 'hello',
29
+ origin: 'http://localhost:5173',
30
+ url: 'http://localhost:5173/dashboard',
31
+ title: 'Analytics Dashboard',
32
+ })
33
+ );
34
+ });
35
+
36
+ clientWs.on('message', (raw) => {
37
+ const msg = JSON.parse(raw.toString());
38
+ if (msg.type === 'hello_ack') {
39
+ assignedSessionId = msg.sessionId;
40
+ resolve();
41
+ }
42
+
43
+ // Handle daemon command dispatches during verify_note
44
+ if (msg.type === 'command') {
45
+ const cmd = msg.command;
46
+ if (cmd.type === 'get_page_state') {
47
+ clientWs.send(
48
+ JSON.stringify({
49
+ type: 'command_response',
50
+ sessionId: assignedSessionId,
51
+ response: {
52
+ id: cmd.id,
53
+ ok: true,
54
+ result: {
55
+ url: 'http://localhost:5173/dashboard',
56
+ route: '/dashboard',
57
+ title: 'Analytics Dashboard',
58
+ readyState: 'complete',
59
+ },
60
+ },
61
+ })
62
+ );
63
+ } else if (cmd.type === 'query_element') {
64
+ clientWs.send(
65
+ JSON.stringify({
66
+ type: 'command_response',
67
+ sessionId: assignedSessionId,
68
+ response: {
69
+ id: cmd.id,
70
+ ok: true,
71
+ result: {
72
+ exists: true,
73
+ visible: true,
74
+ boundingRect: { x: 20, y: 80, width: 350, height: 160, top: 80, left: 20, bottom: 240, right: 370 },
75
+ },
76
+ },
77
+ })
78
+ );
79
+ } else if (cmd.type === 'check_overflow') {
80
+ clientWs.send(
81
+ JSON.stringify({
82
+ type: 'command_response',
83
+ sessionId: assignedSessionId,
84
+ response: {
85
+ id: cmd.id,
86
+ ok: true,
87
+ result: {
88
+ selector: '#stats-banner',
89
+ overflow: false, // Overflow fixed!
90
+ viewportWidth: 390,
91
+ viewportHeight: 844,
92
+ overflowRightPx: 0,
93
+ overflowBottomPx: 0,
94
+ rect: { x: 20, y: 80, width: 350, height: 160, top: 80, left: 20, bottom: 240, right: 370 },
95
+ },
96
+ },
97
+ })
98
+ );
99
+ } else if (cmd.type === 'capture_element') {
100
+ clientWs.send(
101
+ JSON.stringify({
102
+ type: 'command_response',
103
+ sessionId: assignedSessionId,
104
+ response: {
105
+ id: cmd.id,
106
+ ok: true,
107
+ result: {
108
+ format: 'webp',
109
+ dataUrl: 'data:image/webp;base64,UklGRmYAAABXRUJQVlA4IFoAAAAwAQCdASoBAAEAD8D+JaQAA3AA/ua2wAAA',
110
+ width: 350,
111
+ height: 160,
112
+ },
113
+ },
114
+ })
115
+ );
116
+ }
117
+ }
118
+ });
119
+
120
+ clientWs.on('error', reject);
121
+ });
122
+ });
123
+
124
+ afterAll(async () => {
125
+ clientWs?.close();
126
+ await daemon.stop();
127
+ });
128
+
129
+ it('runs complete lifecycle of a visual note', async () => {
130
+ // 1. Client creates a note on overflowing stats card
131
+ const noteCreatedPromise = new Promise<string>((resolve) => {
132
+ const listener = (raw: any) => {
133
+ const msg = JSON.parse(raw.toString());
134
+ if (msg.type === 'note_created_ack') {
135
+ clientWs.removeListener('message', listener);
136
+ resolve(msg.noteId);
137
+ }
138
+ };
139
+ clientWs.on('message', listener);
140
+ });
141
+
142
+ clientWs.send(
143
+ JSON.stringify({
144
+ type: 'create_note',
145
+ sessionId: assignedSessionId,
146
+ noteType: 'element',
147
+ message: "Mobile'da buraya bak, sağa taşıyor",
148
+ route: '/dashboard',
149
+ url: 'http://localhost:5173/dashboard',
150
+ viewport: { width: 390, height: 844, devicePixelRatio: 3 },
151
+ scroll: { scrollX: 0, scrollY: 0 },
152
+ target: {
153
+ selector: '#stats-banner',
154
+ boundingRect: { x: 20, y: 80, width: 440, height: 160, top: 80, left: 20, bottom: 240, right: 460 },
155
+ visible: true,
156
+ confidence: 'high',
157
+ },
158
+ elementContext: {
159
+ selector: '#stats-banner',
160
+ tag: 'div',
161
+ attributes: { id: 'stats-banner' },
162
+ outerHTML: '<div id="stats-banner"><input type="password" value="[REDACTED]" /></div>',
163
+ },
164
+ screenshot: 'data:image/webp;base64,UklGRmYAAABXRUJQVlA4IFoAAAAwAQCdASoBAAEAD8D+JaQAA3AA/ua2wAAA',
165
+ })
166
+ );
167
+
168
+ const noteId = await noteCreatedPromise;
169
+ expect(noteId).toMatch(/^note_/);
170
+
171
+ // 2. MCP list_notes returns the note
172
+ const mcpCtx = {
173
+ db: daemon.db,
174
+ sessionManager: daemon.sessionManager,
175
+ noteVerificationEngine: daemon.noteVerificationEngine,
176
+ };
177
+
178
+ const listRes = await handleToolCall('list_notes', {}, mcpCtx);
179
+ expect(listRes.total).toBe(1);
180
+ expect(listRes.notes[0].id).toBe(noteId);
181
+ expect(listRes.notes[0].status).toBe('OPEN');
182
+ expect(listRes.notes[0].target).toBe('#stats-banner');
183
+
184
+ // 3. MCP get_note returns sanitized DOM and screenshot
185
+ const getRes = await handleToolCall('get_note', { noteId }, mcpCtx);
186
+ expect(getRes.id).toBe(noteId);
187
+ expect(getRes.elementContext.outerHTML).toContain('[REDACTED]');
188
+ expect(getRes.screenshot.available).toBe(true);
189
+
190
+ // 4. Verify note with layout probes (checks element & viewport overflow)
191
+ const verifyRes = await handleToolCall('verify_note', { noteId, observationWindowMs: 50 }, mcpCtx);
192
+ expect(verifyRes.noteId).toBe(noteId);
193
+ expect(verifyRes.status).toBe('VERIFIED');
194
+ expect(verifyRes.geometryDiff.overflowFixed).toBe(true);
195
+ expect(verifyRes.screenshots.after).toBeDefined();
196
+
197
+ // 5. MCP resolve_note marks note as RESOLVED
198
+ const resolveRes = await handleToolCall('resolve_note', { noteId }, mcpCtx);
199
+ expect(resolveRes.ok).toBe(true);
200
+ expect(resolveRes.status).toBe('RESOLVED');
201
+
202
+ const finalNote = daemon.db.getNote(noteId);
203
+ expect(finalNote?.status).toBe('RESOLVED');
204
+ });
205
+ });
@@ -0,0 +1,116 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { StorageDB } from '../../packages/daemon/src/storage/db.js';
6
+ import { handleToolCall } from '../../packages/mcp/src/handlers.js';
7
+
8
+ describe('MCP Tool Handlers', () => {
9
+ let tempDir: string;
10
+ let db: StorageDB;
11
+
12
+ beforeEach(() => {
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bt_mcp_test_'));
14
+ const dbPath = path.join(tempDir, 'test.db');
15
+ db = new StorageDB(dbPath);
16
+
17
+ // Seed test data
18
+ db.upsertProject({
19
+ id: 'proj_mcp',
20
+ name: 'mcp-app',
21
+ origin: 'http://localhost:3000',
22
+ path: '/Users/test/mcp-app',
23
+ });
24
+
25
+ db.upsertSession({
26
+ id: 'sess_mcp_1',
27
+ projectId: 'proj_mcp',
28
+ origin: 'http://localhost:3000',
29
+ url: 'http://localhost:3000/users',
30
+ title: 'Users List',
31
+ userAgent: 'Chrome/120',
32
+ connectedAt: new Date().toISOString(),
33
+ lastSeenAt: new Date().toISOString(),
34
+ active: true,
35
+ });
36
+
37
+ db.insertIncident({
38
+ id: 'inc_42',
39
+ projectId: 'proj_mcp',
40
+ sessionId: 'sess_mcp_1',
41
+ type: 'TypeError',
42
+ severity: 'error',
43
+ message: "Cannot read properties of undefined (reading 'avatar')",
44
+ source: { file: '/src/components/UserAvatar.tsx', line: 18, column: 5 },
45
+ fingerprint: 'fp_mcp_user_avatar',
46
+ route: '/users/42',
47
+ firstSeen: new Date().toISOString(),
48
+ lastSeen: new Date().toISOString(),
49
+ occurrences: 4,
50
+ status: 'OPEN',
51
+ stack: 'TypeError: Cannot read properties...\n at UserAvatar (/src/components/UserAvatar.tsx:18:5)',
52
+ breadcrumbs: [
53
+ { type: 'navigation', message: 'navigate /users', timestamp: Date.now() - 1000 },
54
+ { type: 'click', message: 'click button[data-testid="user-42"]', timestamp: Date.now() - 500 },
55
+ ],
56
+ networkFailures: [
57
+ { url: '/api/users/42', method: 'GET', status: 500, durationMs: 120, timestamp: Date.now() - 300 },
58
+ ],
59
+ lastElement: {
60
+ selector: '[data-testid="user-42"]',
61
+ tag: 'button',
62
+ visible: true,
63
+ innerText: 'View Profile',
64
+ },
65
+ });
66
+
67
+ db.insertEvent({
68
+ id: 'evt_log_1',
69
+ sessionId: 'sess_mcp_1',
70
+ eventType: 'console',
71
+ payload: { level: 'error', message: 'Failed to load user image' },
72
+ timestamp: Date.now(),
73
+ route: '/users/42',
74
+ });
75
+ });
76
+
77
+ afterEach(() => {
78
+ db.close();
79
+ fs.rmSync(tempDir, { recursive: true, force: true });
80
+ });
81
+
82
+ it('should handle list_projects', async () => {
83
+ const res = await handleToolCall('list_projects', {}, { db });
84
+ expect(res.projects.length).toBe(1);
85
+ expect(res.projects[0].name).toBe('mcp-app');
86
+ });
87
+
88
+ it('should handle list_incidents', async () => {
89
+ const res = await handleToolCall('list_incidents', { limit: 10 }, { db });
90
+ expect(res.total).toBe(1);
91
+ expect(res.incidents[0].id).toBe('inc_42');
92
+ expect(res.incidents[0].occurrences).toBe(4);
93
+ });
94
+
95
+ it('should handle get_incident with rich debugging context', async () => {
96
+ const res = await handleToolCall('get_incident', { incidentId: 'inc_42' }, { db });
97
+ expect(res.id).toBe('inc_42');
98
+ expect(res.source.file).toBe('/src/components/UserAvatar.tsx');
99
+ expect(res.source.line).toBe(18);
100
+ expect(res.lastInteractedElement?.selector).toBe('[data-testid="user-42"]');
101
+ expect(res.recentBreadcrumbs.length).toBe(2);
102
+ expect(res.networkFailures.length).toBe(1);
103
+ });
104
+
105
+ it('should handle get_console', async () => {
106
+ const res = await handleToolCall('get_console', { sessionId: 'sess_mcp_1' }, { db });
107
+ expect(res.logs.length).toBe(1);
108
+ expect(res.logs[0].message).toBe('Failed to load user image');
109
+ });
110
+
111
+ it('should handle get_breadcrumbs', async () => {
112
+ const res = await handleToolCall('get_breadcrumbs', { incidentId: 'inc_42' }, { db });
113
+ expect(res.breadcrumbs.length).toBe(2);
114
+ expect(res.breadcrumbs[0].type).toBe('navigation');
115
+ });
116
+ });
@@ -0,0 +1,107 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { StorageDB } from '../../packages/daemon/src/storage/db.js';
3
+ import { ScreenshotStore } from '../../packages/daemon/src/storage/screenshot-store.js';
4
+ import { SessionManager } from '../../packages/daemon/src/session/manager.js';
5
+ import { NotesEngine } from '../../packages/daemon/src/notes/engine.js';
6
+ import { NoteVerificationEngine } from '../../packages/daemon/src/notes/verification.js';
7
+ import { handleToolCall } from '../../packages/mcp/src/handlers.js';
8
+
9
+ describe('MCP Tools - Visual Notes', () => {
10
+ let db: StorageDB;
11
+ let screenshotStore: ScreenshotStore;
12
+ let sessionManager: SessionManager;
13
+ let notesEngine: NotesEngine;
14
+ let noteVerificationEngine: NoteVerificationEngine;
15
+
16
+ beforeEach(() => {
17
+ db = new StorageDB(':memory:');
18
+ screenshotStore = new ScreenshotStore('/tmp/test-mcp-notes');
19
+ sessionManager = new SessionManager(db);
20
+ notesEngine = new NotesEngine(db, screenshotStore);
21
+ noteVerificationEngine = new NoteVerificationEngine(db, sessionManager, notesEngine);
22
+
23
+ db.upsertProject({ id: 'proj_mcp', name: 'store-app', origin: 'http://localhost:5173' });
24
+ db.upsertSession({
25
+ id: 'sess_mcp',
26
+ projectId: 'proj_mcp',
27
+ origin: 'http://localhost:5173',
28
+ connectedAt: new Date().toISOString(),
29
+ lastSeenAt: new Date().toISOString(),
30
+ });
31
+
32
+ db.insertNote({
33
+ id: 'note_mcp_1',
34
+ projectId: 'proj_mcp',
35
+ sessionId: 'sess_mcp',
36
+ type: 'element',
37
+ message: 'Mobile nav hamburger button is misaligned',
38
+ route: '/checkout',
39
+ url: 'http://localhost:5173/checkout',
40
+ viewport: { width: 390, height: 844, devicePixelRatio: 3 },
41
+ scroll: { scrollX: 0, scrollY: 0 },
42
+ target: {
43
+ selector: '#nav-hamburger',
44
+ boundingRect: { x: 340, y: 15, width: 40, height: 40, top: 15, left: 340, bottom: 55, right: 380 },
45
+ visible: true,
46
+ },
47
+ elementContext: {
48
+ selector: '#nav-hamburger',
49
+ tag: 'button',
50
+ attributes: { id: 'nav-hamburger', 'aria-label': 'Menu' },
51
+ outerHTML: '<button id="nav-hamburger" aria-label="Menu"><span></span></button>',
52
+ },
53
+ status: 'OPEN',
54
+ createdAt: new Date().toISOString(),
55
+ updatedAt: new Date().toISOString(),
56
+ });
57
+ });
58
+
59
+ it('lists visual notes via list_notes tool', async () => {
60
+ const res = await handleToolCall(
61
+ 'list_notes',
62
+ { projectId: 'proj_mcp' },
63
+ { db, sessionManager, noteVerificationEngine }
64
+ );
65
+
66
+ expect(res.total).toBe(1);
67
+ expect(res.notes[0].id).toBe('note_mcp_1');
68
+ expect(res.notes[0].route).toBe('/checkout');
69
+ expect(res.notes[0].target).toBe('#nav-hamburger');
70
+ expect(res.notes[0].message).toBe('Mobile nav hamburger button is misaligned');
71
+ });
72
+
73
+ it('retrieves note debugging context via get_note tool', async () => {
74
+ const res = await handleToolCall(
75
+ 'get_note',
76
+ { noteId: 'note_mcp_1' },
77
+ { db, sessionManager, noteVerificationEngine }
78
+ );
79
+
80
+ expect(res.id).toBe('note_mcp_1');
81
+ expect(res.type).toBe('element');
82
+ expect(res.viewport.width).toBe(390);
83
+ expect(res.elementContext.tag).toBe('button');
84
+ expect(res.project.name).toBe('store-app');
85
+ expect(res.status).toBe('OPEN');
86
+ });
87
+
88
+ it('resolves and reopens note via resolve_note and reopen_note tools', async () => {
89
+ const resolveRes = await handleToolCall(
90
+ 'resolve_note',
91
+ { noteId: 'note_mcp_1' },
92
+ { db, sessionManager, noteVerificationEngine }
93
+ );
94
+ expect(resolveRes.ok).toBe(true);
95
+ expect(resolveRes.status).toBe('RESOLVED');
96
+ expect(db.getNote('note_mcp_1')?.status).toBe('RESOLVED');
97
+
98
+ const reopenRes = await handleToolCall(
99
+ 'reopen_note',
100
+ { noteId: 'note_mcp_1' },
101
+ { db, sessionManager, noteVerificationEngine }
102
+ );
103
+ expect(reopenRes.ok).toBe(true);
104
+ expect(reopenRes.status).toBe('OPEN');
105
+ expect(db.getNote('note_mcp_1')?.status).toBe('OPEN');
106
+ });
107
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true
16
+ }
17
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "baseUrl": ".",
17
+ "paths": {
18
+ "@browserdiag/core": ["./packages/core/src/index.ts"],
19
+ "browsertrack/core": ["./packages/core/src/index.ts"],
20
+ "browsertrack/client": ["./packages/client/src/index.ts"],
21
+ "browsertrack/daemon": ["./packages/daemon/src/index.ts"],
22
+ "browsertrack/mcp": ["./packages/mcp/src/index.ts"]
23
+ }
24
+ },
25
+ "include": ["src/**/*", "packages/**/*", "test/**/*"]
26
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig([
4
+ // Core, Client, Daemon, MCP Library exports
5
+ {
6
+ entry: {
7
+ index: 'src/index.ts',
8
+ 'core/index': 'packages/core/src/index.ts',
9
+ 'client/index': 'packages/client/src/index.ts',
10
+ 'daemon/index': 'packages/daemon/src/index.ts',
11
+ 'mcp/index': 'packages/mcp/src/index.ts',
12
+ },
13
+ format: ['esm'],
14
+ dts: true,
15
+ sourcemap: true,
16
+ clean: true,
17
+ target: 'node20',
18
+ },
19
+ // CLI Binary
20
+ {
21
+ entry: {
22
+ 'cli/index': 'packages/cli/src/index.ts',
23
+ },
24
+ format: ['esm'],
25
+ banner: {
26
+ js: '#!/usr/bin/env node',
27
+ },
28
+ sourcemap: true,
29
+ target: 'node20',
30
+ },
31
+ // Client browser CJS bundle
32
+ {
33
+ entry: {
34
+ 'client/index': 'packages/client/src/index.ts',
35
+ },
36
+ format: ['cjs'],
37
+ dts: false,
38
+ target: 'es2020',
39
+ },
40
+ // Standalone browser client script (for http://127.0.0.1:7331/client.js)
41
+ {
42
+ entry: {
43
+ 'client.iife': 'packages/client/src/index.ts',
44
+ },
45
+ format: ['iife'],
46
+ globalName: 'BrowserTrack',
47
+ platform: 'browser',
48
+ target: 'es2020',
49
+ outExtension() {
50
+ return { js: '.js' };
51
+ },
52
+ minify: true,
53
+ },
54
+ ]);
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['test/**/*.test.ts'],
8
+ },
9
+ });