sessioncast-cli 2.2.2 → 2.3.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.
@@ -170,9 +170,23 @@ class ApiWebSocketClient {
170
170
  try {
171
171
  const payload = meta.payload ? JSON.parse(meta.payload) : {};
172
172
  const { model, messages, temperature, max_tokens, stream } = payload;
173
- console.log(`[API] llm_chat: model=${model}, messages=${messages?.length || 0}`);
174
- const result = await this.llmService.chat(model, messages, temperature, max_tokens, stream);
175
- this.sendApiResponse(meta.requestId, result);
173
+ console.log(`[API] llm_chat: model=${model}, messages=${messages?.length || 0}, stream=${!!stream}`);
174
+ if (stream) {
175
+ const result = await this.llmService.chatStream(model, messages, temperature, max_tokens, (chunk) => {
176
+ this.send({
177
+ type: 'api_response_stream',
178
+ meta: {
179
+ requestId: meta.requestId,
180
+ payload: JSON.stringify(chunk)
181
+ }
182
+ });
183
+ });
184
+ this.sendApiResponse(meta.requestId, result);
185
+ }
186
+ else {
187
+ const result = await this.llmService.chat(model, messages, temperature, max_tokens, stream);
188
+ this.sendApiResponse(meta.requestId, result);
189
+ }
176
190
  }
177
191
  catch (error) {
178
192
  this.sendApiResponse(meta.requestId, {
@@ -0,0 +1,100 @@
1
+ export interface BrowserStartOptions {
2
+ url?: string;
3
+ width?: number;
4
+ height?: number;
5
+ chromePath?: string;
6
+ }
7
+ export interface DomEvent {
8
+ type: number;
9
+ data: any;
10
+ timestamp: number;
11
+ }
12
+ type DomEventCallback = (event: DomEvent) => void;
13
+ /**
14
+ * Manages a headless Chrome instance with rrweb-based DOM recording.
15
+ * Spawns Chrome with --headless=new, connects via CDP, injects rrweb,
16
+ * and streams DOM mutation events through a callback.
17
+ */
18
+ export declare class BrowserHandler {
19
+ private chrome;
20
+ private cdp;
21
+ private domCallback;
22
+ private eventBuffer;
23
+ private running;
24
+ /**
25
+ * Launch headless Chrome and connect CDP.
26
+ * @param port Remote debugging port for Chrome
27
+ * @param opts Optional browser configuration
28
+ */
29
+ start(port: number, opts?: BrowserStartOptions): Promise<void>;
30
+ /**
31
+ * Register a callback for rrweb DOM events.
32
+ * Flushes any events buffered before the callback was registered.
33
+ */
34
+ onDomEvent(cb: DomEventCallback): void;
35
+ /**
36
+ * Dispatch an input event to the page via CDP.
37
+ * Accepts WebInputEvent from the web client (inputType field).
38
+ */
39
+ dispatchInput(event: {
40
+ inputType?: string;
41
+ type?: string;
42
+ x?: number;
43
+ y?: number;
44
+ button?: string;
45
+ clickCount?: number;
46
+ key?: string;
47
+ code?: string;
48
+ text?: string;
49
+ modifiers?: number;
50
+ deltaX?: number;
51
+ deltaY?: number;
52
+ }): Promise<void>;
53
+ /**
54
+ * Trigger rrweb to emit a fresh FullSnapshot by restarting the recording.
55
+ * Stops the current recording and starts a new one, which immediately
56
+ * produces a FullSnapshot of the current DOM state.
57
+ */
58
+ takeFullSnapshot(): Promise<void>;
59
+ /**
60
+ * Navigate the browser to a new URL.
61
+ * Only localhost/127.0.0.1 URLs are allowed for security.
62
+ */
63
+ navigate(url: string): Promise<void>;
64
+ /**
65
+ * Stop Chrome and close the CDP connection.
66
+ */
67
+ stop(): void;
68
+ /**
69
+ * Check whether the browser is currently running.
70
+ */
71
+ isRunning(): boolean;
72
+ /**
73
+ * Wait for Chrome to output the DevTools WebSocket URL on stderr.
74
+ * Falls back to querying the /json/version endpoint.
75
+ */
76
+ private waitForDebugUrl;
77
+ /**
78
+ * Fetch the DevTools WebSocket URL via the /json/version HTTP endpoint.
79
+ * Returns the browser-level WebSocket URL.
80
+ */
81
+ private fetchDebugUrl;
82
+ /**
83
+ * Find the page-level WebSocket target via /json endpoint.
84
+ * Page targets support Page.enable, Runtime.evaluate, etc.
85
+ * The browser-level target only supports browser-wide commands.
86
+ */
87
+ private findPageTarget;
88
+ /**
89
+ * Build the rrweb injection script source.
90
+ * Used by both addScriptToEvaluateOnNewDocument and Runtime.evaluate.
91
+ */
92
+ private getRrwebInjectionScript;
93
+ /**
94
+ * Inject rrweb recording script into the current page.
95
+ * The script loads rrweb from CDN, then starts recording,
96
+ * emitting events through the __sessioncast_emit CDP binding.
97
+ */
98
+ private injectRrweb;
99
+ }
100
+ export {};
@@ -0,0 +1,485 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BrowserHandler = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const chrome_finder_1 = require("./chrome-finder");
39
+ const cdp_client_1 = require("./cdp-client");
40
+ const debug_1 = require("./debug");
41
+ // rrweb CDN — v1.x reliably exposes global `rrweb.record` via <script> tag.
42
+ // The v2 Replayer (used in the web client) is backward-compatible with v1 events.
43
+ const RRWEB_CDN = 'https://cdn.jsdelivr.net/npm/rrweb@1.1.3/dist/rrweb.min.js';
44
+ /**
45
+ * Manages a headless Chrome instance with rrweb-based DOM recording.
46
+ * Spawns Chrome with --headless=new, connects via CDP, injects rrweb,
47
+ * and streams DOM mutation events through a callback.
48
+ */
49
+ class BrowserHandler {
50
+ constructor() {
51
+ this.chrome = null;
52
+ this.cdp = null;
53
+ this.domCallback = null;
54
+ this.eventBuffer = [];
55
+ this.running = false;
56
+ }
57
+ /**
58
+ * Launch headless Chrome and connect CDP.
59
+ * @param port Remote debugging port for Chrome
60
+ * @param opts Optional browser configuration
61
+ */
62
+ async start(port, opts = {}) {
63
+ const chromePath = opts.chromePath || (0, chrome_finder_1.findChromePath)();
64
+ if (!chromePath) {
65
+ throw new Error('Chrome/Chromium not found. Install Chrome or pass chromePath in options.');
66
+ }
67
+ const width = opts.width || 1280;
68
+ const height = opts.height || 720;
69
+ const url = opts.url || 'about:blank';
70
+ const args = [
71
+ '--headless=new',
72
+ `--remote-debugging-port=${port}`,
73
+ `--window-size=${width},${height}`,
74
+ '--disable-gpu',
75
+ '--no-first-run',
76
+ '--no-default-browser-check',
77
+ '--disable-extensions',
78
+ '--disable-background-networking',
79
+ '--disable-sync',
80
+ '--disable-translate',
81
+ '--mute-audio',
82
+ url,
83
+ ];
84
+ (0, debug_1.debugLog)('Browser', 'Launching', chromePath, args.join(' '));
85
+ this.chrome = (0, child_process_1.spawn)(chromePath, args, {
86
+ stdio: ['ignore', 'pipe', 'pipe'],
87
+ });
88
+ // Wait for Chrome to be ready, then find the page-level WebSocket target.
89
+ // We must connect to a page target (not the browser target) for Page.enable to work.
90
+ await this.waitForDebugUrl(port);
91
+ const pageUrl = await this.findPageTarget(port);
92
+ (0, debug_1.debugLog)('Browser', 'Page DevTools URL:', pageUrl);
93
+ // Connect CDP to the page target
94
+ this.cdp = new cdp_client_1.CdpClient();
95
+ await this.cdp.connect(pageUrl);
96
+ this.running = true;
97
+ // Enable required CDP domains
98
+ await this.cdp.send('Page.enable');
99
+ await this.cdp.send('Runtime.enable');
100
+ // Set up the binding that rrweb will call to emit events
101
+ await this.cdp.send('Runtime.addBinding', {
102
+ name: '__sessioncast_emit',
103
+ });
104
+ // Listen for the binding call — buffer events if callback isn't registered yet
105
+ this.cdp.on('Runtime.bindingCalled', (params) => {
106
+ if (params.name === '__sessioncast_emit') {
107
+ try {
108
+ const event = JSON.parse(params.payload);
109
+ if (this.domCallback) {
110
+ this.domCallback(event);
111
+ }
112
+ else {
113
+ this.eventBuffer.push(event);
114
+ }
115
+ }
116
+ catch (e) {
117
+ (0, debug_1.debugLog)('Browser', 'Failed to parse DOM event:', e.message);
118
+ }
119
+ }
120
+ });
121
+ // Detect page navigation → take fresh FullSnapshot for viewers.
122
+ // addScriptToEvaluateOnNewDocument handles rrweb re-injection,
123
+ // but binding-based event delivery is unreliable after navigation.
124
+ // This handler uses the direct-capture approach instead.
125
+ let navTimer = null;
126
+ this.cdp.on('Page.frameNavigated', (params) => {
127
+ if (params.frame?.parentId)
128
+ return; // skip iframes
129
+ (0, debug_1.debugLog)('Browser', 'Navigation detected:', params.frame?.url);
130
+ if (navTimer)
131
+ clearTimeout(navTimer);
132
+ navTimer = setTimeout(async () => {
133
+ if (!this.cdp || !this.running)
134
+ return;
135
+ try {
136
+ // Re-add binding for the new execution context
137
+ try {
138
+ await this.cdp.send('Runtime.addBinding', { name: '__sessioncast_emit' });
139
+ }
140
+ catch { /* binding may already exist */ }
141
+ // Wait for document to be ready
142
+ await this.cdp.send('Runtime.evaluate', {
143
+ expression: `new Promise(function(resolve) {
144
+ if (document.readyState !== 'loading') resolve();
145
+ else document.addEventListener('DOMContentLoaded', resolve);
146
+ })`,
147
+ awaitPromise: true,
148
+ });
149
+ // addScriptToEvaluateOnNewDocument may have set the flag but failed
150
+ // to load rrweb CDN. Reset and manually inject.
151
+ await this.cdp.send('Runtime.evaluate', {
152
+ expression: 'window.__sessioncast_recording = false;',
153
+ });
154
+ await this.injectRrweb();
155
+ if (!this.running)
156
+ return;
157
+ await this.takeFullSnapshot();
158
+ }
159
+ catch (e) {
160
+ (0, debug_1.debugLog)('Browser', 'Post-navigation handler failed:', e.message);
161
+ }
162
+ }, 500);
163
+ });
164
+ // Use addScriptToEvaluateOnNewDocument so rrweb is injected
165
+ // on every navigation (including the initial load) automatically.
166
+ // This is more reliable than listening for frameNavigated.
167
+ await this.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
168
+ source: this.getRrwebInjectionScript(),
169
+ });
170
+ // Also inject into the current page immediately (for already-loaded pages)
171
+ await this.injectRrweb();
172
+ // Handle Chrome exit
173
+ this.chrome.on('exit', (code) => {
174
+ (0, debug_1.debugLog)('Browser', 'Chrome exited with code', code);
175
+ this.running = false;
176
+ });
177
+ }
178
+ /**
179
+ * Register a callback for rrweb DOM events.
180
+ * Flushes any events buffered before the callback was registered.
181
+ */
182
+ onDomEvent(cb) {
183
+ this.domCallback = cb;
184
+ // Flush buffered events (e.g. FullSnapshot emitted during start())
185
+ if (this.eventBuffer.length > 0) {
186
+ const buffered = this.eventBuffer;
187
+ this.eventBuffer = [];
188
+ for (const event of buffered) {
189
+ cb(event);
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Dispatch an input event to the page via CDP.
195
+ * Accepts WebInputEvent from the web client (inputType field).
196
+ */
197
+ async dispatchInput(event) {
198
+ if (!this.cdp || !this.running)
199
+ return;
200
+ // Support both 'inputType' (from web client) and 'type' (legacy)
201
+ const inputType = event.inputType || event.type || '';
202
+ if (inputType === 'mousePressed' || inputType === 'mouseReleased' || inputType === 'mouseMoved') {
203
+ // CDP expects button: 'none' for mouseMoved, 'left'/'right'/'middle' for press/release
204
+ const button = inputType === 'mouseMoved' ? 'none' : (event.button || 'left');
205
+ await this.cdp.send('Input.dispatchMouseEvent', {
206
+ type: inputType,
207
+ x: event.x || 0,
208
+ y: event.y || 0,
209
+ button,
210
+ clickCount: event.clickCount || (inputType === 'mousePressed' ? 1 : 0),
211
+ modifiers: event.modifiers || 0,
212
+ });
213
+ }
214
+ else if (inputType === 'mouseWheel') {
215
+ await this.cdp.send('Input.dispatchMouseEvent', {
216
+ type: 'mouseWheel',
217
+ x: event.x || 0,
218
+ y: event.y || 0,
219
+ deltaX: event.deltaX || 0,
220
+ deltaY: event.deltaY || 0,
221
+ modifiers: event.modifiers || 0,
222
+ });
223
+ }
224
+ else if (inputType === 'keyDown' || inputType === 'keyUp') {
225
+ await this.cdp.send('Input.dispatchKeyEvent', {
226
+ type: inputType,
227
+ key: event.key || '',
228
+ code: event.code || '',
229
+ text: event.text || (inputType === 'keyDown' && event.key?.length === 1 ? event.key : ''),
230
+ modifiers: event.modifiers || 0,
231
+ });
232
+ }
233
+ }
234
+ /**
235
+ * Trigger rrweb to emit a fresh FullSnapshot by restarting the recording.
236
+ * Stops the current recording and starts a new one, which immediately
237
+ * produces a FullSnapshot of the current DOM state.
238
+ */
239
+ async takeFullSnapshot() {
240
+ if (!this.cdp || !this.running)
241
+ return;
242
+ try {
243
+ // Stop existing recording, restart, and collect the FullSnapshot
244
+ // all within Chrome's JS context, then return the snapshot data.
245
+ const result = await this.cdp.send('Runtime.evaluate', {
246
+ expression: `
247
+ (function() {
248
+ // Stop existing recording
249
+ if (window.__sessioncast_stopFn) {
250
+ window.__sessioncast_stopFn();
251
+ window.__sessioncast_stopFn = null;
252
+ }
253
+ // Capture the FullSnapshot synchronously
254
+ var captured = null;
255
+ if (typeof rrweb !== 'undefined' && rrweb.record) {
256
+ window.__sessioncast_stopFn = rrweb.record({
257
+ emit: function(event) {
258
+ if (event.type === 2 && !captured) {
259
+ captured = JSON.stringify(event);
260
+ }
261
+ try { __sessioncast_emit(JSON.stringify(event)); } catch(e) {}
262
+ },
263
+ sampling: {
264
+ mousemove: 50,
265
+ mouseInteraction: true,
266
+ scroll: 150,
267
+ input: 'last',
268
+ },
269
+ });
270
+ }
271
+ return captured || '';
272
+ })();
273
+ `,
274
+ returnByValue: true,
275
+ });
276
+ const snapshotJson = result?.result?.value;
277
+ if (snapshotJson && snapshotJson.length > 0) {
278
+ (0, debug_1.debugLog)('Browser', 'takeFullSnapshot: captured', snapshotJson.length, 'bytes, emitting via binding');
279
+ // The binding call inside evaluate might be queued —
280
+ // also emit directly from Node.js side as fallback
281
+ if (this.domCallback) {
282
+ try {
283
+ const event = JSON.parse(snapshotJson);
284
+ this.domCallback(event);
285
+ }
286
+ catch (e) {
287
+ (0, debug_1.debugLog)('Browser', 'takeFullSnapshot: parse error', e.message);
288
+ }
289
+ }
290
+ }
291
+ else {
292
+ (0, debug_1.debugLog)('Browser', 'takeFullSnapshot: no snapshot captured (rrweb may not be loaded)');
293
+ }
294
+ }
295
+ catch (e) {
296
+ (0, debug_1.debugLog)('Browser', 'takeFullSnapshot failed:', e.message);
297
+ }
298
+ }
299
+ /**
300
+ * Navigate the browser to a new URL.
301
+ * Only localhost/127.0.0.1 URLs are allowed for security.
302
+ */
303
+ async navigate(url) {
304
+ if (!this.cdp || !this.running) {
305
+ throw new Error('Browser is not running');
306
+ }
307
+ // Security: only allow localhost navigation
308
+ try {
309
+ const parsed = new URL(url);
310
+ if (parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
311
+ throw new Error(`Navigation blocked: only localhost URLs are allowed (got ${parsed.hostname})`);
312
+ }
313
+ }
314
+ catch (e) {
315
+ if (e.message.includes('Navigation blocked'))
316
+ throw e;
317
+ throw new Error(`Invalid URL: ${url}`);
318
+ }
319
+ (0, debug_1.debugLog)('Browser', 'Navigating to', url);
320
+ await this.cdp.send('Page.navigate', { url });
321
+ }
322
+ /**
323
+ * Stop Chrome and close the CDP connection.
324
+ */
325
+ stop() {
326
+ this.running = false;
327
+ if (this.cdp) {
328
+ this.cdp.close();
329
+ this.cdp = null;
330
+ }
331
+ if (this.chrome) {
332
+ this.chrome.kill('SIGTERM');
333
+ this.chrome = null;
334
+ }
335
+ this.domCallback = null;
336
+ (0, debug_1.debugLog)('Browser', 'Stopped');
337
+ }
338
+ /**
339
+ * Check whether the browser is currently running.
340
+ */
341
+ isRunning() {
342
+ return this.running;
343
+ }
344
+ // -- Private methods --
345
+ /**
346
+ * Wait for Chrome to output the DevTools WebSocket URL on stderr.
347
+ * Falls back to querying the /json/version endpoint.
348
+ */
349
+ waitForDebugUrl(port) {
350
+ return new Promise((resolve, reject) => {
351
+ const timeout = setTimeout(() => {
352
+ // Fallback: query the HTTP endpoint
353
+ this.fetchDebugUrl(port).then(resolve).catch(reject);
354
+ }, 5000);
355
+ if (!this.chrome?.stderr) {
356
+ clearTimeout(timeout);
357
+ // No stderr available, try HTTP fallback directly
358
+ setTimeout(() => {
359
+ this.fetchDebugUrl(port).then(resolve).catch(reject);
360
+ }, 1000);
361
+ return;
362
+ }
363
+ let buffer = '';
364
+ const onData = (data) => {
365
+ buffer += data.toString();
366
+ // Chrome outputs: DevTools listening on ws://127.0.0.1:PORT/devtools/browser/UUID
367
+ const match = buffer.match(/DevTools listening on (ws:\/\/[^\s]+)/);
368
+ if (match) {
369
+ clearTimeout(timeout);
370
+ this.chrome?.stderr?.removeListener('data', onData);
371
+ resolve(match[1]);
372
+ }
373
+ };
374
+ this.chrome.stderr.on('data', onData);
375
+ this.chrome.on('exit', (code) => {
376
+ clearTimeout(timeout);
377
+ reject(new Error(`Chrome exited unexpectedly with code ${code}`));
378
+ });
379
+ });
380
+ }
381
+ /**
382
+ * Fetch the DevTools WebSocket URL via the /json/version HTTP endpoint.
383
+ * Returns the browser-level WebSocket URL.
384
+ */
385
+ async fetchDebugUrl(port) {
386
+ const fetch = (await Promise.resolve().then(() => __importStar(require('node-fetch')))).default;
387
+ const maxRetries = 5;
388
+ for (let i = 0; i < maxRetries; i++) {
389
+ try {
390
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`);
391
+ const json = (await res.json());
392
+ if (json.webSocketDebuggerUrl) {
393
+ return json.webSocketDebuggerUrl;
394
+ }
395
+ }
396
+ catch {
397
+ // Chrome may not be ready yet
398
+ }
399
+ await new Promise((r) => setTimeout(r, 500));
400
+ }
401
+ throw new Error(`Could not get DevTools URL from Chrome on port ${port}`);
402
+ }
403
+ /**
404
+ * Find the page-level WebSocket target via /json endpoint.
405
+ * Page targets support Page.enable, Runtime.evaluate, etc.
406
+ * The browser-level target only supports browser-wide commands.
407
+ */
408
+ async findPageTarget(port) {
409
+ const fetch = (await Promise.resolve().then(() => __importStar(require('node-fetch')))).default;
410
+ const maxRetries = 10;
411
+ for (let i = 0; i < maxRetries; i++) {
412
+ try {
413
+ const res = await fetch(`http://127.0.0.1:${port}/json`);
414
+ const targets = (await res.json());
415
+ const page = targets.find(t => t.type === 'page');
416
+ if (page?.webSocketDebuggerUrl) {
417
+ return page.webSocketDebuggerUrl;
418
+ }
419
+ }
420
+ catch {
421
+ // Chrome may not be ready yet
422
+ }
423
+ await new Promise((r) => setTimeout(r, 500));
424
+ }
425
+ throw new Error(`Could not find page target on Chrome port ${port}`);
426
+ }
427
+ /**
428
+ * Build the rrweb injection script source.
429
+ * Used by both addScriptToEvaluateOnNewDocument and Runtime.evaluate.
430
+ */
431
+ getRrwebInjectionScript() {
432
+ return `
433
+ (async function() {
434
+ if (window.__sessioncast_recording) return;
435
+ window.__sessioncast_recording = true;
436
+
437
+ // Load rrweb from CDN
438
+ await new Promise((resolve, reject) => {
439
+ const s = document.createElement('script');
440
+ s.src = '${RRWEB_CDN}';
441
+ s.onload = resolve;
442
+ s.onerror = reject;
443
+ (document.head || document.documentElement).appendChild(s);
444
+ });
445
+
446
+ // Start recording (save stop function for later restart)
447
+ if (typeof rrweb !== 'undefined' && rrweb.record) {
448
+ window.__sessioncast_stopFn = rrweb.record({
449
+ emit(event) {
450
+ try {
451
+ __sessioncast_emit(JSON.stringify(event));
452
+ } catch(e) { /* binding may be gone */ }
453
+ },
454
+ sampling: {
455
+ mousemove: 50,
456
+ mouseInteraction: true,
457
+ scroll: 150,
458
+ input: 'last',
459
+ },
460
+ });
461
+ }
462
+ })();
463
+ `;
464
+ }
465
+ /**
466
+ * Inject rrweb recording script into the current page.
467
+ * The script loads rrweb from CDN, then starts recording,
468
+ * emitting events through the __sessioncast_emit CDP binding.
469
+ */
470
+ async injectRrweb() {
471
+ if (!this.cdp || !this.running)
472
+ return;
473
+ try {
474
+ await this.cdp.send('Runtime.evaluate', {
475
+ expression: this.getRrwebInjectionScript(),
476
+ awaitPromise: true,
477
+ });
478
+ (0, debug_1.debugLog)('Browser', 'rrweb injected successfully');
479
+ }
480
+ catch (e) {
481
+ (0, debug_1.debugLog)('Browser', 'rrweb injection failed:', e.message);
482
+ }
483
+ }
484
+ }
485
+ exports.BrowserHandler = BrowserHandler;
@@ -0,0 +1,28 @@
1
+ import { EventEmitter } from 'events';
2
+ /**
3
+ * Lightweight Chrome DevTools Protocol client.
4
+ * Uses the existing `ws` package — no additional dependencies.
5
+ */
6
+ export declare class CdpClient extends EventEmitter {
7
+ private ws;
8
+ private nextId;
9
+ private callbacks;
10
+ private connected;
11
+ /**
12
+ * Connect to a Chrome DevTools WebSocket URL.
13
+ * URL is obtained from Chrome's --remote-debugging-port output.
14
+ */
15
+ connect(browserDebugUrl: string): Promise<void>;
16
+ /**
17
+ * Send a CDP command and wait for the response.
18
+ */
19
+ send(method: string, params?: Record<string, any>): Promise<any>;
20
+ /**
21
+ * Check if connected.
22
+ */
23
+ isConnected(): boolean;
24
+ /**
25
+ * Close the CDP connection.
26
+ */
27
+ close(): void;
28
+ }