browsertrack 0.2.0 → 0.2.2
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 +11 -6
- package/README.md +2 -0
- package/dist/{chunk-WB7ZKWK7.js → chunk-4HRLW6YF.js} +509 -109
- package/dist/chunk-4HRLW6YF.js.map +1 -0
- package/dist/{chunk-3HOXPTM2.js → chunk-AYSVE6NG.js} +808 -53
- package/dist/chunk-AYSVE6NG.js.map +1 -0
- package/dist/{chunk-6VA7GBAO.js → chunk-QRZ57ME3.js} +70 -2
- package/dist/chunk-QRZ57ME3.js.map +1 -0
- package/dist/{chunk-UP5JKCFY.js → chunk-TWEYRBDU.js} +281 -44
- package/dist/chunk-TWEYRBDU.js.map +1 -0
- package/dist/cli/index.js +1040 -546
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +536 -109
- package/dist/client/index.d.ts +36 -4
- package/dist/client/index.js +8 -4
- package/dist/client.iife.js +42 -19
- package/dist/{notes-BMnonq46.d.ts → commands-fjuqKzkm.d.ts} +125 -114
- package/dist/core/index.d.ts +26 -3
- package/dist/core/index.js +9 -1
- package/dist/daemon/index.d.ts +4 -4
- package/dist/daemon/index.js +6 -8
- package/dist/{engine-B43IohQY.d.ts → engine-CmchnMDq.d.ts} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +14 -7
- package/dist/mcp/index.d.ts +4 -4
- package/dist/mcp/index.js +7 -4
- package/dist/{projects-D5J-egVN.d.ts → projects-DB7S312i.d.ts} +1 -1
- package/dist/{server-BztYp1Zc.d.ts → server-DjV7RWQM.d.ts} +10 -2
- package/docs/cli.md +4 -1
- package/docs/component-resolver.md +108 -0
- package/docs/getting-started.md +60 -6
- package/docs/index.md +1 -0
- package/docs/mcp-reference.md +44 -2
- package/docs/visual-notes.md +36 -0
- package/package.json +1 -1
- package/packages/cli/src/index.ts +247 -151
- package/packages/client/src/client.ts +18 -1
- package/packages/client/src/config.ts +84 -1
- package/packages/client/src/index.ts +4 -1
- package/packages/client/src/interceptors/interaction.ts +3 -0
- 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 +186 -51
- package/packages/client/src/source/resolver.ts +278 -0
- 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/types/events.ts +3 -0
- package/packages/core/src/types/notes.ts +11 -0
- 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 +115 -45
- package/packages/mcp/src/server.ts +202 -2
- package/test/client/component-resolver.test.ts +141 -0
- package/test/client/interceptors.test.ts +56 -0
- package/test/core/safety.test.ts +106 -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-6VA7GBAO.js.map +0 -1
- package/dist/chunk-7OCOQGDN.js +0 -635
- package/dist/chunk-7OCOQGDN.js.map +0 -1
- package/dist/chunk-UP5JKCFY.js.map +0 -1
- package/dist/chunk-WB7ZKWK7.js.map +0 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { ComponentSourceInfo } from '../../../core/src/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves framework component metadata, source file paths, line numbers,
|
|
5
|
+
* and component hierarchy directly from a live DOM element.
|
|
6
|
+
* Supports React (Fiber), Vue 2 & 3, Svelte, Custom Elements, and standard data attributes.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveComponentSource(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
9
|
+
if (!el || typeof el !== 'object') return undefined;
|
|
10
|
+
|
|
11
|
+
// 1. Try React Fiber Resolution
|
|
12
|
+
const reactInfo = resolveReactComponent(el);
|
|
13
|
+
if (reactInfo) return reactInfo;
|
|
14
|
+
|
|
15
|
+
// 2. Try Vue (Vue 3 / Vue 2) Resolution
|
|
16
|
+
const vueInfo = resolveVueComponent(el);
|
|
17
|
+
if (vueInfo) return vueInfo;
|
|
18
|
+
|
|
19
|
+
// 3. Try Svelte Resolution
|
|
20
|
+
const svelteInfo = resolveSvelteComponent(el);
|
|
21
|
+
if (svelteInfo) return svelteInfo;
|
|
22
|
+
|
|
23
|
+
// 4. Try Web Component / Custom Element
|
|
24
|
+
if (el.tagName && el.tagName.includes('-')) {
|
|
25
|
+
return {
|
|
26
|
+
framework: 'web-component',
|
|
27
|
+
componentName: el.tagName.toLowerCase(),
|
|
28
|
+
hierarchy: [el.tagName.toLowerCase()],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 5. Try Data Attribute Annotations (e.g. data-component, data-source-file)
|
|
33
|
+
const attrInfo = resolveDataAttributeComponent(el);
|
|
34
|
+
if (attrInfo) return attrInfo;
|
|
35
|
+
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extracts React Fiber debug source, owner, and hierarchy
|
|
41
|
+
*/
|
|
42
|
+
function resolveReactComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const fiberKey = Object.keys(el).find(
|
|
45
|
+
(key) => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')
|
|
46
|
+
);
|
|
47
|
+
if (!fiberKey) return undefined;
|
|
48
|
+
|
|
49
|
+
const hostFiber = (el as any)[fiberKey];
|
|
50
|
+
if (!hostFiber) return undefined;
|
|
51
|
+
|
|
52
|
+
let sourceFile: string | undefined;
|
|
53
|
+
let sourceLine: number | undefined;
|
|
54
|
+
let sourceColumn: number | undefined;
|
|
55
|
+
let componentName: string | undefined;
|
|
56
|
+
const hierarchy: string[] = [];
|
|
57
|
+
let props: Record<string, any> | undefined;
|
|
58
|
+
|
|
59
|
+
// Check host fiber debugSource first (often populated by JSX transform)
|
|
60
|
+
if (hostFiber._debugSource) {
|
|
61
|
+
sourceFile = hostFiber._debugSource.fileName;
|
|
62
|
+
sourceLine = hostFiber._debugSource.lineNumber;
|
|
63
|
+
sourceColumn = hostFiber._debugSource.columnNumber;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check _debugOwner if available
|
|
67
|
+
if (hostFiber._debugOwner) {
|
|
68
|
+
const ownerType = hostFiber._debugOwner.type;
|
|
69
|
+
componentName = getReactComponentName(ownerType);
|
|
70
|
+
if (!sourceFile && hostFiber._debugOwner._debugSource) {
|
|
71
|
+
sourceFile = hostFiber._debugOwner._debugSource.fileName;
|
|
72
|
+
sourceLine = hostFiber._debugOwner._debugSource.lineNumber;
|
|
73
|
+
sourceColumn = hostFiber._debugOwner._debugSource.columnNumber;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Walk up fiber return tree to discover composite components and hierarchy
|
|
78
|
+
let curr = hostFiber;
|
|
79
|
+
let depth = 0;
|
|
80
|
+
while (curr && depth < 50) {
|
|
81
|
+
depth++;
|
|
82
|
+
const type = curr.type;
|
|
83
|
+
const name = getReactComponentName(type);
|
|
84
|
+
|
|
85
|
+
if (name && !name.startsWith('html:') && name !== 'Fragment') {
|
|
86
|
+
if (!componentName) {
|
|
87
|
+
componentName = name;
|
|
88
|
+
}
|
|
89
|
+
if (!hierarchy.includes(name)) {
|
|
90
|
+
hierarchy.unshift(name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if composite component has debug source
|
|
94
|
+
if (!sourceFile && curr._debugSource) {
|
|
95
|
+
sourceFile = curr._debugSource.fileName;
|
|
96
|
+
sourceLine = curr._debugSource.lineNumber;
|
|
97
|
+
sourceColumn = curr._debugSource.columnNumber;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Capture safe props from the nearest custom component
|
|
101
|
+
if (!props && curr.memoizedProps && typeof curr.memoizedProps === 'object') {
|
|
102
|
+
props = sanitizeProps(curr.memoizedProps);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
curr = curr.return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!componentName && !sourceFile) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
framework: 'react',
|
|
115
|
+
componentName,
|
|
116
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
117
|
+
sourceLine,
|
|
118
|
+
sourceColumn,
|
|
119
|
+
hierarchy: hierarchy.length > 0 ? hierarchy : componentName ? [componentName] : undefined,
|
|
120
|
+
props,
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getReactComponentName(type: any): string | undefined {
|
|
128
|
+
if (!type) return undefined;
|
|
129
|
+
if (typeof type === 'string') return undefined; // Host DOM tag (div, span, button)
|
|
130
|
+
if (type.displayName) return type.displayName;
|
|
131
|
+
if (type.name) return type.name;
|
|
132
|
+
if (type.render?.displayName) return type.render.displayName;
|
|
133
|
+
if (type.render?.name) return type.render.name;
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extracts Vue 3 and Vue 2 component instances and file paths
|
|
139
|
+
*/
|
|
140
|
+
function resolveVueComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
141
|
+
try {
|
|
142
|
+
// Vue 3
|
|
143
|
+
const vueParent = (el as any).__vueParentComponent || (el as any).__vnode?.ctx;
|
|
144
|
+
if (vueParent) {
|
|
145
|
+
const type = vueParent.type || {};
|
|
146
|
+
const componentName = type.name || type.__name || type.displayName || 'AnonymousComponent';
|
|
147
|
+
const sourceFile = type.__file;
|
|
148
|
+
const hierarchy: string[] = [];
|
|
149
|
+
|
|
150
|
+
let curr = vueParent;
|
|
151
|
+
let depth = 0;
|
|
152
|
+
while (curr && depth < 50) {
|
|
153
|
+
depth++;
|
|
154
|
+
const cType = curr.type || {};
|
|
155
|
+
const cName = cType.name || cType.__name || cType.displayName;
|
|
156
|
+
if (cName && !hierarchy.includes(cName)) {
|
|
157
|
+
hierarchy.unshift(cName);
|
|
158
|
+
}
|
|
159
|
+
curr = curr.parent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
framework: 'vue',
|
|
164
|
+
componentName,
|
|
165
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
166
|
+
hierarchy: hierarchy.length > 0 ? hierarchy : [componentName],
|
|
167
|
+
props: vueParent.props ? sanitizeProps(vueParent.props) : undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Vue 2
|
|
172
|
+
const vue2Instance = (el as any).__vue__;
|
|
173
|
+
if (vue2Instance) {
|
|
174
|
+
const options = vue2Instance.$options || {};
|
|
175
|
+
const componentName = options.name || options._componentTag || 'VueComponent';
|
|
176
|
+
const sourceFile = options.__file;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
framework: 'vue',
|
|
180
|
+
componentName,
|
|
181
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
182
|
+
hierarchy: [componentName],
|
|
183
|
+
props: vue2Instance.$props ? sanitizeProps(vue2Instance.$props) : undefined,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return undefined;
|
|
188
|
+
} catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Extracts Svelte __svelte_meta debug locations
|
|
195
|
+
*/
|
|
196
|
+
function resolveSvelteComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
197
|
+
try {
|
|
198
|
+
let curr: HTMLElement | null = el;
|
|
199
|
+
let depth = 0;
|
|
200
|
+
while (curr && depth < 50) {
|
|
201
|
+
depth++;
|
|
202
|
+
const meta = (curr as any).__svelte_meta;
|
|
203
|
+
if (meta && meta.loc) {
|
|
204
|
+
return {
|
|
205
|
+
framework: 'svelte',
|
|
206
|
+
sourceFile: normalizeFilePath(meta.loc.file),
|
|
207
|
+
sourceLine: meta.loc.line,
|
|
208
|
+
sourceColumn: meta.loc.column,
|
|
209
|
+
componentName: meta.loc.file ? getBaseNameWithoutExt(meta.loc.file) : undefined,
|
|
210
|
+
hierarchy: meta.loc.file ? [getBaseNameWithoutExt(meta.loc.file)] : undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
curr = curr.parentElement;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
} catch {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolves components from standard data attributes (e.g. data-component="UserCard")
|
|
223
|
+
*/
|
|
224
|
+
function resolveDataAttributeComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
225
|
+
try {
|
|
226
|
+
const compEl = el.closest('[data-component], [data-component-name], [data-source-file]');
|
|
227
|
+
if (!compEl) return undefined;
|
|
228
|
+
|
|
229
|
+
const componentName =
|
|
230
|
+
compEl.getAttribute('data-component') || compEl.getAttribute('data-component-name') || undefined;
|
|
231
|
+
const sourceFile = compEl.getAttribute('data-source-file') || undefined;
|
|
232
|
+
const lineAttr = compEl.getAttribute('data-source-line');
|
|
233
|
+
const sourceLine = lineAttr ? parseInt(lineAttr, 10) : undefined;
|
|
234
|
+
|
|
235
|
+
if (!componentName && !sourceFile) return undefined;
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
framework: 'vanilla',
|
|
239
|
+
componentName,
|
|
240
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
241
|
+
sourceLine: isNaN(sourceLine as number) ? undefined : sourceLine,
|
|
242
|
+
hierarchy: componentName ? [componentName] : undefined,
|
|
243
|
+
};
|
|
244
|
+
} catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeFilePath(filePath?: string): string | undefined {
|
|
250
|
+
if (!filePath) return undefined;
|
|
251
|
+
// Strip query strings or vite bundle hashes (e.g. /App.tsx?t=123)
|
|
252
|
+
const cleaned = filePath.split('?')[0];
|
|
253
|
+
return cleaned;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getBaseNameWithoutExt(filePath: string): string {
|
|
257
|
+
const parts = filePath.split('/');
|
|
258
|
+
const last = parts[parts.length - 1] || filePath;
|
|
259
|
+
return last.split('.')[0] || last;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function sanitizeProps(props: Record<string, any>): Record<string, any> {
|
|
263
|
+
const result: Record<string, any> = {};
|
|
264
|
+
for (const [key, value] of Object.entries(props)) {
|
|
265
|
+
if (key.startsWith('__') || typeof value === 'function') continue;
|
|
266
|
+
|
|
267
|
+
if (value === null || value === undefined) {
|
|
268
|
+
result[key] = value;
|
|
269
|
+
} else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
270
|
+
result[key] = value;
|
|
271
|
+
} else if (Array.isArray(value)) {
|
|
272
|
+
result[key] = `Array(${value.length})`;
|
|
273
|
+
} else if (typeof value === 'object') {
|
|
274
|
+
result[key] = '[Object]';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HelloMessage, ClientCommand, CommandResponse } from '../../../core/src/index.js';
|
|
2
|
+
import { safeJsonStringify } from '../../../core/src/index.js';
|
|
2
3
|
import type { ClientCommandHandler } from '../commands/handler.js';
|
|
3
4
|
|
|
4
5
|
export interface TransportOptions {
|
|
@@ -111,15 +112,19 @@ export class WebSocketTransport {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
public send(payload: any): void {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
try {
|
|
116
|
+
const raw = safeJsonStringify(payload);
|
|
117
|
+
if (this.isConnected()) {
|
|
118
|
+
try {
|
|
119
|
+
this.ws!.send(raw);
|
|
120
|
+
} catch {
|
|
121
|
+
this.enqueue(raw);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
119
124
|
this.enqueue(raw);
|
|
120
125
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
126
|
+
} catch {
|
|
127
|
+
// Defensive: never let payload serialization crash caller
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -148,18 +153,18 @@ export class WebSocketTransport {
|
|
|
148
153
|
private sendHello(): void {
|
|
149
154
|
if (typeof window === 'undefined') return;
|
|
150
155
|
|
|
151
|
-
const hello: HelloMessage = {
|
|
152
|
-
type: 'hello',
|
|
153
|
-
origin: window.location.origin,
|
|
154
|
-
url: window.location.href,
|
|
155
|
-
title: document.title,
|
|
156
|
-
userAgent: navigator.userAgent,
|
|
157
|
-
timestamp: Date.now(),
|
|
158
|
-
projectId: this.projectId,
|
|
159
|
-
};
|
|
160
|
-
|
|
161
156
|
try {
|
|
162
|
-
|
|
157
|
+
const hello: HelloMessage = {
|
|
158
|
+
type: 'hello',
|
|
159
|
+
origin: window.location.origin,
|
|
160
|
+
url: window.location.href,
|
|
161
|
+
title: document.title,
|
|
162
|
+
userAgent: navigator.userAgent,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
projectId: this.projectId,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
this.ws!.send(safeJsonStringify(hello));
|
|
163
168
|
} catch {
|
|
164
169
|
// Defensive
|
|
165
170
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserTrack Safety & Error-Resilience Utilities
|
|
3
|
+
* Provides bulletproof try/catch wrappers, circular-safe JSON serialization, and defensive helpers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Safely parses a JSON string with fallback. Never throws.
|
|
8
|
+
*/
|
|
9
|
+
export function safeJsonParse<T>(raw: any, fallback: T): T {
|
|
10
|
+
if (raw === null || raw === undefined) return fallback;
|
|
11
|
+
if (typeof raw !== 'string') return typeof raw === 'object' ? (raw as T) : fallback;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
} catch {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Safely serializes an object to JSON, handling circular references and non-serializable values.
|
|
21
|
+
* Never throws "TypeError: Converting circular structure to JSON".
|
|
22
|
+
*/
|
|
23
|
+
export function safeJsonStringify(val: any, fallback = '{}'): string {
|
|
24
|
+
if (val === undefined) return fallback;
|
|
25
|
+
try {
|
|
26
|
+
const seen = new WeakSet();
|
|
27
|
+
return JSON.stringify(val, (key, value) => {
|
|
28
|
+
if (typeof value === 'object' && value !== null) {
|
|
29
|
+
if (seen.has(value)) {
|
|
30
|
+
return '[Circular]';
|
|
31
|
+
}
|
|
32
|
+
seen.add(value);
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'bigint') {
|
|
35
|
+
return value.toString();
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(String(val));
|
|
42
|
+
} catch {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Executes a synchronous function within an isolated try/catch block.
|
|
50
|
+
* Returns the function result, or fallback if an exception was thrown.
|
|
51
|
+
*/
|
|
52
|
+
export function safeExecute<T>(fn: () => T, fallback: T, onError?: (err: any) => void): T {
|
|
53
|
+
try {
|
|
54
|
+
return fn();
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
if (onError) {
|
|
57
|
+
try {
|
|
58
|
+
onError(err);
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes an asynchronous promise or async function safely without unhandled rejection.
|
|
67
|
+
*/
|
|
68
|
+
export async function safeAsync<T>(
|
|
69
|
+
action: Promise<T> | (() => Promise<T>),
|
|
70
|
+
fallback: T,
|
|
71
|
+
onError?: (err: any) => void
|
|
72
|
+
): Promise<T> {
|
|
73
|
+
try {
|
|
74
|
+
if (typeof action === 'function') {
|
|
75
|
+
return await action();
|
|
76
|
+
}
|
|
77
|
+
return await action;
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
if (onError) {
|
|
80
|
+
try {
|
|
81
|
+
onError(err);
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ComponentSourceInfo } from './notes.js';
|
|
2
|
+
|
|
1
3
|
export type EventType =
|
|
2
4
|
| 'runtime_error'
|
|
3
5
|
| 'unhandled_rejection'
|
|
@@ -17,6 +19,7 @@ export interface ElementSummary {
|
|
|
17
19
|
attributes?: Record<string, string>;
|
|
18
20
|
outerHTML?: string;
|
|
19
21
|
innerText?: string;
|
|
22
|
+
componentSource?: ComponentSourceInfo;
|
|
20
23
|
boundingRect?: {
|
|
21
24
|
x: number;
|
|
22
25
|
y: number;
|
|
@@ -39,12 +39,23 @@ export interface NoteTarget {
|
|
|
39
39
|
confidence?: 'high' | 'medium' | 'low';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface ComponentSourceInfo {
|
|
43
|
+
framework?: 'react' | 'vue' | 'svelte' | 'web-component' | 'vanilla';
|
|
44
|
+
componentName?: string;
|
|
45
|
+
sourceFile?: string;
|
|
46
|
+
sourceLine?: number;
|
|
47
|
+
sourceColumn?: number;
|
|
48
|
+
hierarchy?: string[];
|
|
49
|
+
props?: Record<string, any>;
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export interface ElementContext {
|
|
43
53
|
selector: string;
|
|
44
54
|
tag: string;
|
|
45
55
|
attributes?: Record<string, string>;
|
|
46
56
|
outerHTML?: string;
|
|
47
57
|
innerText?: string;
|
|
58
|
+
componentSource?: ComponentSourceInfo;
|
|
48
59
|
parent?: {
|
|
49
60
|
selector: string;
|
|
50
61
|
tag: string;
|
|
@@ -40,7 +40,13 @@ export class BrowserTrackDaemon {
|
|
|
40
40
|
public async start(): Promise<void> {
|
|
41
41
|
if (this.isRunning) return;
|
|
42
42
|
|
|
43
|
-
const httpHandler = createHttpHandler(
|
|
43
|
+
const httpHandler = createHttpHandler(
|
|
44
|
+
this.db,
|
|
45
|
+
this.sessionManager,
|
|
46
|
+
this.config.screenshotsDir,
|
|
47
|
+
this.verificationEngine,
|
|
48
|
+
this.noteVerificationEngine
|
|
49
|
+
);
|
|
44
50
|
this.httpServer = http.createServer(httpHandler);
|
|
45
51
|
|
|
46
52
|
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
@@ -4,11 +4,59 @@ import path from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import type { StorageDB } from '../storage/db.js';
|
|
6
6
|
import type { SessionManager } from '../session/manager.js';
|
|
7
|
+
import type { VerificationEngine } from '../verification/engine.js';
|
|
8
|
+
import type { NoteVerificationEngine } from '../notes/verification.js';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
function readJsonBody(req: http.IncomingMessage, res: http.ServerResponse, maxSizeBytes = 10 * 1024 * 1024): Promise<any> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let body = '';
|
|
13
|
+
let isTooLarge = false;
|
|
14
|
+
|
|
15
|
+
req.on('data', (chunk) => {
|
|
16
|
+
if (isTooLarge) return;
|
|
17
|
+
body += chunk;
|
|
18
|
+
if (body.length > maxSizeBytes) {
|
|
19
|
+
isTooLarge = true;
|
|
20
|
+
if (!res.headersSent) {
|
|
21
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
22
|
+
res.end(JSON.stringify({ ok: false, error: 'Payload too large' }));
|
|
23
|
+
}
|
|
24
|
+
req.destroy();
|
|
25
|
+
reject(new Error('Payload too large'));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
req.on('end', () => {
|
|
30
|
+
if (isTooLarge) return;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
33
|
+
resolve(parsed);
|
|
34
|
+
} catch (err: any) {
|
|
35
|
+
if (!res.headersSent) {
|
|
36
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
37
|
+
res.end(JSON.stringify({ ok: false, error: 'Malformed JSON payload' }));
|
|
38
|
+
}
|
|
39
|
+
reject(err);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
req.on('error', (err) => {
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createHttpHandler(
|
|
50
|
+
db: StorageDB,
|
|
51
|
+
sessionManager: SessionManager,
|
|
52
|
+
baseScreenshotsDir: string,
|
|
53
|
+
verificationEngine?: VerificationEngine,
|
|
54
|
+
noteVerificationEngine?: NoteVerificationEngine
|
|
55
|
+
) {
|
|
56
|
+
return async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
57
|
+
try {
|
|
58
|
+
const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
59
|
+
const pathname = parsedUrl.pathname;
|
|
12
60
|
|
|
13
61
|
// CORS headers for local development access
|
|
14
62
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -158,8 +206,80 @@ export function createHttpHandler(db: StorageDB, sessionManager: SessionManager,
|
|
|
158
206
|
return;
|
|
159
207
|
}
|
|
160
208
|
|
|
209
|
+
// 10. API - Dispatch Live Command to Browser Session
|
|
210
|
+
if (pathname === '/api/command' && req.method === 'POST') {
|
|
211
|
+
try {
|
|
212
|
+
const data = await readJsonBody(req, res);
|
|
213
|
+
const sessionId = data.sessionId || sessionManager.getAnyActiveSession()?.id;
|
|
214
|
+
if (!sessionId) {
|
|
215
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
216
|
+
res.end(JSON.stringify({ ok: false, error: 'No active browser session connected' }));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const cmdRes = await sessionManager.sendCommand(sessionId, data.command, data.timeoutMs || 5000);
|
|
220
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
221
|
+
res.end(JSON.stringify(cmdRes));
|
|
222
|
+
} catch (err: any) {
|
|
223
|
+
if (!res.headersSent) {
|
|
224
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
225
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 11. API - Verify Incident
|
|
232
|
+
if (pathname === '/api/verify/incident' && req.method === 'POST') {
|
|
233
|
+
try {
|
|
234
|
+
if (!verificationEngine) {
|
|
235
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
236
|
+
res.end(JSON.stringify({ ok: false, error: 'Verification engine not attached to daemon' }));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const data = await readJsonBody(req, res);
|
|
240
|
+
const result = await verificationEngine.verifyIncident(data.incidentId, data.options);
|
|
241
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({ ok: true, result }));
|
|
243
|
+
} catch (err: any) {
|
|
244
|
+
if (!res.headersSent) {
|
|
245
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
246
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 12. API - Verify Note
|
|
253
|
+
if (pathname === '/api/verify/note' && req.method === 'POST') {
|
|
254
|
+
try {
|
|
255
|
+
if (!noteVerificationEngine) {
|
|
256
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
257
|
+
res.end(JSON.stringify({ ok: false, error: 'Note verification engine not attached to daemon' }));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const data = await readJsonBody(req, res);
|
|
261
|
+
const result = await noteVerificationEngine.verifyNote(data.noteId, data.options);
|
|
262
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify({ ok: true, result }));
|
|
264
|
+
} catch (err: any) {
|
|
265
|
+
if (!res.headersSent) {
|
|
266
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
267
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
161
273
|
// Not found
|
|
162
274
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
163
275
|
res.end(JSON.stringify({ ok: false, error: 'Not found' }));
|
|
164
|
-
}
|
|
276
|
+
} catch (err: any) {
|
|
277
|
+
if (!res.headersSent) {
|
|
278
|
+
try {
|
|
279
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
280
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || 'Internal Server Error' }));
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
165
285
|
}
|