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.
- package/README.md +143 -0
- package/dist/chunk-6A7FFDIB.js +221 -0
- package/dist/chunk-6A7FFDIB.js.map +1 -0
- package/dist/chunk-ANMR5WBH.js +659 -0
- package/dist/chunk-ANMR5WBH.js.map +1 -0
- package/dist/chunk-BSKNG4V7.js +1199 -0
- package/dist/chunk-BSKNG4V7.js.map +1 -0
- package/dist/chunk-X7C5CHBO.js +2080 -0
- package/dist/chunk-X7C5CHBO.js.map +1 -0
- package/dist/chunk-ZLNGOOH2.js +554 -0
- package/dist/chunk-ZLNGOOH2.js.map +1 -0
- package/dist/cli/index.js +2829 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.cjs +2218 -0
- package/dist/client/index.d.ts +197 -0
- package/dist/client/index.js +20 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.iife.js +309 -0
- package/dist/core/index.d.ts +78 -0
- package/dist/core/index.js +31 -0
- package/dist/core/index.js.map +1 -0
- package/dist/daemon/index.d.ts +58 -0
- package/dist/daemon/index.js +32 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/engine-DKloFjvs.d.ts +157 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +11 -0
- package/dist/mcp/index.js +12 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/notes-CBvN91Wf.d.ts +260 -0
- package/dist/projects-CY8ungMt.d.ts +99 -0
- package/dist/server-CN-se8td.d.ts +55 -0
- package/examples/test-app/index.html +299 -0
- package/package.json +52 -7
- package/packages/cli/src/index.ts +413 -0
- package/packages/client/package.json +28 -0
- package/packages/client/src/breadcrumbs.ts +30 -0
- package/packages/client/src/client.ts +278 -0
- package/packages/client/src/commands/handler.ts +301 -0
- package/packages/client/src/config.ts +37 -0
- package/packages/client/src/index.ts +50 -0
- package/packages/client/src/interceptors/console.ts +68 -0
- package/packages/client/src/interceptors/interaction.ts +119 -0
- package/packages/client/src/interceptors/navigation.ts +93 -0
- package/packages/client/src/interceptors/network.ts +165 -0
- package/packages/client/src/interceptors/runtime.ts +65 -0
- package/packages/client/src/notes/inspector.ts +943 -0
- package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
- package/packages/client/src/screenshot/driver.ts +14 -0
- package/packages/client/src/transport/websocket.ts +179 -0
- package/packages/client/tsconfig.json +8 -0
- package/packages/core/dist/index.d.ts +317 -0
- package/packages/core/dist/index.js +221 -0
- package/packages/core/package.json +23 -0
- package/packages/core/src/fingerprint.ts +120 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/redaction.ts +139 -0
- package/packages/core/src/selector.ts +77 -0
- package/packages/core/src/types/commands.ts +101 -0
- package/packages/core/src/types/events.ts +108 -0
- package/packages/core/src/types/incidents.ts +54 -0
- package/packages/core/src/types/notes.ts +106 -0
- package/packages/core/src/types/probes.ts +44 -0
- package/packages/core/src/types/projects.ts +20 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/daemon/src/config.ts +29 -0
- package/packages/daemon/src/incidents/engine.ts +211 -0
- package/packages/daemon/src/index.ts +18 -0
- package/packages/daemon/src/notes/engine.ts +92 -0
- package/packages/daemon/src/notes/verification.ts +191 -0
- package/packages/daemon/src/server/daemon.ts +112 -0
- package/packages/daemon/src/server/http.ts +165 -0
- package/packages/daemon/src/server/ws.ts +175 -0
- package/packages/daemon/src/session/manager.ts +126 -0
- package/packages/daemon/src/storage/db.ts +733 -0
- package/packages/daemon/src/storage/screenshot-store.ts +49 -0
- package/packages/daemon/src/verification/engine.ts +264 -0
- package/packages/mcp/src/handlers.ts +398 -0
- package/packages/mcp/src/index.ts +3 -0
- package/packages/mcp/src/server.ts +86 -0
- package/packages/mcp/src/tools.ts +236 -0
- package/src/index.ts +4 -0
- package/test/client/interceptors.test.ts +69 -0
- package/test/core/fingerprint.test.ts +53 -0
- package/test/core/notes.test.ts +66 -0
- package/test/core/redaction.test.ts +48 -0
- package/test/daemon/incident-engine.test.ts +98 -0
- package/test/daemon/notes-storage.test.ts +130 -0
- package/test/daemon/notes-verification.test.ts +135 -0
- package/test/daemon/storage.test.ts +123 -0
- package/test/daemon/verification-engine.test.ts +88 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
- package/test/e2e/visual-notes-e2e.test.ts +205 -0
- package/test/mcp/handlers.test.ts +116 -0
- package/test/mcp/notes.test.ts +107 -0
- package/tsconfig.base.json +17 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +54 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export class ScreenshotStore {
|
|
5
|
+
private baseDir: string;
|
|
6
|
+
|
|
7
|
+
constructor(baseDir: string) {
|
|
8
|
+
this.baseDir = baseDir;
|
|
9
|
+
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public saveScreenshot(projectId: string, incidentId: string, name: string, dataUrl: string): { filePath: string; format: string } | null {
|
|
13
|
+
try {
|
|
14
|
+
if (!dataUrl || !dataUrl.startsWith('data:image/')) return null;
|
|
15
|
+
|
|
16
|
+
const match = dataUrl.match(/^data:image\/([a-zA-Z0-9+]+);base64,(.+)$/);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
|
|
19
|
+
let format = match[1].toLowerCase();
|
|
20
|
+
if (format === 'jpeg') format = 'jpg';
|
|
21
|
+
const base64Data = match[2];
|
|
22
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
23
|
+
|
|
24
|
+
const targetDir = path.join(this.baseDir, projectId || 'default', 'incidents', incidentId);
|
|
25
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
const fileName = `${name}.${format}`;
|
|
28
|
+
const filePath = path.join(targetDir, fileName);
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(filePath, buffer);
|
|
31
|
+
|
|
32
|
+
return { filePath, format };
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getScreenshotPath(projectId: string, incidentId: string, name: string): string | null {
|
|
39
|
+
const targetDir = path.join(this.baseDir, projectId || 'default', 'incidents', incidentId);
|
|
40
|
+
if (!fs.existsSync(targetDir)) return null;
|
|
41
|
+
|
|
42
|
+
const files = fs.readdirSync(targetDir);
|
|
43
|
+
const match = files.find((f) => f.startsWith(`${name}.`));
|
|
44
|
+
if (match) {
|
|
45
|
+
return path.join(targetDir, match);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
Incident,
|
|
4
|
+
IncidentStatus,
|
|
5
|
+
ProbeResult,
|
|
6
|
+
VerificationProbe,
|
|
7
|
+
VerificationRecipe,
|
|
8
|
+
VerificationResult,
|
|
9
|
+
} from '../../../core/src/index.js';
|
|
10
|
+
import type { SessionManager } from '../session/manager.js';
|
|
11
|
+
import type { StorageDB } from '../storage/db.js';
|
|
12
|
+
import type { ScreenshotStore } from '../storage/screenshot-store.js';
|
|
13
|
+
|
|
14
|
+
export class VerificationEngine {
|
|
15
|
+
private db: StorageDB;
|
|
16
|
+
private sessionManager: SessionManager;
|
|
17
|
+
private screenshotStore: ScreenshotStore;
|
|
18
|
+
|
|
19
|
+
constructor(db: StorageDB, sessionManager: SessionManager, screenshotStore: ScreenshotStore) {
|
|
20
|
+
this.db = db;
|
|
21
|
+
this.sessionManager = sessionManager;
|
|
22
|
+
this.screenshotStore = screenshotStore;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async verifyIncident(incidentId: string, recipe?: VerificationRecipe): Promise<VerificationResult> {
|
|
26
|
+
const incident = this.db.getIncident(incidentId);
|
|
27
|
+
if (!incident) {
|
|
28
|
+
return {
|
|
29
|
+
incidentId,
|
|
30
|
+
status: 'FAILED',
|
|
31
|
+
checks: [{ type: 'no_incident', passed: false, details: `Incident ${incidentId} not found` }],
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
message: `Incident ${incidentId} not found in database`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1. Find active session
|
|
38
|
+
let session = this.sessionManager.getActiveSessionForProject(incident.projectId);
|
|
39
|
+
if (!session) {
|
|
40
|
+
session = this.sessionManager.getAnyActiveSession();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!session) {
|
|
44
|
+
const result: VerificationResult = {
|
|
45
|
+
incidentId,
|
|
46
|
+
status: 'INCONCLUSIVE',
|
|
47
|
+
checks: [],
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
message: 'No active browser session currently connected to verify the fix.',
|
|
50
|
+
};
|
|
51
|
+
this.recordVerification(result, incident);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. Set status to VERIFYING and record baseline
|
|
56
|
+
this.db.updateIncidentStatus(incidentId, 'VERIFYING');
|
|
57
|
+
const baselineOccurrences = incident.occurrences;
|
|
58
|
+
|
|
59
|
+
// 3. Initiate browser reload or navigation
|
|
60
|
+
const targetRoute = recipe?.route || incident.route;
|
|
61
|
+
const observationMs = recipe?.observationWindowMs || 2000;
|
|
62
|
+
|
|
63
|
+
if (recipe?.route && recipe.route !== incident.route) {
|
|
64
|
+
await this.sessionManager.sendCommand(session.id, {
|
|
65
|
+
id: `cmd_${crypto.randomUUID().slice(0, 8)}`,
|
|
66
|
+
type: 'navigate',
|
|
67
|
+
params: { url: recipe.route },
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
await this.sessionManager.sendCommand(session.id, {
|
|
71
|
+
id: `cmd_${crypto.randomUUID().slice(0, 8)}`,
|
|
72
|
+
type: 'reload',
|
|
73
|
+
params: { force: true },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. Observation window
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, observationMs));
|
|
79
|
+
|
|
80
|
+
// 5. Check if incident re-occurred
|
|
81
|
+
const freshIncident = this.db.getIncident(incidentId);
|
|
82
|
+
const hasReoccurred = freshIncident ? freshIncident.occurrences > baselineOccurrences : false;
|
|
83
|
+
|
|
84
|
+
const checks: ProbeResult[] = [];
|
|
85
|
+
checks.push({
|
|
86
|
+
type: 'no_incident',
|
|
87
|
+
passed: !hasReoccurred,
|
|
88
|
+
details: hasReoccurred
|
|
89
|
+
? `Incident reoccurred (${freshIncident?.occurrences} occurrences vs baseline ${baselineOccurrences})`
|
|
90
|
+
: 'No recurring error observed during verification window',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 6. Run additional probes
|
|
94
|
+
const probes = recipe?.expect || [];
|
|
95
|
+
for (const probe of probes) {
|
|
96
|
+
const probeRes = await this.evaluateProbe(session.id, probe);
|
|
97
|
+
checks.push(probeRes);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 7. Check target element or last element for after-screenshot
|
|
101
|
+
let afterScreenshotPath: string | undefined;
|
|
102
|
+
const targetSelector = recipe?.targetSelector || incident.lastElement?.selector;
|
|
103
|
+
|
|
104
|
+
if (targetSelector) {
|
|
105
|
+
const captureCmd = await this.sessionManager.sendCommand(session.id, {
|
|
106
|
+
id: `cmd_${crypto.randomUUID().slice(0, 8)}`,
|
|
107
|
+
type: 'capture_element',
|
|
108
|
+
params: { selector: targetSelector },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (captureCmd.ok && captureCmd.result?.dataUrl) {
|
|
112
|
+
const saved = this.screenshotStore.saveScreenshot(
|
|
113
|
+
incident.projectId,
|
|
114
|
+
incident.id,
|
|
115
|
+
'verified',
|
|
116
|
+
captureCmd.result.dataUrl
|
|
117
|
+
);
|
|
118
|
+
if (saved) {
|
|
119
|
+
afterScreenshotPath = saved.filePath;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 8. Calculate verdict
|
|
125
|
+
let verdict: IncidentStatus = 'VERIFIED';
|
|
126
|
+
const anyCheckFailed = checks.some((c) => !c.passed);
|
|
127
|
+
|
|
128
|
+
if (anyCheckFailed || hasReoccurred) {
|
|
129
|
+
verdict = 'FAILED';
|
|
130
|
+
} else {
|
|
131
|
+
// Check if incident required an explicit interaction (like button click) that wasn't covered by probes
|
|
132
|
+
const wasInteractionError = incident.breadcrumbs.some((b) => b.type === 'click' || b.type === 'submit');
|
|
133
|
+
if (wasInteractionError && probes.length === 0) {
|
|
134
|
+
verdict = 'INCONCLUSIVE';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const verificationResult: VerificationResult = {
|
|
139
|
+
incidentId,
|
|
140
|
+
status: verdict,
|
|
141
|
+
checks,
|
|
142
|
+
screenshots: {
|
|
143
|
+
before: incident.screenshots?.error,
|
|
144
|
+
after: afterScreenshotPath,
|
|
145
|
+
},
|
|
146
|
+
timestamp: new Date().toISOString(),
|
|
147
|
+
message:
|
|
148
|
+
verdict === 'VERIFIED'
|
|
149
|
+
? 'Fix verified: Error did not reoccur and all checks passed.'
|
|
150
|
+
: verdict === 'FAILED'
|
|
151
|
+
? 'Verification failed: Error reoccurred or probe expectation was not met.'
|
|
152
|
+
: 'Verification inconclusive: Error did not appear on reload, but specific interaction may need manual verification or custom probes.',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
this.recordVerification(verificationResult, incident);
|
|
156
|
+
return verificationResult;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async evaluateProbe(sessionId: string, probe: VerificationProbe): Promise<ProbeResult> {
|
|
160
|
+
try {
|
|
161
|
+
switch (probe.type) {
|
|
162
|
+
case 'element_exists': {
|
|
163
|
+
if (!probe.selector) {
|
|
164
|
+
return { type: probe.type, passed: false, details: 'Missing selector in probe' };
|
|
165
|
+
}
|
|
166
|
+
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
167
|
+
id: `cmd_${crypto.randomUUID().slice(0, 8)}`,
|
|
168
|
+
type: 'query_element',
|
|
169
|
+
params: { selector: probe.selector },
|
|
170
|
+
});
|
|
171
|
+
const exists = res.ok && !!res.result?.exists;
|
|
172
|
+
return {
|
|
173
|
+
type: probe.type,
|
|
174
|
+
passed: exists,
|
|
175
|
+
details: exists ? `Element ${probe.selector} exists in DOM` : `Element ${probe.selector} does not exist`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case 'element_visible': {
|
|
180
|
+
if (!probe.selector) {
|
|
181
|
+
return { type: probe.type, passed: false, details: 'Missing selector in probe' };
|
|
182
|
+
}
|
|
183
|
+
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
184
|
+
id: `cmd_${crypto.randomUUID().slice(0, 8)}`,
|
|
185
|
+
type: 'query_element',
|
|
186
|
+
params: { selector: probe.selector },
|
|
187
|
+
});
|
|
188
|
+
const visible = res.ok && !!res.result?.exists && !!res.result?.visible;
|
|
189
|
+
return {
|
|
190
|
+
type: probe.type,
|
|
191
|
+
passed: visible,
|
|
192
|
+
details: visible ? `Element ${probe.selector} is visible` : `Element ${probe.selector} is not visible`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case 'text_contains': {
|
|
197
|
+
if (!probe.selector || !probe.text) {
|
|
198
|
+
return { type: probe.type, passed: false, details: 'Missing selector or text in probe' };
|
|
199
|
+
}
|
|
200
|
+
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
201
|
+
id: `cmd_${crypto.randomUUID().slice(0, 8)}`,
|
|
202
|
+
type: 'query_element',
|
|
203
|
+
params: { selector: probe.selector },
|
|
204
|
+
});
|
|
205
|
+
const innerText = res.result?.innerText || '';
|
|
206
|
+
const contains = res.ok && innerText.includes(probe.text);
|
|
207
|
+
return {
|
|
208
|
+
type: probe.type,
|
|
209
|
+
passed: contains,
|
|
210
|
+
details: contains
|
|
211
|
+
? `Element ${probe.selector} contains text "${probe.text}"`
|
|
212
|
+
: `Element text "${innerText}" did not contain "${probe.text}"`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'route_is': {
|
|
217
|
+
if (!probe.route) {
|
|
218
|
+
return { type: probe.type, passed: false, details: 'Missing route in probe' };
|
|
219
|
+
}
|
|
220
|
+
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
221
|
+
id: `cmd_${crypto.randomUUID().slice(0, 8)}`,
|
|
222
|
+
type: 'get_page_state',
|
|
223
|
+
});
|
|
224
|
+
const currentRoute = res.result?.route || '';
|
|
225
|
+
const matches = res.ok && (currentRoute === probe.route || currentRoute.startsWith(probe.route));
|
|
226
|
+
return {
|
|
227
|
+
type: probe.type,
|
|
228
|
+
passed: matches,
|
|
229
|
+
details: matches
|
|
230
|
+
? `Current route matches ${probe.route}`
|
|
231
|
+
: `Current route is "${currentRoute}", expected "${probe.route}"`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
default:
|
|
236
|
+
return {
|
|
237
|
+
type: probe.type,
|
|
238
|
+
passed: true,
|
|
239
|
+
details: `Probe ${probe.type} evaluated`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
} catch (err: any) {
|
|
243
|
+
return {
|
|
244
|
+
type: probe.type,
|
|
245
|
+
passed: false,
|
|
246
|
+
details: err?.message || 'Probe execution error',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private recordVerification(result: VerificationResult, incident: Incident): void {
|
|
252
|
+
this.db.updateIncidentStatus(incident.id, result.status);
|
|
253
|
+
this.db.insertVerification({
|
|
254
|
+
id: `ver_${crypto.randomUUID().slice(0, 8)}`,
|
|
255
|
+
incidentId: incident.id,
|
|
256
|
+
status: result.status,
|
|
257
|
+
checks: result.checks,
|
|
258
|
+
beforeScreenshot: result.screenshots?.before,
|
|
259
|
+
afterScreenshot: result.screenshots?.after,
|
|
260
|
+
message: result.message,
|
|
261
|
+
createdAt: result.timestamp,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import type { StorageDB } from '../../daemon/src/storage/db.js';
|
|
2
|
+
import type { VerificationEngine } from '../../daemon/src/verification/engine.js';
|
|
3
|
+
import type { NoteVerificationEngine } from '../../daemon/src/notes/verification.js';
|
|
4
|
+
import type { SessionManager } from '../../daemon/src/session/manager.js';
|
|
5
|
+
|
|
6
|
+
export interface McpContext {
|
|
7
|
+
db: StorageDB;
|
|
8
|
+
sessionManager?: SessionManager;
|
|
9
|
+
verificationEngine?: VerificationEngine;
|
|
10
|
+
noteVerificationEngine?: NoteVerificationEngine;
|
|
11
|
+
daemonUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function handleToolCall(name: string, args: any, ctx: McpContext): Promise<any> {
|
|
15
|
+
const { db, sessionManager, verificationEngine, noteVerificationEngine } = ctx;
|
|
16
|
+
|
|
17
|
+
switch (name) {
|
|
18
|
+
case 'list_projects': {
|
|
19
|
+
const projects = db.listProjects();
|
|
20
|
+
return {
|
|
21
|
+
projects: projects.map((p) => ({
|
|
22
|
+
id: p.id,
|
|
23
|
+
name: p.name,
|
|
24
|
+
origin: p.origin,
|
|
25
|
+
path: p.path,
|
|
26
|
+
updatedAt: p.updatedAt,
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case 'list_sessions': {
|
|
32
|
+
const activeOnly = args.activeOnly !== false;
|
|
33
|
+
const sessions = db.listSessions(args.projectId, activeOnly);
|
|
34
|
+
return {
|
|
35
|
+
count: sessions.length,
|
|
36
|
+
sessions: sessions.map((s) => ({
|
|
37
|
+
id: s.id,
|
|
38
|
+
projectId: s.projectId,
|
|
39
|
+
origin: s.origin,
|
|
40
|
+
url: s.url,
|
|
41
|
+
title: s.title,
|
|
42
|
+
active: s.active,
|
|
43
|
+
lastSeenAt: s.lastSeenAt,
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'list_incidents': {
|
|
49
|
+
const incidents = db.listIncidents({
|
|
50
|
+
projectId: args.projectId,
|
|
51
|
+
status: args.status,
|
|
52
|
+
severity: args.severity,
|
|
53
|
+
limit: args.limit || 20,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
total: incidents.length,
|
|
57
|
+
incidents: incidents.map((inc) => ({
|
|
58
|
+
id: inc.id,
|
|
59
|
+
type: inc.type,
|
|
60
|
+
severity: inc.severity,
|
|
61
|
+
message: inc.message,
|
|
62
|
+
source: `${inc.source.file}:${inc.source.line}`,
|
|
63
|
+
route: inc.route,
|
|
64
|
+
status: inc.status,
|
|
65
|
+
occurrences: inc.occurrences,
|
|
66
|
+
firstSeen: inc.firstSeen,
|
|
67
|
+
lastSeen: inc.lastSeen,
|
|
68
|
+
})),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'get_incident': {
|
|
73
|
+
const incident = db.getIncident(args.incidentId);
|
|
74
|
+
if (!incident) {
|
|
75
|
+
throw new Error(`Incident '${args.incidentId}' not found.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Compact breadcrumbs summary (last 10 most relevant)
|
|
79
|
+
const breadcrumbsTimeline = incident.breadcrumbs.slice(-15).map((b) => {
|
|
80
|
+
const time = new Date(b.timestamp).toISOString().split('T')[1]?.slice(0, 8);
|
|
81
|
+
return `[${time}] ${b.message}`;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
id: incident.id,
|
|
86
|
+
status: incident.status,
|
|
87
|
+
type: incident.type,
|
|
88
|
+
severity: incident.severity,
|
|
89
|
+
message: incident.message,
|
|
90
|
+
source: {
|
|
91
|
+
file: incident.source.file,
|
|
92
|
+
line: incident.source.line,
|
|
93
|
+
column: incident.source.column,
|
|
94
|
+
},
|
|
95
|
+
route: incident.route,
|
|
96
|
+
occurrences: incident.occurrences,
|
|
97
|
+
firstSeen: incident.firstSeen,
|
|
98
|
+
lastSeen: incident.lastSeen,
|
|
99
|
+
stack: incident.stack,
|
|
100
|
+
lastInteractedElement: incident.lastElement
|
|
101
|
+
? {
|
|
102
|
+
selector: incident.lastElement.selector,
|
|
103
|
+
tag: incident.lastElement.tag,
|
|
104
|
+
visible: incident.lastElement.visible,
|
|
105
|
+
innerText: incident.lastElement.innerText,
|
|
106
|
+
outerHTML: incident.lastElement.outerHTML,
|
|
107
|
+
}
|
|
108
|
+
: undefined,
|
|
109
|
+
recentBreadcrumbs: breadcrumbsTimeline,
|
|
110
|
+
networkFailures: incident.networkFailures,
|
|
111
|
+
screenshot: incident.screenshots?.error,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'get_console': {
|
|
116
|
+
const limit = args.limit || 30;
|
|
117
|
+
const events = db.getEvents({
|
|
118
|
+
sessionId: args.sessionId,
|
|
119
|
+
eventType: 'console',
|
|
120
|
+
limit,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
logs: events.map((e) => ({
|
|
125
|
+
level: e.payload.level,
|
|
126
|
+
message: e.payload.message,
|
|
127
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
128
|
+
route: e.route,
|
|
129
|
+
})),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'get_network_failures': {
|
|
134
|
+
const limit = args.limit || 20;
|
|
135
|
+
const events = db.getEvents({
|
|
136
|
+
sessionId: args.sessionId,
|
|
137
|
+
limit: 100,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const failures = events
|
|
141
|
+
.filter((e) => (e.eventType === 'fetch' || e.eventType === 'xhr') && (e.payload.status >= 400 || e.payload.error))
|
|
142
|
+
.slice(0, limit)
|
|
143
|
+
.map((e) => ({
|
|
144
|
+
url: e.payload.url,
|
|
145
|
+
method: e.payload.method,
|
|
146
|
+
status: e.payload.status,
|
|
147
|
+
error: e.payload.error,
|
|
148
|
+
durationMs: e.payload.durationMs,
|
|
149
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
return { failures };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case 'get_breadcrumbs': {
|
|
156
|
+
if (args.incidentId) {
|
|
157
|
+
const incident = db.getIncident(args.incidentId);
|
|
158
|
+
if (!incident) {
|
|
159
|
+
throw new Error(`Incident '${args.incidentId}' not found.`);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
incidentId: args.incidentId,
|
|
163
|
+
breadcrumbs: incident.breadcrumbs.slice(-(args.limit || 50)),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const events = db.getEvents({
|
|
168
|
+
sessionId: args.sessionId,
|
|
169
|
+
limit: args.limit || 50,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
breadcrumbs: events.map((e) => ({
|
|
174
|
+
type: e.eventType,
|
|
175
|
+
message: e.payload.message || `${e.eventType} on ${e.route}`,
|
|
176
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
177
|
+
route: e.route,
|
|
178
|
+
})),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'get_page_state': {
|
|
183
|
+
if (!sessionManager) {
|
|
184
|
+
throw new Error('Live browser connection not available: Daemon session manager not attached.');
|
|
185
|
+
}
|
|
186
|
+
let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
|
|
187
|
+
if (!session) {
|
|
188
|
+
throw new Error('No active browser session connected.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const cmdRes = await sessionManager.sendCommand(session.id, {
|
|
192
|
+
id: `cmd_mcp_${Date.now()}`,
|
|
193
|
+
type: 'get_page_state',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!cmdRes.ok) {
|
|
197
|
+
throw new Error(cmdRes.error || 'Failed to retrieve page state from browser.');
|
|
198
|
+
}
|
|
199
|
+
return cmdRes.result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'capture_element': {
|
|
203
|
+
if (!sessionManager) {
|
|
204
|
+
throw new Error('Live browser connection not available: Daemon session manager not attached.');
|
|
205
|
+
}
|
|
206
|
+
let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
|
|
207
|
+
if (!session) {
|
|
208
|
+
throw new Error('No active browser session connected.');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const cmdRes = await sessionManager.sendCommand(session.id, {
|
|
212
|
+
id: `cmd_mcp_${Date.now()}`,
|
|
213
|
+
type: 'capture_element',
|
|
214
|
+
params: { selector: args.selector },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!cmdRes.ok) {
|
|
218
|
+
throw new Error(cmdRes.error || cmdRes.reason || 'Failed to capture element screenshot.');
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
format: cmdRes.result?.format || 'webp',
|
|
223
|
+
width: cmdRes.result?.width,
|
|
224
|
+
height: cmdRes.result?.height,
|
|
225
|
+
dataUrlPreview: cmdRes.result?.dataUrl ? `${cmdRes.result.dataUrl.slice(0, 100)}...` : undefined,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case 'verify_incident': {
|
|
230
|
+
if (!verificationEngine) {
|
|
231
|
+
throw new Error('Verification engine not available: Daemon session manager not attached.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const res = await verificationEngine.verifyIncident(args.incidentId, {
|
|
235
|
+
route: args.route,
|
|
236
|
+
targetSelector: args.targetSelector,
|
|
237
|
+
expect: args.expect,
|
|
238
|
+
observationWindowMs: args.observationWindowMs,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return res;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case 'get_verification': {
|
|
245
|
+
const v = db.getLatestVerification(args.incidentId);
|
|
246
|
+
if (!v) {
|
|
247
|
+
throw new Error(`No verification records found for incident '${args.incidentId}'.`);
|
|
248
|
+
}
|
|
249
|
+
return v;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'list_notes': {
|
|
253
|
+
const notes = db.listNotes({
|
|
254
|
+
projectId: args.projectId,
|
|
255
|
+
status: args.status,
|
|
256
|
+
limit: args.limit || 20,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
total: notes.length,
|
|
261
|
+
notes: notes.map((n) => ({
|
|
262
|
+
id: n.id,
|
|
263
|
+
type: n.type,
|
|
264
|
+
status: n.status,
|
|
265
|
+
route: n.route,
|
|
266
|
+
viewport: `${n.viewport.width} × ${n.viewport.height}`,
|
|
267
|
+
target: n.target?.selector || n.type,
|
|
268
|
+
message: n.message,
|
|
269
|
+
screenshotAvailable: !!n.screenshots?.original,
|
|
270
|
+
createdAt: n.createdAt,
|
|
271
|
+
})),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case 'get_note': {
|
|
276
|
+
const note = db.getNote(args.noteId);
|
|
277
|
+
if (!note) {
|
|
278
|
+
throw new Error(`Visual note '${args.noteId}' not found.`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const project = db.getProject(note.projectId);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
id: note.id,
|
|
285
|
+
type: note.type,
|
|
286
|
+
status: note.status,
|
|
287
|
+
message: note.message,
|
|
288
|
+
route: note.route,
|
|
289
|
+
url: note.url,
|
|
290
|
+
viewport: note.viewport,
|
|
291
|
+
scroll: note.scroll,
|
|
292
|
+
target: note.target,
|
|
293
|
+
elementContext: note.elementContext,
|
|
294
|
+
region: note.region,
|
|
295
|
+
screenshot: {
|
|
296
|
+
available: !!note.screenshots?.original,
|
|
297
|
+
original: note.screenshots?.original,
|
|
298
|
+
after: note.screenshots?.after,
|
|
299
|
+
},
|
|
300
|
+
project: project
|
|
301
|
+
? {
|
|
302
|
+
id: project.id,
|
|
303
|
+
name: project.name,
|
|
304
|
+
origin: project.origin,
|
|
305
|
+
path: project.path,
|
|
306
|
+
}
|
|
307
|
+
: undefined,
|
|
308
|
+
relatedIncidentId: note.incidentId,
|
|
309
|
+
createdAt: note.createdAt,
|
|
310
|
+
updatedAt: note.updatedAt,
|
|
311
|
+
resolvedAt: note.resolvedAt,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case 'resolve_note': {
|
|
316
|
+
const note = db.getNote(args.noteId);
|
|
317
|
+
if (!note) {
|
|
318
|
+
throw new Error(`Visual note '${args.noteId}' not found.`);
|
|
319
|
+
}
|
|
320
|
+
db.updateNoteStatus(args.noteId, 'RESOLVED');
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
noteId: args.noteId,
|
|
324
|
+
status: 'RESOLVED',
|
|
325
|
+
resolvedAt: new Date().toISOString(),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
case 'reopen_note': {
|
|
330
|
+
const note = db.getNote(args.noteId);
|
|
331
|
+
if (!note) {
|
|
332
|
+
throw new Error(`Visual note '${args.noteId}' not found.`);
|
|
333
|
+
}
|
|
334
|
+
db.updateNoteStatus(args.noteId, 'OPEN');
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
noteId: args.noteId,
|
|
338
|
+
status: 'OPEN',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
case 'verify_note': {
|
|
343
|
+
if (!noteVerificationEngine) {
|
|
344
|
+
throw new Error('Note verification engine not available: Daemon session manager not attached.');
|
|
345
|
+
}
|
|
346
|
+
const res = await noteVerificationEngine.verifyNote(args.noteId, {
|
|
347
|
+
observationWindowMs: args.observationWindowMs,
|
|
348
|
+
});
|
|
349
|
+
return res;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
case 'get_note_verification': {
|
|
353
|
+
const v = db.getLatestNoteVerification(args.noteId);
|
|
354
|
+
if (!v) {
|
|
355
|
+
throw new Error(`No verification records found for visual note '${args.noteId}'.`);
|
|
356
|
+
}
|
|
357
|
+
return v;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
case 'capture_note_context': {
|
|
361
|
+
if (!sessionManager) {
|
|
362
|
+
throw new Error('Live browser connection not available: Daemon session manager not attached.');
|
|
363
|
+
}
|
|
364
|
+
let session = args.sessionId ? db.getSession(args.sessionId) : sessionManager.getAnyActiveSession();
|
|
365
|
+
if (!session) {
|
|
366
|
+
throw new Error('No active browser session connected.');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const queryCmd = await sessionManager.sendCommand(session.id, {
|
|
370
|
+
id: `cmd_ctx_${Date.now()}`,
|
|
371
|
+
type: 'query_element',
|
|
372
|
+
params: { selector: args.selector },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const overflowCmd = await sessionManager.sendCommand(session.id, {
|
|
376
|
+
id: `cmd_ovf_${Date.now()}`,
|
|
377
|
+
type: 'check_overflow',
|
|
378
|
+
params: { selector: args.selector },
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const styleCmd = await sessionManager.sendCommand(session.id, {
|
|
382
|
+
id: `cmd_sty_${Date.now()}`,
|
|
383
|
+
type: 'get_element_style',
|
|
384
|
+
params: { selector: args.selector },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
selector: args.selector,
|
|
389
|
+
element: queryCmd.result,
|
|
390
|
+
overflow: overflowCmd.result,
|
|
391
|
+
styles: styleCmd.result?.styles,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
default:
|
|
396
|
+
throw new Error(`Unknown MCP tool: ${name}`);
|
|
397
|
+
}
|
|
398
|
+
}
|