@effect-uai/browser 0.9.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.mts","names":[],"sources":["../../src/internal/session.ts"],"mappings":";;;;;;cA6Ca,eAAA,GAAmB,CAAA,EAAG,QAAA;AAAA,KAsEvB,kBAAA;EAAA,SACD,GAAA,EAAK,GAAA;EAAA,SACL,EAAA,EAAI,WAAA,CAAY,gBAAA,EAMhB;EAAA,SAJA,SAAA;EAAA,SACA,QAAA;EAAA,SACA,OAAA,EAAS,WAAA,CAAY,oBAAA,EAEC;EAAA,SAAtB,OAAA,EAAS,MAAA,CAAO,MAAA;AAAA;AAAA,cAGd,WAAA,GACX,OAAA,EAAS,kBAAA,KACR,MAAA,CAAO,MAAA,CAAO,WAAA,CAAY,cAAA,SAAuB,KAAA,CAAM,KAAA"}
@@ -0,0 +1,322 @@
1
+ import { InjectedBox, InjectedElementInfo, InjectedPoint, InjectedSize, agentExpression } from "./injected.mjs";
2
+ import * as BrowserError from "@effect-uai/core/BrowserError";
3
+ import { Clock, Duration, Effect, Encoding, Option, PubSub, Ref, Schema } from "effect";
4
+ //#region src/internal/session.ts
5
+ const NAVIGATE_BUDGET = Duration.seconds(30);
6
+ const WAITFOR_BUDGET = Duration.seconds(10);
7
+ const ACTION_BUDGET = Duration.seconds(30);
8
+ const WAITFOR_BACKSTOP = Duration.millis(Duration.toMillis(WAITFOR_BUDGET) + 5e3);
9
+ /** `query` returns at most this many matches to bound payload size. */
10
+ const QUERY_LIMIT = 100;
11
+ /** Reply errors that mean "this endpoint does not implement the method". */
12
+ const isMethodMissing = (e) => e.kind === "reply" && /wasn't found|not found|unknown method|not implemented|unsupported/i.test(e.reason ?? "");
13
+ const KEY_DETAILS = {
14
+ Enter: {
15
+ code: "Enter",
16
+ keyCode: 13,
17
+ text: "\r"
18
+ },
19
+ Tab: {
20
+ code: "Tab",
21
+ keyCode: 9
22
+ },
23
+ Escape: {
24
+ code: "Escape",
25
+ keyCode: 27
26
+ },
27
+ Backspace: {
28
+ code: "Backspace",
29
+ keyCode: 8
30
+ },
31
+ Delete: {
32
+ code: "Delete",
33
+ keyCode: 46
34
+ },
35
+ ArrowUp: {
36
+ code: "ArrowUp",
37
+ keyCode: 38
38
+ },
39
+ ArrowDown: {
40
+ code: "ArrowDown",
41
+ keyCode: 40
42
+ },
43
+ ArrowLeft: {
44
+ code: "ArrowLeft",
45
+ keyCode: 37
46
+ },
47
+ ArrowRight: {
48
+ code: "ArrowRight",
49
+ keyCode: 39
50
+ },
51
+ Home: {
52
+ code: "Home",
53
+ keyCode: 36
54
+ },
55
+ End: {
56
+ code: "End",
57
+ keyCode: 35
58
+ },
59
+ PageUp: {
60
+ code: "PageUp",
61
+ keyCode: 33
62
+ },
63
+ PageDown: {
64
+ code: "PageDown",
65
+ keyCode: 34
66
+ }
67
+ };
68
+ const decodeLifecycleParams = Schema.decodeUnknownOption(Schema.Struct({ name: Schema.String }));
69
+ const decodeAxString = Schema.decodeUnknownOption(Schema.String);
70
+ const decodeAxValue = Schema.decodeUnknownOption(Schema.Union([Schema.String, Schema.Number]));
71
+ const decodeElements = Schema.decodeUnknownEffect(Schema.Array(InjectedElementInfo));
72
+ const buildAxTree = (nodes) => {
73
+ const byId = new Map(nodes.map((n) => [n.nodeId, n]));
74
+ const childrenOf = (node) => (node.childIds ?? []).flatMap((childId) => {
75
+ const child = byId.get(childId);
76
+ if (child === void 0) return [];
77
+ return child.ignored ? childrenOf(child) : [build(child)];
78
+ });
79
+ const build = (node) => {
80
+ const name = Option.filter(decodeAxString(node.name?.value), (s) => s !== "");
81
+ const value = Option.filter(Option.map(decodeAxValue(node.value?.value), String), (s) => s !== "");
82
+ return {
83
+ role: Option.getOrElse(decodeAxString(node.role?.value), () => "unknown"),
84
+ ...Option.isSome(name) ? { name: name.value } : {},
85
+ ...Option.isSome(value) ? { value: value.value } : {},
86
+ children: childrenOf(node)
87
+ };
88
+ };
89
+ const root = nodes.find((n) => n.parentId === void 0) ?? nodes[0];
90
+ if (root === void 0) return {
91
+ role: "document",
92
+ children: []
93
+ };
94
+ return root.ignored ? {
95
+ role: "document",
96
+ children: childrenOf(root)
97
+ } : build(root);
98
+ };
99
+ const makeSession = (options) => Effect.gen(function* () {
100
+ const { cdp, dispose, id, provider, request, sessionId } = options;
101
+ const expired = yield* Ref.make(false);
102
+ const lastActivity = yield* Ref.make(yield* Clock.currentTimeMillis);
103
+ const expire = Ref.set(expired, true).pipe(Effect.andThen(Effect.ignore(dispose)));
104
+ if (request.timeout !== void 0) yield* Effect.sleep(request.timeout).pipe(Effect.andThen(expire), Effect.forkScoped);
105
+ if (request.idleTimeout !== void 0) {
106
+ const idleMs = Duration.toMillis(Duration.fromInputUnsafe(request.idleTimeout));
107
+ yield* Effect.forkScoped(Effect.gen(function* () {
108
+ while (true) {
109
+ const elapsed = (yield* Clock.currentTimeMillis) - (yield* Ref.get(lastActivity));
110
+ if (elapsed >= idleMs) return yield* expire;
111
+ yield* Effect.sleep(Duration.millis(idleMs - elapsed));
112
+ }
113
+ }));
114
+ }
115
+ const sessionExpired = () => new BrowserError.BrowserSessionExpired({
116
+ provider,
117
+ id
118
+ });
119
+ const mapCdp = (operation, selector) => (e) => e.kind === "closed" ? sessionExpired() : new BrowserError.BrowserActionFailed({
120
+ provider,
121
+ operation,
122
+ ...selector !== void 0 ? { selector } : {},
123
+ ...e.reason !== void 0 ? { reason: e.reason } : {},
124
+ raw: e
125
+ });
126
+ const guarded = (effect) => Effect.gen(function* () {
127
+ if (yield* Ref.get(expired)) return yield* Effect.fail(sessionExpired());
128
+ yield* Ref.set(lastActivity, yield* Clock.currentTimeMillis);
129
+ return yield* effect;
130
+ });
131
+ const withBudget = (operation, budget, effect) => Effect.timeoutOrElse(effect, {
132
+ duration: budget,
133
+ orElse: () => Effect.fail(new BrowserError.BrowserTimeout({
134
+ provider,
135
+ operation,
136
+ budget
137
+ }))
138
+ });
139
+ const evalInPage = (expression, operation, selector) => cdp.send("Runtime.evaluate", {
140
+ expression,
141
+ returnByValue: true,
142
+ awaitPromise: true
143
+ }, sessionId).pipe(Effect.mapError(mapCdp(operation, selector)), Effect.flatMap((reply) => reply.exceptionDetails !== void 0 ? Effect.fail(new BrowserError.BrowserActionFailed({
144
+ provider,
145
+ operation,
146
+ ...selector !== void 0 ? { selector } : {},
147
+ reason: reply.exceptionDetails.exception?.description ?? reply.exceptionDetails.text,
148
+ raw: reply.exceptionDetails
149
+ })) : Effect.succeed(reply.result.value)));
150
+ /** Call a page-agent helper and decode its result with `schema`. */
151
+ const agent = (operation, fn, args, schema, selector) => evalInPage(agentExpression(fn, args), operation, selector).pipe(Effect.flatMap((value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError((e) => new BrowserError.BrowserActionFailed({
152
+ provider,
153
+ operation,
154
+ ...selector !== void 0 ? { selector } : {},
155
+ reason: "page agent returned an unexpected shape",
156
+ raw: e
157
+ })))));
158
+ /** Call a page-agent helper whose result is irrelevant. */
159
+ const agentVoid = (operation, fn, args, selector) => evalInPage(agentExpression(fn, args), operation, selector).pipe(Effect.asVoid);
160
+ const mouse = (params) => cdp.send("Input.dispatchMouseEvent", params, sessionId);
161
+ const clickAt = (point, clickCount) => Effect.gen(function* () {
162
+ yield* mouse({
163
+ type: "mouseMoved",
164
+ x: point.x,
165
+ y: point.y
166
+ });
167
+ yield* mouse({
168
+ type: "mousePressed",
169
+ x: point.x,
170
+ y: point.y,
171
+ button: "left",
172
+ clickCount
173
+ });
174
+ yield* mouse({
175
+ type: "mouseReleased",
176
+ x: point.x,
177
+ y: point.y,
178
+ button: "left",
179
+ clickCount
180
+ });
181
+ });
182
+ const clickSelector = (selector, clickCount) => agent("interact", "targetPoint", [selector], InjectedPoint, selector).pipe(Effect.flatMap((point) => clickAt(point, clickCount).pipe(Effect.mapError(mapCdp("interact", selector)))));
183
+ const keyEvents = (key) => {
184
+ const detail = KEY_DETAILS[key];
185
+ const text = detail?.text ?? (key.length === 1 ? key : void 0);
186
+ const common = detail === void 0 ? {} : {
187
+ code: detail.code,
188
+ windowsVirtualKeyCode: detail.keyCode,
189
+ nativeVirtualKeyCode: detail.keyCode
190
+ };
191
+ return cdp.send("Input.dispatchKeyEvent", {
192
+ type: "keyDown",
193
+ key,
194
+ ...common,
195
+ ...text !== void 0 ? { text } : {}
196
+ }, sessionId).pipe(Effect.andThen(cdp.send("Input.dispatchKeyEvent", {
197
+ type: "keyUp",
198
+ key,
199
+ ...common
200
+ }, sessionId)), Effect.asVoid);
201
+ };
202
+ const isLoadEvent = (event) => (event.sessionId === void 0 || event.sessionId === sessionId) && (event.method === "Page.loadEventFired" || event.method === "Page.lifecycleEvent" && Option.getOrUndefined(decodeLifecycleParams(event.params))?.name === "load");
203
+ const goto = (url) => guarded(Effect.scoped(Effect.gen(function* () {
204
+ const sub = yield* cdp.subscribe;
205
+ const reply = yield* cdp.send("Page.navigate", { url }, sessionId).pipe(Effect.mapError(mapCdp("navigate")));
206
+ if (reply.errorText !== void 0 && reply.errorText !== "") return yield* Effect.fail(new BrowserError.BrowserActionFailed({
207
+ provider,
208
+ operation: "navigate",
209
+ reason: reply.errorText,
210
+ raw: reply
211
+ }));
212
+ yield* PubSub.take(sub).pipe(Effect.repeat({ until: isLoadEvent }));
213
+ })).pipe((effect) => Effect.timeoutOrElse(effect, {
214
+ duration: NAVIGATE_BUDGET,
215
+ orElse: () => agent("navigate", "readyState", [], Schema.String).pipe(Effect.option, Effect.flatMap((state) => Option.isSome(state) && state.value === "complete" ? Effect.void : Effect.fail(new BrowserError.BrowserTimeout({
216
+ provider,
217
+ operation: "navigate",
218
+ budget: NAVIGATE_BUDGET
219
+ }))))
220
+ })));
221
+ const waitFor = (selector) => guarded(withBudget("query", WAITFOR_BACKSTOP, agent("observe", "waitForSelector", [selector, Duration.toMillis(WAITFOR_BUDGET)], Schema.Boolean, selector).pipe(Effect.flatMap((found) => found ? Effect.void : Effect.fail(new BrowserError.BrowserTimeout({
222
+ provider,
223
+ operation: "query",
224
+ budget: WAITFOR_BUDGET
225
+ }))))));
226
+ const content = (format) => guarded(withBudget("action", ACTION_BUDGET, agent("observe", format === "html" ? "html" : "markdown", [], Schema.String)));
227
+ const screenshot = (opts) => guarded(withBudget("action", ACTION_BUDGET, Effect.gen(function* () {
228
+ const clip = opts?.selector !== void 0 ? yield* agent("observe", "pageBox", [opts.selector], InjectedBox, opts.selector) : opts?.fullPage === true ? {
229
+ x: 0,
230
+ y: 0,
231
+ ...yield* agent("observe", "contentSize", [], InjectedSize)
232
+ } : void 0;
233
+ const reply = yield* cdp.send("Page.captureScreenshot", {
234
+ format: "png",
235
+ ...clip !== void 0 ? {
236
+ clip: {
237
+ ...clip,
238
+ scale: 1
239
+ },
240
+ captureBeyondViewport: true
241
+ } : {}
242
+ }, sessionId).pipe(Effect.mapError(mapCdp("observe")));
243
+ return yield* Effect.fromResult(Encoding.decodeBase64(reply.data)).pipe(Effect.mapError((e) => new BrowserError.BrowserActionFailed({
244
+ provider,
245
+ operation: "observe",
246
+ reason: "invalid base64 screenshot payload",
247
+ raw: e
248
+ })));
249
+ })));
250
+ const snapshot = guarded(withBudget("query", ACTION_BUDGET, cdp.send("Accessibility.getFullAXTree", {}, sessionId).pipe(Effect.mapError((e) => isMethodMissing(e) ? new BrowserError.BrowserUnsupported({
251
+ provider,
252
+ capability: "snapshot",
253
+ reason: "endpoint does not implement the CDP Accessibility domain"
254
+ }) : mapCdp("observe")(e)), Effect.map((reply) => buildAxTree(reply.nodes)))));
255
+ const query = (selector) => guarded(withBudget("query", ACTION_BUDGET, evalInPage(agentExpression("query", [selector, QUERY_LIMIT]), "observe", selector).pipe(Effect.flatMap((value) => decodeElements(value).pipe(Effect.mapError((e) => new BrowserError.BrowserActionFailed({
256
+ provider,
257
+ operation: "observe",
258
+ selector,
259
+ reason: "page agent returned an unexpected shape",
260
+ raw: e
261
+ })))), Effect.map((els) => els.map((el) => ({
262
+ ref: el.ref,
263
+ tag: el.tag,
264
+ ...el.text !== "" ? { text: el.text } : {},
265
+ attributes: el.attributes,
266
+ box: el.box
267
+ }))))));
268
+ const cookies = {
269
+ get: guarded(cdp.send("Network.getCookies", {}, sessionId).pipe(Effect.mapError(mapCdp("cookies")), Effect.map((reply) => reply.cookies.map((c) => ({
270
+ name: c.name,
271
+ value: c.value,
272
+ domain: c.domain,
273
+ path: c.path,
274
+ expires: c.expires,
275
+ httpOnly: c.httpOnly,
276
+ secure: c.secure,
277
+ ...c.sameSite !== void 0 ? { sameSite: c.sameSite } : {}
278
+ }))))),
279
+ set: (toSet) => guarded(cdp.send("Network.setCookies", { cookies: toSet.map((c) => ({
280
+ name: c.name,
281
+ value: c.value,
282
+ ...c.domain !== void 0 ? { domain: c.domain } : {},
283
+ ...c.path !== void 0 ? { path: c.path } : {},
284
+ ...c.expires !== void 0 ? { expires: c.expires } : {},
285
+ ...c.httpOnly !== void 0 ? { httpOnly: c.httpOnly } : {},
286
+ ...c.secure !== void 0 ? { secure: c.secure } : {},
287
+ ...c.sameSite !== void 0 ? { sameSite: c.sameSite } : {}
288
+ })) }, sessionId).pipe(Effect.mapError(mapCdp("cookies")), Effect.asVoid))
289
+ };
290
+ const interact = (effect) => guarded(withBudget("action", ACTION_BUDGET, effect));
291
+ return {
292
+ id,
293
+ goto,
294
+ waitFor,
295
+ click: (selector) => interact(clickSelector(selector, 1)),
296
+ dblclick: (selector) => interact(agent("interact", "targetPoint", [selector], InjectedPoint, selector).pipe(Effect.flatMap((point) => clickAt(point, 1).pipe(Effect.andThen(clickAt(point, 2)), Effect.mapError(mapCdp("interact", selector)))))),
297
+ fill: (selector, text) => interact(agentVoid("interact", "fill", [selector, text], selector)),
298
+ type: (selector, text) => interact(agentVoid("interact", "focusEl", [selector], selector).pipe(Effect.andThen(Effect.forEach(Array.from(text), keyEvents).pipe(Effect.asVoid, Effect.mapError(mapCdp("interact", selector)))))),
299
+ press: (key) => interact(keyEvents(key).pipe(Effect.mapError(mapCdp("interact")))),
300
+ hover: (selector) => interact(agent("interact", "targetPoint", [selector], InjectedPoint, selector).pipe(Effect.flatMap((point) => mouse({
301
+ type: "mouseMoved",
302
+ x: point.x,
303
+ y: point.y
304
+ }).pipe(Effect.asVoid, Effect.mapError(mapCdp("interact", selector)))))),
305
+ focus: (selector) => interact(agentVoid("interact", "focusEl", [selector], selector)),
306
+ select: (selector, value) => interact(agentVoid("interact", "selectValue", [selector, value], selector)),
307
+ check: (selector) => interact(agent("interact", "isChecked", [selector], Schema.Boolean, selector).pipe(Effect.flatMap((checked) => checked ? Effect.void : clickSelector(selector, 1)))),
308
+ uncheck: (selector) => interact(agent("interact", "isChecked", [selector], Schema.Boolean, selector).pipe(Effect.flatMap((checked) => checked ? clickSelector(selector, 1) : Effect.void))),
309
+ scroll: (opts) => interact(agentVoid("interact", "scrollBy", [opts.direction, opts.pixels ?? 600])),
310
+ scrollIntoView: (selector) => interact(agentVoid("interact", "scrollIntoView", [selector], selector)),
311
+ content,
312
+ screenshot,
313
+ snapshot,
314
+ query,
315
+ cookies,
316
+ evaluate: (script) => interact(evalInPage(script, "evaluate"))
317
+ };
318
+ });
319
+ //#endregion
320
+ export { isMethodMissing, makeSession };
321
+
322
+ //# sourceMappingURL=session.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.mjs","names":[],"sources":["../../src/internal/session.ts"],"sourcesContent":["/**\n * Builds a core `BrowserSession` over one attached CDP target.\n *\n * Verb mechanics (the hybrid split):\n * - Real input (`click`, `dblclick`, `hover`, `press`, `type`, `check`)\n * goes through the CDP `Input` domain, so events are trusted where the\n * engine distinguishes (obscura implements `Input.dispatch*`; full\n * Chrome obviously does).\n * - Everything DOM-shaped (`query`, `fill`, `select`, `waitFor`,\n * `content`, scrolling) runs as injected TypeScript from `injected.ts`\n * via `Runtime.evaluate`, the lowest-common-denominator CDP method.\n * Results are decoded with the wire schemas, never cast.\n * - `goto` awaits the `Page` load event through the transport's event\n * demux; `waitFor` parks a `MutationObserver` promise in the page via\n * `awaitPromise`. The host never polls.\n *\n * Failures: transport `CdpError`s are mapped here onto the public\n * `BrowserError` taxonomy with the operation each verb knows. Expiry (max\n * lifetime / idle) is enforced locally by scoped reaper fibers; after\n * expiry every verb fails `BrowserSessionExpired`.\n */\nimport * as CoreBrowser from \"@effect-uai/core/Browser\"\nimport * as BrowserError from \"@effect-uai/core/BrowserError\"\nimport { Clock, Duration, Effect, Encoding, Option, PubSub, Ref, Schema, type Scope } from \"effect\"\nimport type { Cdp, CdpError, CdpEvent, CdpParams, CdpReturn } from \"./cdp.js\"\nimport {\n agentExpression,\n InjectedBox,\n InjectedElementInfo,\n InjectedPoint,\n InjectedSize,\n type PageAgentApi,\n} from \"./injected.js\"\n\n// Per-operation defaults. `waitFor` runs the same budget inside the page\n// (the MutationObserver self-times-out) with the host bound as a backstop.\nconst NAVIGATE_BUDGET = Duration.seconds(30)\nconst WAITFOR_BUDGET = Duration.seconds(10)\nconst ACTION_BUDGET = Duration.seconds(30)\nconst WAITFOR_BACKSTOP = Duration.millis(Duration.toMillis(WAITFOR_BUDGET) + 5000)\n\n/** `query` returns at most this many matches to bound payload size. */\nconst QUERY_LIMIT = 100\n\n/** Reply errors that mean \"this endpoint does not implement the method\". */\nexport const isMethodMissing = (e: CdpError): boolean =>\n e.kind === \"reply\" &&\n /wasn't found|not found|unknown method|not implemented|unsupported/i.test(e.reason ?? \"\")\n\nconst KEY_DETAILS: Record<\n string,\n { readonly code: string; readonly keyCode: number; readonly text?: string }\n> = {\n Enter: { code: \"Enter\", keyCode: 13, text: \"\\r\" },\n Tab: { code: \"Tab\", keyCode: 9 },\n Escape: { code: \"Escape\", keyCode: 27 },\n Backspace: { code: \"Backspace\", keyCode: 8 },\n Delete: { code: \"Delete\", keyCode: 46 },\n ArrowUp: { code: \"ArrowUp\", keyCode: 38 },\n ArrowDown: { code: \"ArrowDown\", keyCode: 40 },\n ArrowLeft: { code: \"ArrowLeft\", keyCode: 37 },\n ArrowRight: { code: \"ArrowRight\", keyCode: 39 },\n Home: { code: \"Home\", keyCode: 36 },\n End: { code: \"End\", keyCode: 35 },\n PageUp: { code: \"PageUp\", keyCode: 33 },\n PageDown: { code: \"PageDown\", keyCode: 34 },\n}\n\n// ---------------------------------------------------------------------------\n// Boundary decoders: CDP leaves these values untyped (`any`), so they are\n// decoded rather than cast.\n// ---------------------------------------------------------------------------\n\nconst decodeLifecycleParams = Schema.decodeUnknownOption(Schema.Struct({ name: Schema.String }))\nconst decodeAxString = Schema.decodeUnknownOption(Schema.String)\nconst decodeAxValue = Schema.decodeUnknownOption(Schema.Union([Schema.String, Schema.Number]))\nconst decodeElements = Schema.decodeUnknownEffect(Schema.Array(InjectedElementInfo))\n\n// ---------------------------------------------------------------------------\n// Accessibility tree: flat AXNode list -> recursive core AxNode. Ignored\n// nodes are elided, their children promoted.\n// ---------------------------------------------------------------------------\n\ntype RawAxNode = CdpReturn<\"Accessibility.getFullAXTree\">[\"nodes\"][number]\n\nconst buildAxTree = (nodes: ReadonlyArray<RawAxNode>): CoreBrowser.AxNode => {\n const byId = new Map(nodes.map((n) => [n.nodeId, n]))\n const childrenOf = (node: RawAxNode): Array<CoreBrowser.AxNode> =>\n (node.childIds ?? []).flatMap((childId) => {\n const child = byId.get(childId)\n if (child === undefined) return []\n return child.ignored ? childrenOf(child) : [build(child)]\n })\n const build = (node: RawAxNode): CoreBrowser.AxNode => {\n const name = Option.filter(decodeAxString(node.name?.value), (s) => s !== \"\")\n const value = Option.filter(\n Option.map(decodeAxValue(node.value?.value), String),\n (s) => s !== \"\",\n )\n return {\n role: Option.getOrElse(decodeAxString(node.role?.value), () => \"unknown\"),\n ...(Option.isSome(name) ? { name: name.value } : {}),\n ...(Option.isSome(value) ? { value: value.value } : {}),\n children: childrenOf(node),\n }\n }\n const root = nodes.find((n) => n.parentId === undefined) ?? nodes[0]\n if (root === undefined) return { role: \"document\", children: [] }\n return root.ignored ? { role: \"document\", children: childrenOf(root) } : build(root)\n}\n\n// ---------------------------------------------------------------------------\n// Session builder\n// ---------------------------------------------------------------------------\n\nexport type MakeSessionOptions = {\n readonly cdp: Cdp\n readonly id: CoreBrowser.BrowserSessionId\n /** CDP target session id (flatten mode); routes every command. */\n readonly sessionId: string\n readonly provider: string\n readonly request: CoreBrowser.CommonSessionRequest\n /** Best-effort provider-side teardown, run when the session expires. */\n readonly dispose: Effect.Effect<void>\n}\n\nexport const makeSession = (\n options: MakeSessionOptions,\n): Effect.Effect<CoreBrowser.BrowserSession, never, Scope.Scope> =>\n Effect.gen(function* () {\n const { cdp, dispose, id, provider, request, sessionId } = options\n\n // -- expiry: local reaper fibers, scoped to the session -------------------\n\n const expired = yield* Ref.make(false)\n const lastActivity = yield* Ref.make(yield* Clock.currentTimeMillis)\n const expire = Ref.set(expired, true).pipe(Effect.andThen(Effect.ignore(dispose)))\n\n if (request.timeout !== undefined) {\n yield* Effect.sleep(request.timeout).pipe(Effect.andThen(expire), Effect.forkScoped)\n }\n if (request.idleTimeout !== undefined) {\n const idleMs = Duration.toMillis(Duration.fromInputUnsafe(request.idleTimeout))\n yield* Effect.forkScoped(\n Effect.gen(function* () {\n while (true) {\n const elapsed = (yield* Clock.currentTimeMillis) - (yield* Ref.get(lastActivity))\n if (elapsed >= idleMs) return yield* expire\n yield* Effect.sleep(Duration.millis(idleMs - elapsed))\n }\n }),\n )\n }\n\n // -- error mapping and shared wrappers -------------------------------------\n\n const sessionExpired = () => new BrowserError.BrowserSessionExpired({ provider, id })\n\n const mapCdp =\n (operation: BrowserError.BrowserActionFailed[\"operation\"], selector?: string) =>\n (e: CdpError): BrowserError.BrowserError =>\n e.kind === \"closed\"\n ? sessionExpired()\n : new BrowserError.BrowserActionFailed({\n provider,\n operation,\n ...(selector !== undefined ? { selector } : {}),\n ...(e.reason !== undefined ? { reason: e.reason } : {}),\n raw: e,\n })\n\n const guarded = <A, E>(\n effect: Effect.Effect<A, E>,\n ): Effect.Effect<A, E | BrowserError.BrowserSessionExpired> =>\n Effect.gen(function* () {\n if (yield* Ref.get(expired)) return yield* Effect.fail(sessionExpired())\n yield* Ref.set(lastActivity, yield* Clock.currentTimeMillis)\n return yield* effect\n })\n\n const withBudget = <A, E>(\n operation: BrowserError.BrowserTimeout[\"operation\"],\n budget: Duration.Duration,\n effect: Effect.Effect<A, E>,\n ): Effect.Effect<A, E | BrowserError.BrowserTimeout> =>\n Effect.timeoutOrElse(effect, {\n duration: budget,\n orElse: () => Effect.fail(new BrowserError.BrowserTimeout({ provider, operation, budget })),\n })\n\n const evalInPage = (\n expression: string,\n operation: BrowserError.BrowserActionFailed[\"operation\"],\n selector?: string,\n ): Effect.Effect<unknown, BrowserError.BrowserError> =>\n cdp\n .send(\n \"Runtime.evaluate\",\n { expression, returnByValue: true, awaitPromise: true },\n sessionId,\n )\n .pipe(\n Effect.mapError(mapCdp(operation, selector)),\n Effect.flatMap((reply) =>\n reply.exceptionDetails !== undefined\n ? Effect.fail(\n new BrowserError.BrowserActionFailed({\n provider,\n operation,\n ...(selector !== undefined ? { selector } : {}),\n reason:\n reply.exceptionDetails.exception?.description ?? reply.exceptionDetails.text,\n raw: reply.exceptionDetails,\n }),\n )\n : Effect.succeed(reply.result.value as unknown),\n ),\n )\n\n /** Call a page-agent helper and decode its result with `schema`. */\n const agent = <S extends Schema.Top & { readonly DecodingServices: never }>(\n operation: BrowserError.BrowserActionFailed[\"operation\"],\n fn: keyof PageAgentApi,\n args: ReadonlyArray<unknown>,\n schema: S,\n selector?: string,\n ): Effect.Effect<S[\"Type\"], BrowserError.BrowserError> =>\n evalInPage(agentExpression(fn, args), operation, selector).pipe(\n Effect.flatMap((value) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError(\n (e) =>\n new BrowserError.BrowserActionFailed({\n provider,\n operation,\n ...(selector !== undefined ? { selector } : {}),\n reason: \"page agent returned an unexpected shape\",\n raw: e,\n }),\n ),\n ),\n ),\n )\n\n /** Call a page-agent helper whose result is irrelevant. */\n const agentVoid = (\n operation: BrowserError.BrowserActionFailed[\"operation\"],\n fn: keyof PageAgentApi,\n args: ReadonlyArray<unknown>,\n selector?: string,\n ): Effect.Effect<void, BrowserError.BrowserError> =>\n evalInPage(agentExpression(fn, args), operation, selector).pipe(Effect.asVoid)\n\n // -- input primitives (trusted events via the Input domain) ----------------\n\n const mouse = (params: CdpParams<\"Input.dispatchMouseEvent\">) =>\n cdp.send(\"Input.dispatchMouseEvent\", params, sessionId)\n\n const clickAt = (point: InjectedPoint, clickCount: number): Effect.Effect<void, CdpError> =>\n Effect.gen(function* () {\n yield* mouse({ type: \"mouseMoved\", x: point.x, y: point.y })\n yield* mouse({ type: \"mousePressed\", x: point.x, y: point.y, button: \"left\", clickCount })\n yield* mouse({ type: \"mouseReleased\", x: point.x, y: point.y, button: \"left\", clickCount })\n })\n\n const clickSelector = (selector: string, clickCount: number) =>\n agent(\"interact\", \"targetPoint\", [selector], InjectedPoint, selector).pipe(\n Effect.flatMap((point) =>\n clickAt(point, clickCount).pipe(Effect.mapError(mapCdp(\"interact\", selector))),\n ),\n )\n\n const keyEvents = (key: string): Effect.Effect<void, CdpError> => {\n const detail = KEY_DETAILS[key]\n const text = detail?.text ?? (key.length === 1 ? key : undefined)\n const common =\n detail === undefined\n ? {}\n : {\n code: detail.code,\n windowsVirtualKeyCode: detail.keyCode,\n nativeVirtualKeyCode: detail.keyCode,\n }\n return cdp\n .send(\n \"Input.dispatchKeyEvent\",\n { type: \"keyDown\", key, ...common, ...(text !== undefined ? { text } : {}) },\n sessionId,\n )\n .pipe(\n Effect.andThen(\n cdp.send(\"Input.dispatchKeyEvent\", { type: \"keyUp\", key, ...common }, sessionId),\n ),\n Effect.asVoid,\n )\n }\n\n // -- navigation --------------------------------------------------------------\n\n const isLoadEvent = (event: CdpEvent): boolean =>\n (event.sessionId === undefined || event.sessionId === sessionId) &&\n (event.method === \"Page.loadEventFired\" ||\n (event.method === \"Page.lifecycleEvent\" &&\n Option.getOrUndefined(decodeLifecycleParams(event.params))?.name === \"load\"))\n\n const goto = (url: string) =>\n guarded(\n Effect.scoped(\n Effect.gen(function* () {\n // Subscribe BEFORE navigating so the load event cannot slip past.\n const sub = yield* cdp.subscribe\n const reply = yield* cdp\n .send(\"Page.navigate\", { url }, sessionId)\n .pipe(Effect.mapError(mapCdp(\"navigate\")))\n if (reply.errorText !== undefined && reply.errorText !== \"\") {\n return yield* Effect.fail(\n new BrowserError.BrowserActionFailed({\n provider,\n operation: \"navigate\",\n reason: reply.errorText,\n raw: reply,\n }),\n )\n }\n yield* PubSub.take(sub).pipe(Effect.repeat({ until: isLoadEvent }))\n }),\n ).pipe((effect) =>\n Effect.timeoutOrElse(effect, {\n duration: NAVIGATE_BUDGET,\n // Endpoints that don't emit load events still settle the\n // document; accept a complete readyState before failing.\n orElse: () =>\n agent(\"navigate\", \"readyState\", [], Schema.String).pipe(\n Effect.option,\n Effect.flatMap((state) =>\n Option.isSome(state) && state.value === \"complete\"\n ? Effect.void\n : Effect.fail(\n new BrowserError.BrowserTimeout({\n provider,\n operation: \"navigate\",\n budget: NAVIGATE_BUDGET,\n }),\n ),\n ),\n ),\n }),\n ),\n )\n\n const waitFor = (selector: string) =>\n guarded(\n withBudget(\n \"query\",\n WAITFOR_BACKSTOP,\n agent(\n \"observe\",\n \"waitForSelector\",\n [selector, Duration.toMillis(WAITFOR_BUDGET)],\n Schema.Boolean,\n selector,\n ).pipe(\n Effect.flatMap((found) =>\n found\n ? Effect.void\n : Effect.fail(\n new BrowserError.BrowserTimeout({\n provider,\n operation: \"query\",\n budget: WAITFOR_BUDGET,\n }),\n ),\n ),\n ),\n ),\n )\n\n // -- observation ---------------------------------------------------------------\n\n const content = (format?: CoreBrowser.PageFormat) =>\n guarded(\n withBudget(\n \"action\",\n ACTION_BUDGET,\n agent(\"observe\", format === \"html\" ? \"html\" : \"markdown\", [], Schema.String),\n ),\n )\n\n const screenshot = (opts?: CoreBrowser.ScreenshotOptions) =>\n guarded(\n withBudget(\n \"action\",\n ACTION_BUDGET,\n Effect.gen(function* () {\n const clip =\n opts?.selector !== undefined\n ? yield* agent(\"observe\", \"pageBox\", [opts.selector], InjectedBox, opts.selector)\n : opts?.fullPage === true\n ? { x: 0, y: 0, ...(yield* agent(\"observe\", \"contentSize\", [], InjectedSize)) }\n : undefined\n const reply = yield* cdp\n .send(\n \"Page.captureScreenshot\",\n {\n format: \"png\",\n ...(clip !== undefined\n ? { clip: { ...clip, scale: 1 }, captureBeyondViewport: true }\n : {}),\n },\n sessionId,\n )\n .pipe(Effect.mapError(mapCdp(\"observe\")))\n return yield* Effect.fromResult(Encoding.decodeBase64(reply.data)).pipe(\n Effect.mapError(\n (e) =>\n new BrowserError.BrowserActionFailed({\n provider,\n operation: \"observe\",\n reason: \"invalid base64 screenshot payload\",\n raw: e,\n }),\n ),\n )\n }),\n ),\n )\n\n const snapshot = guarded(\n withBudget(\n \"query\",\n ACTION_BUDGET,\n cdp.send(\"Accessibility.getFullAXTree\", {}, sessionId).pipe(\n Effect.mapError((e) =>\n isMethodMissing(e)\n ? new BrowserError.BrowserUnsupported({\n provider,\n capability: \"snapshot\",\n reason: \"endpoint does not implement the CDP Accessibility domain\",\n })\n : mapCdp(\"observe\")(e),\n ),\n Effect.map((reply) => buildAxTree(reply.nodes)),\n ),\n ),\n )\n\n const query = (selector: string) =>\n guarded(\n withBudget(\n \"query\",\n ACTION_BUDGET,\n evalInPage(agentExpression(\"query\", [selector, QUERY_LIMIT]), \"observe\", selector).pipe(\n Effect.flatMap((value) =>\n decodeElements(value).pipe(\n Effect.mapError(\n (e) =>\n new BrowserError.BrowserActionFailed({\n provider,\n operation: \"observe\",\n selector,\n reason: \"page agent returned an unexpected shape\",\n raw: e,\n }),\n ),\n ),\n ),\n Effect.map((els) =>\n els.map(\n (el): CoreBrowser.ElementInfo => ({\n ref: el.ref,\n tag: el.tag,\n ...(el.text !== \"\" ? { text: el.text } : {}),\n attributes: el.attributes,\n box: el.box,\n }),\n ),\n ),\n ),\n ),\n )\n\n // -- state -----------------------------------------------------------------------\n\n const cookies: CoreBrowser.CookieApi = {\n get: guarded(\n cdp.send(\"Network.getCookies\", {}, sessionId).pipe(\n Effect.mapError(mapCdp(\"cookies\")),\n Effect.map((reply) =>\n reply.cookies.map(\n (c): CoreBrowser.Cookie => ({\n name: c.name,\n value: c.value,\n domain: c.domain,\n path: c.path,\n expires: c.expires,\n httpOnly: c.httpOnly,\n secure: c.secure,\n ...(c.sameSite !== undefined ? { sameSite: c.sameSite } : {}),\n }),\n ),\n ),\n ),\n ),\n set: (toSet) =>\n guarded(\n cdp\n .send(\n \"Network.setCookies\",\n {\n cookies: toSet.map((c) => ({\n name: c.name,\n value: c.value,\n ...(c.domain !== undefined ? { domain: c.domain } : {}),\n ...(c.path !== undefined ? { path: c.path } : {}),\n ...(c.expires !== undefined ? { expires: c.expires } : {}),\n ...(c.httpOnly !== undefined ? { httpOnly: c.httpOnly } : {}),\n ...(c.secure !== undefined ? { secure: c.secure } : {}),\n ...(c.sameSite !== undefined ? { sameSite: c.sameSite } : {}),\n })),\n },\n sessionId,\n )\n .pipe(Effect.mapError(mapCdp(\"cookies\")), Effect.asVoid),\n ),\n }\n\n // -- assemble ----------------------------------------------------------------------\n\n const interact = <A>(effect: Effect.Effect<A, BrowserError.BrowserError>) =>\n guarded(withBudget(\"action\", ACTION_BUDGET, effect))\n\n const session: CoreBrowser.BrowserSession = {\n id,\n\n goto,\n waitFor,\n\n click: (selector) => interact(clickSelector(selector, 1)),\n dblclick: (selector) =>\n interact(\n // Two pairs, the second with clickCount 2, matching real\n // double-click event streams.\n agent(\"interact\", \"targetPoint\", [selector], InjectedPoint, selector).pipe(\n Effect.flatMap((point) =>\n clickAt(point, 1).pipe(\n Effect.andThen(clickAt(point, 2)),\n Effect.mapError(mapCdp(\"interact\", selector)),\n ),\n ),\n ),\n ),\n fill: (selector, text) => interact(agentVoid(\"interact\", \"fill\", [selector, text], selector)),\n type: (selector, text) =>\n interact(\n agentVoid(\"interact\", \"focusEl\", [selector], selector).pipe(\n Effect.andThen(\n Effect.forEach(Array.from(text), keyEvents).pipe(\n Effect.asVoid,\n Effect.mapError(mapCdp(\"interact\", selector)),\n ),\n ),\n ),\n ),\n press: (key) => interact(keyEvents(key).pipe(Effect.mapError(mapCdp(\"interact\")))),\n hover: (selector) =>\n interact(\n agent(\"interact\", \"targetPoint\", [selector], InjectedPoint, selector).pipe(\n Effect.flatMap((point) =>\n mouse({ type: \"mouseMoved\", x: point.x, y: point.y }).pipe(\n Effect.asVoid,\n Effect.mapError(mapCdp(\"interact\", selector)),\n ),\n ),\n ),\n ),\n focus: (selector) => interact(agentVoid(\"interact\", \"focusEl\", [selector], selector)),\n select: (selector, value) =>\n interact(agentVoid(\"interact\", \"selectValue\", [selector, value], selector)),\n check: (selector) =>\n interact(\n agent(\"interact\", \"isChecked\", [selector], Schema.Boolean, selector).pipe(\n Effect.flatMap((checked) => (checked ? Effect.void : clickSelector(selector, 1))),\n ),\n ),\n uncheck: (selector) =>\n interact(\n agent(\"interact\", \"isChecked\", [selector], Schema.Boolean, selector).pipe(\n Effect.flatMap((checked) => (checked ? clickSelector(selector, 1) : Effect.void)),\n ),\n ),\n scroll: (opts) =>\n interact(agentVoid(\"interact\", \"scrollBy\", [opts.direction, opts.pixels ?? 600])),\n scrollIntoView: (selector) =>\n interact(agentVoid(\"interact\", \"scrollIntoView\", [selector], selector)),\n\n content,\n screenshot,\n snapshot,\n query,\n\n cookies,\n evaluate: (script) => interact(evalInPage(script, \"evaluate\")),\n }\n\n return session\n })\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@effect-uai/browser",
3
+ "version": "0.9.0",
4
+ "description": "Generic Chrome DevTools Protocol browser provider for @effect-uai/core.",
5
+ "keywords": [
6
+ "agents",
7
+ "ai",
8
+ "browser",
9
+ "cdp",
10
+ "chrome-devtools-protocol",
11
+ "effect"
12
+ ],
13
+ "homepage": "https://github.com/betalyra/effect-uai/tree/main/packages/providers/browser",
14
+ "bugs": {
15
+ "url": "https://github.com/betalyra/effect-uai/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Betalyra",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/betalyra/effect-uai",
22
+ "directory": "packages/providers/browser"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "type": "module",
31
+ "main": "./dist/index.mjs",
32
+ "types": "./dist/index.d.mts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.mts",
36
+ "import": "./dist/index.mjs"
37
+ },
38
+ "./Connect": {
39
+ "types": "./dist/Connect.d.mts",
40
+ "import": "./dist/Connect.mjs"
41
+ }
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {},
47
+ "devDependencies": {
48
+ "devtools-protocol": "^0.0.1651496",
49
+ "effect": "4.0.0-beta.57",
50
+ "typescript": "^6.0.3",
51
+ "@effect-uai/core": "0.9.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@effect-uai/core": ">=0.5.0 <1",
55
+ "effect": "4.0.0-beta.57"
56
+ },
57
+ "scripts": {
58
+ "build": "tsdown",
59
+ "typecheck": "tsc --noEmit"
60
+ }
61
+ }
package/src/Connect.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Generic CDP connect provider.
3
+ *
4
+ * Point it at any browser-level CDP WebSocket endpoint and it registers
5
+ * the core `Browser` capability over it. This is the shared floor for the
6
+ * whole field: a locally launched Chromium (`--remote-debugging-port`),
7
+ * obscura's partial CDP server, or a hosted vendor's `wss://` connect URL
8
+ * pasted straight in. Vendor-specific packages (session-create REST call,
9
+ * auth handshake) can layer on top and reuse this data plane.
10
+ *
11
+ * Sessions map to CDP targets: `create` opens a fresh target (tab),
12
+ * `attach` re-attaches to an existing one by target id (finalizer detaches
13
+ * without closing, for warm reuse), `destroy` closes the target from
14
+ * anywhere. The `timeout` / `idleTimeout` knobs on the create request are
15
+ * enforced locally by the session's reaper fibers; a plain CDP endpoint
16
+ * has no server-side cap to mirror them to.
17
+ */
18
+ import * as CoreBrowser from "@effect-uai/core/Browser"
19
+ import * as BrowserError from "@effect-uai/core/BrowserError"
20
+ import { Context, Effect, Layer } from "effect"
21
+ import { type Cdp, type CdpError, openCdp } from "./internal/cdp.js"
22
+ import { isMethodMissing, makeSession } from "./internal/session.js"
23
+
24
+ const PROVIDER = "cdp"
25
+
26
+ export type CdpConnectConfig = {
27
+ /**
28
+ * Browser-level CDP WebSocket endpoint, e.g.
29
+ * `ws://127.0.0.1:9222/devtools/browser/<uuid>` for local Chromium or
30
+ * obscura, or a hosted provider's connect URL (credentials in the URL).
31
+ */
32
+ readonly endpoint: string
33
+ }
34
+
35
+ /**
36
+ * Provider tag. Identical surface to the core `Browser` service; both tags
37
+ * are registered by {@link layer}.
38
+ */
39
+ export class CdpBrowser extends Context.Service<CdpBrowser, CoreBrowser.BrowserService>()(
40
+ "@betalyra/effect-uai/providers/browser/CdpBrowser",
41
+ ) {}
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Error mapping (control-plane operations; the session maps its own).
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const mapCreate = (e: CdpError): BrowserError.BrowserError =>
48
+ new BrowserError.BrowserCreateFailed({
49
+ provider: PROVIDER,
50
+ ...(e.reason !== undefined ? { reason: e.reason } : {}),
51
+ raw: e,
52
+ })
53
+
54
+ const mapLookup =
55
+ (id: string) =>
56
+ (e: CdpError): BrowserError.BrowserError =>
57
+ e.kind === "reply" && /no target|not found|invalid target|no session/i.test(e.reason ?? "")
58
+ ? new BrowserError.BrowserSessionNotFound({ provider: PROVIDER, id })
59
+ : mapCreate(e)
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Service
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Enable the domains the session verbs rely on. Partial CDP servers that
67
+ * auto-enable (or don't know an `enable` method) are tolerated: a
68
+ * method-missing reply is not an error.
69
+ */
70
+ const enableDomains = (cdp: Cdp, sessionId: string) =>
71
+ Effect.forEach(["Page.enable", "Runtime.enable", "Network.enable"] as const, (method) =>
72
+ cdp.send(method, {}, sessionId).pipe(Effect.catchIf(isMethodMissing, () => Effect.succeed({}))),
73
+ ).pipe(Effect.asVoid, Effect.mapError(mapCreate))
74
+
75
+ const applyViewport = (
76
+ cdp: Cdp,
77
+ sessionId: string,
78
+ viewport: NonNullable<CoreBrowser.CommonSessionRequest["viewport"]>,
79
+ ) =>
80
+ cdp
81
+ .send(
82
+ "Emulation.setDeviceMetricsOverride",
83
+ { width: viewport.width, height: viewport.height, deviceScaleFactor: 1, mobile: false },
84
+ sessionId,
85
+ )
86
+ .pipe(
87
+ // No Emulation domain: the engine's default viewport stands.
88
+ Effect.catchIf(isMethodMissing, () => Effect.succeed({})),
89
+ Effect.asVoid,
90
+ Effect.mapError(mapCreate),
91
+ )
92
+
93
+ const makeService = (config: CdpConnectConfig): CoreBrowser.BrowserService => ({
94
+ create: (request) =>
95
+ Effect.gen(function* () {
96
+ const cdp = yield* openCdp(config.endpoint)
97
+ const { targetId } = yield* cdp
98
+ .send("Target.createTarget", { url: "about:blank" })
99
+ .pipe(Effect.mapError(mapCreate))
100
+ const closeTarget = cdp.send("Target.closeTarget", { targetId }).pipe(Effect.ignore)
101
+ yield* Effect.addFinalizer(() => closeTarget)
102
+ const { sessionId } = yield* cdp
103
+ .send("Target.attachToTarget", { targetId, flatten: true })
104
+ .pipe(Effect.mapError(mapCreate))
105
+ yield* enableDomains(cdp, sessionId)
106
+ if (request.viewport !== undefined) {
107
+ yield* applyViewport(cdp, sessionId, request.viewport)
108
+ }
109
+ return yield* makeSession({
110
+ cdp,
111
+ id: CoreBrowser.BrowserSessionId(targetId),
112
+ sessionId,
113
+ provider: PROVIDER,
114
+ request,
115
+ dispose: closeTarget,
116
+ })
117
+ }),
118
+
119
+ attach: (id) =>
120
+ Effect.gen(function* () {
121
+ const cdp = yield* openCdp(config.endpoint)
122
+ const { sessionId } = yield* cdp
123
+ .send("Target.attachToTarget", { targetId: id, flatten: true })
124
+ .pipe(Effect.mapError(mapLookup(id)))
125
+ // Detach on scope close; the target keeps running for warm reuse.
126
+ yield* Effect.addFinalizer(() =>
127
+ cdp.send("Target.detachFromTarget", { sessionId }).pipe(Effect.ignore),
128
+ )
129
+ yield* enableDomains(cdp, sessionId)
130
+ return yield* makeSession({
131
+ cdp,
132
+ id,
133
+ sessionId,
134
+ provider: PROVIDER,
135
+ request: {},
136
+ dispose: Effect.void,
137
+ })
138
+ }),
139
+
140
+ list: Effect.scoped(
141
+ Effect.gen(function* () {
142
+ const cdp = yield* openCdp(config.endpoint)
143
+ const { targetInfos } = yield* cdp.send("Target.getTargets").pipe(Effect.mapError(mapCreate))
144
+ return targetInfos
145
+ .filter((target) => target.type === "page")
146
+ .map(
147
+ (target): CoreBrowser.BrowserSessionRef => ({
148
+ id: CoreBrowser.BrowserSessionId(target.targetId),
149
+ ...(target.url !== "" ? { url: target.url } : {}),
150
+ }),
151
+ )
152
+ }),
153
+ ),
154
+
155
+ destroy: (id) =>
156
+ Effect.scoped(
157
+ Effect.gen(function* () {
158
+ const cdp = yield* openCdp(config.endpoint)
159
+ yield* cdp.send("Target.closeTarget", { targetId: id }).pipe(Effect.mapError(mapLookup(id)))
160
+ }),
161
+ ),
162
+ })
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Layer
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Registers the generic CDP adapter against both the provider tag
170
+ * ({@link CdpBrowser}) and the core `Browser` tag. No capability markers:
171
+ * proxy / stealth / recording are vendor features, not part of a bare CDP
172
+ * endpoint.
173
+ */
174
+ export const layer = (config: CdpConnectConfig): Layer.Layer<CdpBrowser | CoreBrowser.Browser> => {
175
+ const service = makeService(config)
176
+ return Layer.mergeAll(
177
+ Layer.succeed(CdpBrowser, service),
178
+ Layer.succeed(CoreBrowser.Browser, service),
179
+ )
180
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * as Connect from "./Connect.js"