@alfe.ai/browser 0.0.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/dist/index.cjs ADDED
@@ -0,0 +1,600 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let _alfe_ai_remote = require("@alfe.ai/remote");
25
+ let puppeteer_core = require("puppeteer-core");
26
+ puppeteer_core = __toESM(puppeteer_core);
27
+ //#region src/browser-session.ts
28
+ /**
29
+ * BrowserSession — owns the single headless Chrome instance the agent
30
+ * automates and the human takes over. Lazy-launched on first use, idle-shut
31
+ * down when nothing holds it open (cookies/login persist on disk in
32
+ * `userDataDir` across restarts). Tracks the *active* target so OAuth popups /
33
+ * new tabs are what gets screencast, not a stale main window.
34
+ */
35
+ const DEFAULT_IDLE_MS = 300 * 1e3;
36
+ const noopLogger$2 = {
37
+ info: () => {},
38
+ warn: () => {},
39
+ error: () => {},
40
+ debug: () => {}
41
+ };
42
+ var BrowserSession = class {
43
+ browser = null;
44
+ activePage = null;
45
+ launching = null;
46
+ holds = 0;
47
+ idleTimer = null;
48
+ log;
49
+ idleMs;
50
+ constructor(options) {
51
+ this.options = options;
52
+ this.log = options.logger ?? noopLogger$2;
53
+ this.idleMs = options.idleShutdownMs ?? DEFAULT_IDLE_MS;
54
+ }
55
+ /** Launch Chrome if not already running (idempotent, concurrent-safe). */
56
+ async ensureLaunched() {
57
+ if (this.browser) return;
58
+ if (this.launching) return this.launching;
59
+ this.launching = this.doLaunch().finally(() => {
60
+ this.launching = null;
61
+ });
62
+ return this.launching;
63
+ }
64
+ async doLaunch() {
65
+ const args = [
66
+ "--disable-blink-features=AutomationControlled",
67
+ ...this.options.noSandbox ? ["--no-sandbox", "--disable-setuid-sandbox"] : [],
68
+ ...this.options.extraArgs ?? []
69
+ ];
70
+ this.log.info(`Launching Chrome (${this.options.executablePath})`);
71
+ this.browser = await puppeteer_core.default.launch({
72
+ executablePath: this.options.executablePath,
73
+ headless: this.options.headless ?? true,
74
+ userDataDir: this.options.userDataDir,
75
+ args
76
+ });
77
+ this.activePage = (await this.browser.pages())[0] ?? await this.browser.newPage();
78
+ this.browser.on("targetcreated", (target) => {
79
+ target.page().then((page) => {
80
+ if (page) {
81
+ this.activePage = page;
82
+ this.log.debug("Active page switched to new target");
83
+ }
84
+ }).catch(() => {});
85
+ });
86
+ this.browser.on("disconnected", () => {
87
+ this.browser = null;
88
+ this.activePage = null;
89
+ });
90
+ }
91
+ /** The current active page, launching Chrome first if needed. */
92
+ async getActivePage() {
93
+ await this.ensureLaunched();
94
+ if (!this.activePage || this.activePage.isClosed()) {
95
+ if (!this.browser) throw new Error("Browser not available");
96
+ this.activePage = (await this.browser.pages()).find((p) => !p.isClosed()) ?? await this.browser.newPage();
97
+ }
98
+ return this.activePage;
99
+ }
100
+ /** Prevent idle shutdown while a viewer or op is active. */
101
+ addHold() {
102
+ this.holds += 1;
103
+ this.clearIdleTimer();
104
+ }
105
+ /** Release a hold; arm idle shutdown when the last one is released. */
106
+ removeHold() {
107
+ this.holds = Math.max(0, this.holds - 1);
108
+ if (this.holds === 0) this.armIdleTimer();
109
+ }
110
+ /** Reset the idle timer on any activity (only relevant when unheld). */
111
+ touch() {
112
+ if (this.holds === 0) this.armIdleTimer();
113
+ }
114
+ armIdleTimer() {
115
+ this.clearIdleTimer();
116
+ const timer = setTimeout(() => {
117
+ if (this.holds === 0) {
118
+ this.log.info("Chrome idle — shutting down (cookies persist on disk)");
119
+ this.shutdown();
120
+ }
121
+ }, this.idleMs);
122
+ timer.unref();
123
+ this.idleTimer = timer;
124
+ }
125
+ clearIdleTimer() {
126
+ if (this.idleTimer) {
127
+ clearTimeout(this.idleTimer);
128
+ this.idleTimer = null;
129
+ }
130
+ }
131
+ async shutdown() {
132
+ this.clearIdleTimer();
133
+ const browser = this.browser;
134
+ this.browser = null;
135
+ this.activePage = null;
136
+ if (browser) try {
137
+ await browser.close();
138
+ } catch {}
139
+ }
140
+ };
141
+ //#endregion
142
+ //#region src/screencast-pump.ts
143
+ const noopLogger$1 = {
144
+ info: () => {},
145
+ warn: () => {},
146
+ error: () => {},
147
+ debug: () => {}
148
+ };
149
+ var ScreencastPump = class {
150
+ cdp = null;
151
+ running = false;
152
+ frameSeq = 0;
153
+ outstanding = null;
154
+ log;
155
+ quality;
156
+ ackTimeoutMs;
157
+ constructor(options) {
158
+ this.options = options;
159
+ this.log = options.logger ?? noopLogger$1;
160
+ this.quality = options.quality ?? 70;
161
+ this.ackTimeoutMs = options.ackTimeoutMs ?? 2e3;
162
+ }
163
+ get isRunning() {
164
+ return this.running;
165
+ }
166
+ /** Start (or restart) the screencast on the given page. */
167
+ async start(page, viewport) {
168
+ await this.stop();
169
+ const cdp = await page.createCDPSession();
170
+ this.cdp = cdp;
171
+ await cdp.send("Emulation.setDeviceMetricsOverride", {
172
+ width: viewport.width,
173
+ height: viewport.height,
174
+ deviceScaleFactor: viewport.dpr,
175
+ mobile: false
176
+ });
177
+ cdp.on("Page.screencastFrame", (event) => {
178
+ this.onCdpFrame(event.data, event.sessionId, event.metadata);
179
+ });
180
+ await cdp.send("Page.startScreencast", {
181
+ format: "jpeg",
182
+ quality: this.quality,
183
+ maxWidth: Math.round(viewport.width * viewport.dpr),
184
+ maxHeight: Math.round(viewport.height * viewport.dpr),
185
+ everyNthFrame: 1
186
+ });
187
+ this.running = true;
188
+ this.log.debug("Screencast started");
189
+ }
190
+ onCdpFrame(dataBase64, cdpSessionId, metadata) {
191
+ if (this.outstanding) this.ackCdp(this.outstanding);
192
+ const frameSeq = ++this.frameSeq;
193
+ const meta = {
194
+ deviceWidth: metadata.deviceWidth ?? 0,
195
+ deviceHeight: metadata.deviceHeight ?? 0,
196
+ frameSeq,
197
+ offsetTop: metadata.offsetTop,
198
+ pageScaleFactor: metadata.pageScaleFactor,
199
+ scrollOffsetX: metadata.scrollOffsetX,
200
+ scrollOffsetY: metadata.scrollOffsetY
201
+ };
202
+ const timer = setTimeout(() => {
203
+ if (this.outstanding?.frameSeq === frameSeq) this.ackCdp(this.outstanding);
204
+ }, this.ackTimeoutMs);
205
+ timer.unref();
206
+ this.outstanding = {
207
+ cdpSessionId,
208
+ frameSeq,
209
+ timer
210
+ };
211
+ this.options.onFrame(Buffer.from(dataBase64, "base64"), meta);
212
+ }
213
+ /** Viewer acked frame `frameSeq` — release Chrome to send the next frame. */
214
+ ackFromViewer(frameSeq) {
215
+ if (this.outstanding?.frameSeq === frameSeq) this.ackCdp(this.outstanding);
216
+ }
217
+ ackCdp(outstanding) {
218
+ clearTimeout(outstanding.timer);
219
+ this.outstanding = null;
220
+ if (this.cdp && this.running) this.cdp.send("Page.screencastFrameAck", { sessionId: outstanding.cdpSessionId }).catch(() => {});
221
+ }
222
+ async stop() {
223
+ this.running = false;
224
+ if (this.outstanding) {
225
+ clearTimeout(this.outstanding.timer);
226
+ this.outstanding = null;
227
+ }
228
+ const cdp = this.cdp;
229
+ this.cdp = null;
230
+ if (cdp) {
231
+ try {
232
+ await cdp.send("Page.stopScreencast");
233
+ } catch {}
234
+ try {
235
+ await cdp.detach();
236
+ } catch {}
237
+ }
238
+ }
239
+ };
240
+ //#endregion
241
+ //#region src/input-injector.ts
242
+ const MOUSE_TYPE = {
243
+ mousemoved: "mouseMoved",
244
+ mousepressed: "mousePressed",
245
+ mousereleased: "mouseReleased"
246
+ };
247
+ var InputInjector = class {
248
+ cdp = null;
249
+ viewport;
250
+ constructor(viewport) {
251
+ this.viewport = viewport;
252
+ }
253
+ async attach(page) {
254
+ await this.detach();
255
+ this.cdp = await page.createCDPSession();
256
+ }
257
+ async detach() {
258
+ const cdp = this.cdp;
259
+ this.cdp = null;
260
+ if (cdp) try {
261
+ await cdp.detach();
262
+ } catch {}
263
+ }
264
+ updateViewport(viewport) {
265
+ this.viewport = viewport;
266
+ }
267
+ toCssPx(nx, ny) {
268
+ return {
269
+ x: Math.round(clamp01(nx) * this.viewport.width),
270
+ y: Math.round(clamp01(ny) * this.viewport.height)
271
+ };
272
+ }
273
+ mouse(p) {
274
+ if (!this.cdp) return;
275
+ const { x, y } = this.toCssPx(p.nx, p.ny);
276
+ this.cdp.send("Input.dispatchMouseEvent", {
277
+ type: MOUSE_TYPE[p.type],
278
+ x,
279
+ y,
280
+ button: p.button ?? "none",
281
+ buttons: p.buttons ?? 0,
282
+ clickCount: p.clickCount ?? (p.type === "mousepressed" ? 1 : 0),
283
+ modifiers: p.modifiers ?? 0
284
+ }).catch(() => {});
285
+ }
286
+ wheel(p) {
287
+ if (!this.cdp) return;
288
+ const { x, y } = this.toCssPx(p.nx, p.ny);
289
+ this.cdp.send("Input.dispatchMouseEvent", {
290
+ type: "mouseWheel",
291
+ x,
292
+ y,
293
+ deltaX: p.deltaX,
294
+ deltaY: p.deltaY,
295
+ modifiers: p.modifiers ?? 0
296
+ }).catch(() => {});
297
+ }
298
+ key(p) {
299
+ if (!this.cdp) return;
300
+ if (p.type === "char") {
301
+ this.cdp.send("Input.dispatchKeyEvent", {
302
+ type: "char",
303
+ text: p.text ?? "",
304
+ modifiers: p.modifiers ?? 0
305
+ }).catch(() => {});
306
+ return;
307
+ }
308
+ this.cdp.send("Input.dispatchKeyEvent", {
309
+ type: p.type === "keydown" ? "keyDown" : "keyUp",
310
+ key: p.key,
311
+ code: p.code,
312
+ text: p.type === "keydown" ? p.text : void 0,
313
+ modifiers: p.modifiers ?? 0
314
+ }).catch(() => {});
315
+ }
316
+ };
317
+ function clamp01(n) {
318
+ if (Number.isNaN(n)) return 0;
319
+ return Math.max(0, Math.min(1, n));
320
+ }
321
+ //#endregion
322
+ //#region src/automation.ts
323
+ var BrowserAutomation = class {
324
+ constructor(session, turn, isNavigationAllowed) {
325
+ this.session = session;
326
+ this.turn = turn;
327
+ this.isNavigationAllowed = isNavigationAllowed;
328
+ }
329
+ async navigate(url) {
330
+ await this.turn.acquireAgent();
331
+ if (!this.isNavigationAllowed(url)) throw new Error(`Navigation to ${url} blocked by SSRF policy`);
332
+ const page = await this.session.getActivePage();
333
+ await page.goto(url, { waitUntil: "domcontentloaded" });
334
+ return {
335
+ url: page.url(),
336
+ title: await page.title()
337
+ };
338
+ }
339
+ async click(selector) {
340
+ await this.turn.acquireAgent();
341
+ await (await this.session.getActivePage()).click(selector);
342
+ }
343
+ async type(selector, text) {
344
+ await this.turn.acquireAgent();
345
+ await (await this.session.getActivePage()).type(selector, text);
346
+ }
347
+ async waitFor(opts) {
348
+ await this.turn.acquireAgent();
349
+ const page = await this.session.getActivePage();
350
+ if (opts.selector) await page.waitForSelector(opts.selector);
351
+ else if (opts.urlPattern) {
352
+ const pattern = opts.urlPattern;
353
+ await page.waitForFunction((p) => window.location.href.includes(p), {}, pattern);
354
+ } else if (typeof opts.ms === "number") await new Promise((resolve) => setTimeout(resolve, opts.ms));
355
+ }
356
+ /** One-shot JPEG screenshot (base64) for the agent's own reasoning — distinct
357
+ * from the continuous screencast stream to viewers. */
358
+ async screenshot() {
359
+ await this.turn.acquireAgent();
360
+ return (await this.session.getActivePage()).screenshot({
361
+ type: "jpeg",
362
+ quality: 70,
363
+ encoding: "base64"
364
+ });
365
+ }
366
+ /** Evaluate an expression in the page context via CDP (no eval on our side). */
367
+ async evaluate(expression) {
368
+ await this.turn.acquireAgent();
369
+ const cdp = await (await this.session.getActivePage()).createCDPSession();
370
+ try {
371
+ return (await cdp.send("Runtime.evaluate", {
372
+ expression,
373
+ returnByValue: true,
374
+ awaitPromise: true
375
+ })).result.value;
376
+ } finally {
377
+ try {
378
+ await cdp.detach();
379
+ } catch {}
380
+ }
381
+ }
382
+ };
383
+ //#endregion
384
+ //#region src/browser-surface.ts
385
+ /**
386
+ * BrowserSurface — the `SurfaceHandler` for browser co-browsing. Ties together
387
+ * the shared Chrome (BrowserSession), the screencast pump, the input injector,
388
+ * the agent/human turn-mutex, and the agent automation tools. Viewers watch the
389
+ * live page; the turn token gates who can write.
390
+ *
391
+ * Multiple viewers can attach to one browser; screencast frames broadcast to
392
+ * all, and the pump's ack-gating advances on the first viewer ack (a slow
393
+ * second viewer may drop frames — acceptable for v1).
394
+ */
395
+ const noopLogger = {
396
+ info: () => {},
397
+ warn: () => {},
398
+ error: () => {},
399
+ debug: () => {}
400
+ };
401
+ var BrowserSurface = class {
402
+ surface = "browser";
403
+ session;
404
+ turn;
405
+ pump;
406
+ injector;
407
+ automation;
408
+ viewers = /* @__PURE__ */ new Set();
409
+ viewport = {
410
+ width: 1280,
411
+ height: 720,
412
+ dpr: 1
413
+ };
414
+ handoff = null;
415
+ log;
416
+ constructor(options, sendFrame) {
417
+ this.options = options;
418
+ this.sendFrame = sendFrame;
419
+ this.log = options.logger ?? noopLogger;
420
+ this.session = new BrowserSession(options);
421
+ this.turn = new _alfe_ai_remote.TurnController({ onOwnerChange: () => {
422
+ this.broadcastState();
423
+ } });
424
+ this.pump = new ScreencastPump({
425
+ onFrame: (jpeg, meta) => {
426
+ this.broadcastFrame(jpeg, meta);
427
+ },
428
+ logger: this.log
429
+ });
430
+ this.injector = new InputInjector(this.viewport);
431
+ this.automation = new BrowserAutomation(this.session, this.turn, options.isNavigationAllowed ?? (() => true));
432
+ }
433
+ async openSession(sessionId, open) {
434
+ this.viewers.add(sessionId);
435
+ this.session.addHold();
436
+ if (open.width && open.height) this.viewport = {
437
+ width: open.width,
438
+ height: open.height,
439
+ dpr: open.dpr ?? 1
440
+ };
441
+ await this.ensureStreaming();
442
+ this.sendState(sessionId);
443
+ }
444
+ handleFrame(frame) {
445
+ switch (frame.type) {
446
+ case _alfe_ai_remote.RemoteFrameType.SCREENCAST_ACK: {
447
+ const ack = (0, _alfe_ai_remote.decodeJson)(frame.payload);
448
+ if (ack) this.pump.ackFromViewer(ack.frameSeq);
449
+ this.session.touch();
450
+ break;
451
+ }
452
+ case _alfe_ai_remote.RemoteFrameType.RESIZE: {
453
+ const r = (0, _alfe_ai_remote.decodeJson)(frame.payload);
454
+ if (r) this.applyResize(r.width, r.height, r.dpr ?? 1);
455
+ break;
456
+ }
457
+ case _alfe_ai_remote.RemoteFrameType.INPUT_MOUSE:
458
+ if (this.turn.humanInControl) {
459
+ const p = (0, _alfe_ai_remote.decodeJson)(frame.payload);
460
+ if (p) this.injector.mouse(p);
461
+ }
462
+ break;
463
+ case _alfe_ai_remote.RemoteFrameType.INPUT_WHEEL:
464
+ if (this.turn.humanInControl) {
465
+ const p = (0, _alfe_ai_remote.decodeJson)(frame.payload);
466
+ if (p) this.injector.wheel(p);
467
+ }
468
+ break;
469
+ case _alfe_ai_remote.RemoteFrameType.INPUT_KEY:
470
+ if (this.turn.humanInControl) {
471
+ const p = (0, _alfe_ai_remote.decodeJson)(frame.payload);
472
+ if (p) this.injector.key(p);
473
+ }
474
+ break;
475
+ case _alfe_ai_remote.RemoteFrameType.TAKEOVER_REQUEST:
476
+ if (this.turn.grantHuman()) this.broadcast(_alfe_ai_remote.RemoteFrameType.TAKEOVER_GRANTED);
477
+ else this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.TAKEOVER_DENIED, frame.sessionId));
478
+ break;
479
+ case _alfe_ai_remote.RemoteFrameType.RELEASE_CONTROL:
480
+ this.releaseToAgent();
481
+ break;
482
+ default: break;
483
+ }
484
+ }
485
+ closeSession(sessionId) {
486
+ if (!this.viewers.delete(sessionId)) return;
487
+ this.session.removeHold();
488
+ if (this.viewers.size === 0) {
489
+ this.pump.stop();
490
+ if (this.turn.humanInControl) this.releaseToAgent();
491
+ }
492
+ }
493
+ /**
494
+ * Agent tool entry point: hand control to the human and block until they
495
+ * release it (RELEASE_CONTROL) or `timeoutMs` elapses. Resolves with the
496
+ * final page URL/title so the agent resumes on the same live page.
497
+ */
498
+ async requestHandoff(timeoutMs) {
499
+ this.turn.grantHuman();
500
+ this.broadcast(_alfe_ai_remote.RemoteFrameType.TAKEOVER_GRANTED);
501
+ return new Promise((resolve) => {
502
+ const timer = setTimeout(() => {
503
+ this.handoff = null;
504
+ this.turn.releaseHuman();
505
+ this.broadcast(_alfe_ai_remote.RemoteFrameType.CONTROL_REVOKED);
506
+ this.currentPageInfo().then(({ url, title }) => {
507
+ resolve({
508
+ released: false,
509
+ timedOut: true,
510
+ url,
511
+ title
512
+ });
513
+ });
514
+ }, timeoutMs);
515
+ timer.unref();
516
+ this.handoff = {
517
+ resolve,
518
+ timer
519
+ };
520
+ });
521
+ }
522
+ async shutdown() {
523
+ await this.pump.stop();
524
+ await this.injector.detach();
525
+ await this.session.shutdown();
526
+ }
527
+ releaseToAgent() {
528
+ this.turn.releaseHuman();
529
+ const waiter = this.handoff;
530
+ this.handoff = null;
531
+ if (waiter) {
532
+ clearTimeout(waiter.timer);
533
+ this.currentPageInfo().then(({ url, title }) => {
534
+ waiter.resolve({
535
+ released: true,
536
+ timedOut: false,
537
+ url,
538
+ title
539
+ });
540
+ });
541
+ }
542
+ }
543
+ async ensureStreaming() {
544
+ if (this.pump.isRunning || this.viewers.size === 0) return;
545
+ const page = await this.session.getActivePage();
546
+ await this.injector.attach(page);
547
+ this.injector.updateViewport(this.viewport);
548
+ await this.pump.start(page, this.viewport);
549
+ }
550
+ async applyResize(width, height, dpr) {
551
+ this.viewport = {
552
+ width,
553
+ height,
554
+ dpr
555
+ };
556
+ this.injector.updateViewport(this.viewport);
557
+ if (this.pump.isRunning) {
558
+ const page = await this.session.getActivePage();
559
+ await this.pump.start(page, this.viewport);
560
+ }
561
+ }
562
+ broadcastFrame(jpeg, meta) {
563
+ for (const sessionId of this.viewers) this.sendFrame((0, _alfe_ai_remote.encodeScreencastFrame)(sessionId, meta, jpeg));
564
+ }
565
+ broadcast(type) {
566
+ for (const sessionId of this.viewers) this.sendFrame((0, _alfe_ai_remote.encodeFrame)(type, sessionId));
567
+ }
568
+ broadcastState() {
569
+ for (const sessionId of this.viewers) this.sendState(sessionId);
570
+ }
571
+ sendState(sessionId) {
572
+ this.currentPageInfo().then(({ url, title }) => {
573
+ const state = {
574
+ surface: "browser",
575
+ url,
576
+ title,
577
+ controller: this.turn.currentOwner
578
+ };
579
+ this.sendFrame((0, _alfe_ai_remote.encodeJsonFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_STATE, sessionId, state));
580
+ });
581
+ }
582
+ async currentPageInfo() {
583
+ try {
584
+ const page = await this.session.getActivePage();
585
+ return {
586
+ url: page.url(),
587
+ title: await page.title()
588
+ };
589
+ } catch {
590
+ return {
591
+ url: "",
592
+ title: ""
593
+ };
594
+ }
595
+ }
596
+ };
597
+ //#endregion
598
+ exports.BrowserAutomation = BrowserAutomation;
599
+ exports.BrowserSession = BrowserSession;
600
+ exports.BrowserSurface = BrowserSurface;