@alfe.ai/browser 0.1.0 → 0.2.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 +56 -11
- package/dist/index.d.cts +27 -3
- package/dist/index.d.ts +27 -3
- package/dist/index.js +57 -12
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,14 @@ puppeteer_core = __toESM(puppeteer_core);
|
|
|
33
33
|
* new tabs are what gets screencast, not a stale main window.
|
|
34
34
|
*/
|
|
35
35
|
const DEFAULT_IDLE_MS = 300 * 1e3;
|
|
36
|
+
/**
|
|
37
|
+
* A page target is worth streaming only once it has a real, navigable http(s)
|
|
38
|
+
* URL. Transient `about:blank` (and `chrome://`, `data:`, blank string) targets
|
|
39
|
+
* are throwaways we must not flip the screencast to.
|
|
40
|
+
*/
|
|
41
|
+
function isNavigablePageUrl(url) {
|
|
42
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
43
|
+
}
|
|
36
44
|
const noopLogger$2 = {
|
|
37
45
|
info: () => {},
|
|
38
46
|
warn: () => {},
|
|
@@ -76,11 +84,13 @@ var BrowserSession = class {
|
|
|
76
84
|
});
|
|
77
85
|
this.activePage = (await this.browser.pages())[0] ?? await this.browser.newPage();
|
|
78
86
|
this.browser.on("targetcreated", (target) => {
|
|
87
|
+
if (target.type() !== puppeteer_core.TargetType.PAGE) return;
|
|
79
88
|
target.page().then((page) => {
|
|
80
|
-
if (page)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
if (!page) return;
|
|
90
|
+
if (this.adoptIfNavigable(page)) return;
|
|
91
|
+
page.on("framenavigated", (frame) => {
|
|
92
|
+
if (frame === page.mainFrame()) this.adoptIfNavigable(page);
|
|
93
|
+
});
|
|
84
94
|
}).catch(() => {});
|
|
85
95
|
});
|
|
86
96
|
this.browser.on("disconnected", () => {
|
|
@@ -88,6 +98,18 @@ var BrowserSession = class {
|
|
|
88
98
|
this.activePage = null;
|
|
89
99
|
});
|
|
90
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Adopt `page` as the streamed active page iff it has a real navigable
|
|
103
|
+
* http(s) URL. Returns whether it was adopted. Guards against flipping the
|
|
104
|
+
* screencast to a transient `about:blank` throwaway target.
|
|
105
|
+
*/
|
|
106
|
+
adoptIfNavigable(page) {
|
|
107
|
+
if (page.isClosed() || !isNavigablePageUrl(page.url())) return false;
|
|
108
|
+
if (this.activePage === page) return true;
|
|
109
|
+
this.activePage = page;
|
|
110
|
+
this.log.debug("Active page switched to new target");
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
91
113
|
/** The current active page, launching Chrome first if needed. */
|
|
92
114
|
async getActivePage() {
|
|
93
115
|
await this.ensureLaunched();
|
|
@@ -491,18 +513,29 @@ var BrowserSurface = class {
|
|
|
491
513
|
}
|
|
492
514
|
}
|
|
493
515
|
/**
|
|
494
|
-
* Agent tool entry point:
|
|
495
|
-
*
|
|
496
|
-
* final page URL/title so the agent
|
|
516
|
+
* Agent tool entry point: PARK the agent and wait for a human to take over
|
|
517
|
+
* and hand back (RELEASE_CONTROL / viewer teardown after a claim) or for
|
|
518
|
+
* `timeoutMs` to elapse. Resolves with the final page URL/title so the agent
|
|
519
|
+
* resumes on the same live page.
|
|
520
|
+
*
|
|
521
|
+
* Crucially this does NOT grant the human turn up front. The turn is granted
|
|
522
|
+
* only when a human ACTUALLY takes control — i.e. when the controlling
|
|
523
|
+
* (`canControl:true`) viewer sends a `TAKEOVER_REQUEST` frame (see
|
|
524
|
+
* `handleFrame` → `grantHuman()`). Granting at tool-call time made the session
|
|
525
|
+
* "human in control" before anyone had claimed, so a read-only viewer's
|
|
526
|
+
* teardown would fire `releaseToAgent()` and complete the session out from
|
|
527
|
+
* under the user (mislabeled 409 on the real claim; agent only resuming when
|
|
528
|
+
* the tab closed).
|
|
497
529
|
*/
|
|
498
530
|
async requestHandoff(timeoutMs) {
|
|
499
|
-
this.
|
|
500
|
-
this.broadcast(_alfe_ai_remote.RemoteFrameType.TAKEOVER_GRANTED);
|
|
531
|
+
if (this.handoff) this.releaseToAgent();
|
|
501
532
|
return new Promise((resolve) => {
|
|
502
533
|
const timer = setTimeout(() => {
|
|
503
534
|
this.handoff = null;
|
|
504
|
-
this.turn.
|
|
505
|
-
|
|
535
|
+
if (this.turn.humanInControl) {
|
|
536
|
+
this.turn.releaseHuman();
|
|
537
|
+
this.broadcast(_alfe_ai_remote.RemoteFrameType.CONTROL_REVOKED);
|
|
538
|
+
}
|
|
506
539
|
this.currentPageInfo().then(({ url, title }) => {
|
|
507
540
|
resolve({
|
|
508
541
|
released: false,
|
|
@@ -519,6 +552,18 @@ var BrowserSurface = class {
|
|
|
519
552
|
};
|
|
520
553
|
});
|
|
521
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Keep the shared Chrome alive across an awaiting-human window. Delegates to
|
|
557
|
+
* the session's hold counter (which also backs per-viewer holds). Idempotent
|
|
558
|
+
* and leak-safe when paired with {@link removeHold} in a `finally`.
|
|
559
|
+
*/
|
|
560
|
+
addHold() {
|
|
561
|
+
this.session.addHold();
|
|
562
|
+
}
|
|
563
|
+
/** Release a hold taken by {@link addHold}. */
|
|
564
|
+
removeHold() {
|
|
565
|
+
this.session.removeHold();
|
|
566
|
+
}
|
|
522
567
|
async shutdown() {
|
|
523
568
|
await this.pump.stop();
|
|
524
569
|
await this.injector.detach();
|
package/dist/index.d.cts
CHANGED
|
@@ -150,6 +150,12 @@ declare class BrowserSession {
|
|
|
150
150
|
/** Launch Chrome if not already running (idempotent, concurrent-safe). */
|
|
151
151
|
ensureLaunched(): Promise<void>;
|
|
152
152
|
private doLaunch;
|
|
153
|
+
/**
|
|
154
|
+
* Adopt `page` as the streamed active page iff it has a real navigable
|
|
155
|
+
* http(s) URL. Returns whether it was adopted. Guards against flipping the
|
|
156
|
+
* screencast to a transient `about:blank` throwaway target.
|
|
157
|
+
*/
|
|
158
|
+
private adoptIfNavigable;
|
|
153
159
|
/** The current active page, launching Chrome first if needed. */
|
|
154
160
|
getActivePage(): Promise<Page>;
|
|
155
161
|
/** Prevent idle shutdown while a viewer or op is active. */
|
|
@@ -213,11 +219,29 @@ declare class BrowserSurface implements SurfaceHandler {
|
|
|
213
219
|
handleFrame(frame: RemoteFrame): void;
|
|
214
220
|
closeSession(sessionId: number): void;
|
|
215
221
|
/**
|
|
216
|
-
* Agent tool entry point:
|
|
217
|
-
*
|
|
218
|
-
* final page URL/title so the agent
|
|
222
|
+
* Agent tool entry point: PARK the agent and wait for a human to take over
|
|
223
|
+
* and hand back (RELEASE_CONTROL / viewer teardown after a claim) or for
|
|
224
|
+
* `timeoutMs` to elapse. Resolves with the final page URL/title so the agent
|
|
225
|
+
* resumes on the same live page.
|
|
226
|
+
*
|
|
227
|
+
* Crucially this does NOT grant the human turn up front. The turn is granted
|
|
228
|
+
* only when a human ACTUALLY takes control — i.e. when the controlling
|
|
229
|
+
* (`canControl:true`) viewer sends a `TAKEOVER_REQUEST` frame (see
|
|
230
|
+
* `handleFrame` → `grantHuman()`). Granting at tool-call time made the session
|
|
231
|
+
* "human in control" before anyone had claimed, so a read-only viewer's
|
|
232
|
+
* teardown would fire `releaseToAgent()` and complete the session out from
|
|
233
|
+
* under the user (mislabeled 409 on the real claim; agent only resuming when
|
|
234
|
+
* the tab closed).
|
|
219
235
|
*/
|
|
220
236
|
requestHandoff(timeoutMs: number): Promise<HandoffResult>;
|
|
237
|
+
/**
|
|
238
|
+
* Keep the shared Chrome alive across an awaiting-human window. Delegates to
|
|
239
|
+
* the session's hold counter (which also backs per-viewer holds). Idempotent
|
|
240
|
+
* and leak-safe when paired with {@link removeHold} in a `finally`.
|
|
241
|
+
*/
|
|
242
|
+
addHold(): void;
|
|
243
|
+
/** Release a hold taken by {@link addHold}. */
|
|
244
|
+
removeHold(): void;
|
|
221
245
|
shutdown(): Promise<void>;
|
|
222
246
|
private releaseToAgent;
|
|
223
247
|
private ensureStreaming;
|
package/dist/index.d.ts
CHANGED
|
@@ -150,6 +150,12 @@ declare class BrowserSession {
|
|
|
150
150
|
/** Launch Chrome if not already running (idempotent, concurrent-safe). */
|
|
151
151
|
ensureLaunched(): Promise<void>;
|
|
152
152
|
private doLaunch;
|
|
153
|
+
/**
|
|
154
|
+
* Adopt `page` as the streamed active page iff it has a real navigable
|
|
155
|
+
* http(s) URL. Returns whether it was adopted. Guards against flipping the
|
|
156
|
+
* screencast to a transient `about:blank` throwaway target.
|
|
157
|
+
*/
|
|
158
|
+
private adoptIfNavigable;
|
|
153
159
|
/** The current active page, launching Chrome first if needed. */
|
|
154
160
|
getActivePage(): Promise<Page>;
|
|
155
161
|
/** Prevent idle shutdown while a viewer or op is active. */
|
|
@@ -213,11 +219,29 @@ declare class BrowserSurface implements SurfaceHandler {
|
|
|
213
219
|
handleFrame(frame: RemoteFrame): void;
|
|
214
220
|
closeSession(sessionId: number): void;
|
|
215
221
|
/**
|
|
216
|
-
* Agent tool entry point:
|
|
217
|
-
*
|
|
218
|
-
* final page URL/title so the agent
|
|
222
|
+
* Agent tool entry point: PARK the agent and wait for a human to take over
|
|
223
|
+
* and hand back (RELEASE_CONTROL / viewer teardown after a claim) or for
|
|
224
|
+
* `timeoutMs` to elapse. Resolves with the final page URL/title so the agent
|
|
225
|
+
* resumes on the same live page.
|
|
226
|
+
*
|
|
227
|
+
* Crucially this does NOT grant the human turn up front. The turn is granted
|
|
228
|
+
* only when a human ACTUALLY takes control — i.e. when the controlling
|
|
229
|
+
* (`canControl:true`) viewer sends a `TAKEOVER_REQUEST` frame (see
|
|
230
|
+
* `handleFrame` → `grantHuman()`). Granting at tool-call time made the session
|
|
231
|
+
* "human in control" before anyone had claimed, so a read-only viewer's
|
|
232
|
+
* teardown would fire `releaseToAgent()` and complete the session out from
|
|
233
|
+
* under the user (mislabeled 409 on the real claim; agent only resuming when
|
|
234
|
+
* the tab closed).
|
|
219
235
|
*/
|
|
220
236
|
requestHandoff(timeoutMs: number): Promise<HandoffResult>;
|
|
237
|
+
/**
|
|
238
|
+
* Keep the shared Chrome alive across an awaiting-human window. Delegates to
|
|
239
|
+
* the session's hold counter (which also backs per-viewer holds). Idempotent
|
|
240
|
+
* and leak-safe when paired with {@link removeHold} in a `finally`.
|
|
241
|
+
*/
|
|
242
|
+
addHold(): void;
|
|
243
|
+
/** Release a hold taken by {@link addHold}. */
|
|
244
|
+
removeHold(): void;
|
|
221
245
|
shutdown(): Promise<void>;
|
|
222
246
|
private releaseToAgent;
|
|
223
247
|
private ensureStreaming;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RemoteFrameType, TurnController, decodeJson, encodeFrame, encodeJsonFrame, encodeScreencastFrame } from "@alfe.ai/remote";
|
|
2
|
-
import puppeteer from "puppeteer-core";
|
|
2
|
+
import puppeteer, { TargetType } from "puppeteer-core";
|
|
3
3
|
//#region src/browser-session.ts
|
|
4
4
|
/**
|
|
5
5
|
* BrowserSession — owns the single headless Chrome instance the agent
|
|
@@ -9,6 +9,14 @@ import puppeteer from "puppeteer-core";
|
|
|
9
9
|
* new tabs are what gets screencast, not a stale main window.
|
|
10
10
|
*/
|
|
11
11
|
const DEFAULT_IDLE_MS = 300 * 1e3;
|
|
12
|
+
/**
|
|
13
|
+
* A page target is worth streaming only once it has a real, navigable http(s)
|
|
14
|
+
* URL. Transient `about:blank` (and `chrome://`, `data:`, blank string) targets
|
|
15
|
+
* are throwaways we must not flip the screencast to.
|
|
16
|
+
*/
|
|
17
|
+
function isNavigablePageUrl(url) {
|
|
18
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
19
|
+
}
|
|
12
20
|
const noopLogger$2 = {
|
|
13
21
|
info: () => {},
|
|
14
22
|
warn: () => {},
|
|
@@ -52,11 +60,13 @@ var BrowserSession = class {
|
|
|
52
60
|
});
|
|
53
61
|
this.activePage = (await this.browser.pages())[0] ?? await this.browser.newPage();
|
|
54
62
|
this.browser.on("targetcreated", (target) => {
|
|
63
|
+
if (target.type() !== TargetType.PAGE) return;
|
|
55
64
|
target.page().then((page) => {
|
|
56
|
-
if (page)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
if (!page) return;
|
|
66
|
+
if (this.adoptIfNavigable(page)) return;
|
|
67
|
+
page.on("framenavigated", (frame) => {
|
|
68
|
+
if (frame === page.mainFrame()) this.adoptIfNavigable(page);
|
|
69
|
+
});
|
|
60
70
|
}).catch(() => {});
|
|
61
71
|
});
|
|
62
72
|
this.browser.on("disconnected", () => {
|
|
@@ -64,6 +74,18 @@ var BrowserSession = class {
|
|
|
64
74
|
this.activePage = null;
|
|
65
75
|
});
|
|
66
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Adopt `page` as the streamed active page iff it has a real navigable
|
|
79
|
+
* http(s) URL. Returns whether it was adopted. Guards against flipping the
|
|
80
|
+
* screencast to a transient `about:blank` throwaway target.
|
|
81
|
+
*/
|
|
82
|
+
adoptIfNavigable(page) {
|
|
83
|
+
if (page.isClosed() || !isNavigablePageUrl(page.url())) return false;
|
|
84
|
+
if (this.activePage === page) return true;
|
|
85
|
+
this.activePage = page;
|
|
86
|
+
this.log.debug("Active page switched to new target");
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
67
89
|
/** The current active page, launching Chrome first if needed. */
|
|
68
90
|
async getActivePage() {
|
|
69
91
|
await this.ensureLaunched();
|
|
@@ -467,18 +489,29 @@ var BrowserSurface = class {
|
|
|
467
489
|
}
|
|
468
490
|
}
|
|
469
491
|
/**
|
|
470
|
-
* Agent tool entry point:
|
|
471
|
-
*
|
|
472
|
-
* final page URL/title so the agent
|
|
492
|
+
* Agent tool entry point: PARK the agent and wait for a human to take over
|
|
493
|
+
* and hand back (RELEASE_CONTROL / viewer teardown after a claim) or for
|
|
494
|
+
* `timeoutMs` to elapse. Resolves with the final page URL/title so the agent
|
|
495
|
+
* resumes on the same live page.
|
|
496
|
+
*
|
|
497
|
+
* Crucially this does NOT grant the human turn up front. The turn is granted
|
|
498
|
+
* only when a human ACTUALLY takes control — i.e. when the controlling
|
|
499
|
+
* (`canControl:true`) viewer sends a `TAKEOVER_REQUEST` frame (see
|
|
500
|
+
* `handleFrame` → `grantHuman()`). Granting at tool-call time made the session
|
|
501
|
+
* "human in control" before anyone had claimed, so a read-only viewer's
|
|
502
|
+
* teardown would fire `releaseToAgent()` and complete the session out from
|
|
503
|
+
* under the user (mislabeled 409 on the real claim; agent only resuming when
|
|
504
|
+
* the tab closed).
|
|
473
505
|
*/
|
|
474
506
|
async requestHandoff(timeoutMs) {
|
|
475
|
-
this.
|
|
476
|
-
this.broadcast(RemoteFrameType.TAKEOVER_GRANTED);
|
|
507
|
+
if (this.handoff) this.releaseToAgent();
|
|
477
508
|
return new Promise((resolve) => {
|
|
478
509
|
const timer = setTimeout(() => {
|
|
479
510
|
this.handoff = null;
|
|
480
|
-
this.turn.
|
|
481
|
-
|
|
511
|
+
if (this.turn.humanInControl) {
|
|
512
|
+
this.turn.releaseHuman();
|
|
513
|
+
this.broadcast(RemoteFrameType.CONTROL_REVOKED);
|
|
514
|
+
}
|
|
482
515
|
this.currentPageInfo().then(({ url, title }) => {
|
|
483
516
|
resolve({
|
|
484
517
|
released: false,
|
|
@@ -495,6 +528,18 @@ var BrowserSurface = class {
|
|
|
495
528
|
};
|
|
496
529
|
});
|
|
497
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Keep the shared Chrome alive across an awaiting-human window. Delegates to
|
|
533
|
+
* the session's hold counter (which also backs per-viewer holds). Idempotent
|
|
534
|
+
* and leak-safe when paired with {@link removeHold} in a `finally`.
|
|
535
|
+
*/
|
|
536
|
+
addHold() {
|
|
537
|
+
this.session.addHold();
|
|
538
|
+
}
|
|
539
|
+
/** Release a hold taken by {@link addHold}. */
|
|
540
|
+
removeHold() {
|
|
541
|
+
this.session.removeHold();
|
|
542
|
+
}
|
|
498
543
|
async shutdown() {
|
|
499
544
|
await this.pump.stop();
|
|
500
545
|
await this.injector.detach();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CDP-driven browser surface for the Alfe interactive remote-control relay — one shared headless Chrome for agent automation + human co-browse takeover",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|