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.
Files changed (67) hide show
  1. package/AGENTS.md +11 -6
  2. package/README.md +2 -0
  3. package/dist/{chunk-WB7ZKWK7.js → chunk-4HRLW6YF.js} +509 -109
  4. package/dist/chunk-4HRLW6YF.js.map +1 -0
  5. package/dist/{chunk-3HOXPTM2.js → chunk-AYSVE6NG.js} +808 -53
  6. package/dist/chunk-AYSVE6NG.js.map +1 -0
  7. package/dist/{chunk-6VA7GBAO.js → chunk-QRZ57ME3.js} +70 -2
  8. package/dist/chunk-QRZ57ME3.js.map +1 -0
  9. package/dist/{chunk-UP5JKCFY.js → chunk-TWEYRBDU.js} +281 -44
  10. package/dist/chunk-TWEYRBDU.js.map +1 -0
  11. package/dist/cli/index.js +1040 -546
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/client/index.cjs +536 -109
  14. package/dist/client/index.d.ts +36 -4
  15. package/dist/client/index.js +8 -4
  16. package/dist/client.iife.js +42 -19
  17. package/dist/{notes-BMnonq46.d.ts → commands-fjuqKzkm.d.ts} +125 -114
  18. package/dist/core/index.d.ts +26 -3
  19. package/dist/core/index.js +9 -1
  20. package/dist/daemon/index.d.ts +4 -4
  21. package/dist/daemon/index.js +6 -8
  22. package/dist/{engine-B43IohQY.d.ts → engine-CmchnMDq.d.ts} +2 -2
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +14 -7
  25. package/dist/mcp/index.d.ts +4 -4
  26. package/dist/mcp/index.js +7 -4
  27. package/dist/{projects-D5J-egVN.d.ts → projects-DB7S312i.d.ts} +1 -1
  28. package/dist/{server-BztYp1Zc.d.ts → server-DjV7RWQM.d.ts} +10 -2
  29. package/docs/cli.md +4 -1
  30. package/docs/component-resolver.md +108 -0
  31. package/docs/getting-started.md +60 -6
  32. package/docs/index.md +1 -0
  33. package/docs/mcp-reference.md +44 -2
  34. package/docs/visual-notes.md +36 -0
  35. package/package.json +1 -1
  36. package/packages/cli/src/index.ts +247 -151
  37. package/packages/client/src/client.ts +18 -1
  38. package/packages/client/src/config.ts +84 -1
  39. package/packages/client/src/index.ts +4 -1
  40. package/packages/client/src/interceptors/interaction.ts +3 -0
  41. package/packages/client/src/interceptors/navigation.ts +38 -26
  42. package/packages/client/src/interceptors/network.ts +22 -17
  43. package/packages/client/src/notes/inspector.ts +186 -51
  44. package/packages/client/src/source/resolver.ts +278 -0
  45. package/packages/client/src/transport/websocket.ts +23 -18
  46. package/packages/core/src/index.ts +1 -0
  47. package/packages/core/src/safety.ts +86 -0
  48. package/packages/core/src/types/events.ts +3 -0
  49. package/packages/core/src/types/notes.ts +11 -0
  50. package/packages/daemon/src/server/daemon.ts +7 -1
  51. package/packages/daemon/src/server/http.ts +125 -5
  52. package/packages/daemon/src/server/ws.ts +33 -29
  53. package/packages/daemon/src/storage/db.ts +57 -35
  54. package/packages/mcp/src/handlers.ts +115 -45
  55. package/packages/mcp/src/server.ts +202 -2
  56. package/test/client/component-resolver.test.ts +141 -0
  57. package/test/client/interceptors.test.ts +56 -0
  58. package/test/core/safety.test.ts +106 -0
  59. package/test/daemon/storage.test.ts +36 -0
  60. package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
  61. package/test/mcp/auto-start.test.ts +87 -0
  62. package/dist/chunk-3HOXPTM2.js.map +0 -1
  63. package/dist/chunk-6VA7GBAO.js.map +0 -1
  64. package/dist/chunk-7OCOQGDN.js +0 -635
  65. package/dist/chunk-7OCOQGDN.js.map +0 -1
  66. package/dist/chunk-UP5JKCFY.js.map +0 -1
  67. 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
- const raw = JSON.stringify(payload);
115
- if (this.isConnected()) {
116
- try {
117
- this.ws!.send(raw);
118
- } catch {
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
- } else {
122
- this.enqueue(raw);
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
- this.ws!.send(JSON.stringify(hello));
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
  }
@@ -7,3 +7,4 @@ export * from './types/notes.js';
7
7
  export * from './fingerprint.js';
8
8
  export * from './redaction.js';
9
9
  export * from './selector.js';
10
+ export * from './safety.js';
@@ -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(this.db, this.sessionManager, this.config.screenshotsDir);
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
- export function createHttpHandler(db: StorageDB, sessionManager: SessionManager, baseScreenshotsDir: string) {
9
- return (req: http.IncomingMessage, res: http.ServerResponse) => {
10
- const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
11
- const pathname = parsedUrl.pathname;
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
  }