@oya-ai/browser 0.1.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,361 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Browser: () => Browser,
24
+ Oya: () => Oya,
25
+ OyaError: () => OyaError,
26
+ default: () => index_default
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/types.ts
31
+ var OyaError = class extends Error {
32
+ status;
33
+ body;
34
+ constructor(message, status, body) {
35
+ super(message);
36
+ this.name = "OyaError";
37
+ this.status = status;
38
+ this.body = body;
39
+ }
40
+ };
41
+
42
+ // src/client.ts
43
+ var Http = class {
44
+ constructor(baseUrl, apiKey, timeoutMs, fetchImpl) {
45
+ this.baseUrl = baseUrl;
46
+ this.apiKey = apiKey;
47
+ this.timeoutMs = timeoutMs;
48
+ this.fetchImpl = fetchImpl;
49
+ }
50
+ baseUrl;
51
+ apiKey;
52
+ timeoutMs;
53
+ fetchImpl;
54
+ async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
55
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
56
+ method,
57
+ headers: {
58
+ Authorization: `Bearer ${this.apiKey}`,
59
+ ...headers,
60
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
61
+ },
62
+ body: body === void 0 ? void 0 : JSON.stringify(body),
63
+ signal: AbortSignal.timeout(timeoutMs)
64
+ });
65
+ const text = await res.text();
66
+ let payload;
67
+ try {
68
+ payload = text ? JSON.parse(text) : null;
69
+ } catch {
70
+ payload = text;
71
+ }
72
+ if (!res.ok) {
73
+ let message = payload?.error || `${method} ${path} failed (${res.status})`;
74
+ if (message === "Invalid API key") message += ` for ${this.baseUrl}. Check OYA_API_KEY: a value exported in your shell beats .env.`;
75
+ throw new OyaError(message, res.status, payload);
76
+ }
77
+ return payload;
78
+ }
79
+ };
80
+
81
+ // src/browser.ts
82
+ var NAVIGATE_TIMEOUT_MS = 12e4;
83
+ var Browser = class {
84
+ constructor(http, info, autoCaptcha) {
85
+ this.http = http;
86
+ this.autoCaptcha = autoCaptcha;
87
+ this.id = info.id;
88
+ this.provider = info.provider;
89
+ this.persona = info.persona;
90
+ this.cdpUrl = info.cdpUrl;
91
+ }
92
+ http;
93
+ autoCaptcha;
94
+ id;
95
+ provider;
96
+ persona;
97
+ /** Point Playwright, Puppeteer or browser-use here. */
98
+ cdpUrl;
99
+ async command(action, params = {}, timeoutMs) {
100
+ const result = await this.http.request(
101
+ "POST",
102
+ `/api/browsers/${this.id}/command`,
103
+ { action, params },
104
+ timeoutMs
105
+ );
106
+ if (result.ok === false) throw new OyaError(result.error || `${action} failed`, 422, result);
107
+ return result.data;
108
+ }
109
+ async goto(url) {
110
+ await this.command("navigate", { url }, NAVIGATE_TIMEOUT_MS);
111
+ if (this.autoCaptcha) {
112
+ const result = await this.solveCaptcha();
113
+ if (result.present && !result.solved && !result.invisible) throw new OyaError(result.error || "CAPTCHA needs attention. Call solveCaptcha() again or open the live view.", 409, result);
114
+ }
115
+ }
116
+ /** The page as markdown plus numbered elements to act on. */
117
+ async analyze() {
118
+ return this.command("analyze");
119
+ }
120
+ /** Only the visible elements, which is what an agent almost always wants. */
121
+ async elements() {
122
+ return (await this.analyze()).elements.filter((e) => e.visible);
123
+ }
124
+ async click(elementId) {
125
+ const id = this.elementId(elementId);
126
+ await this.command("click", { element_id: id, selector: `[data-ac-id="${id}"]` });
127
+ }
128
+ async type(elementId, text) {
129
+ const id = this.elementId(elementId);
130
+ return this.command("type", { element_id: id, selector: `[data-ac-id="${id}"]`, text });
131
+ }
132
+ elementId(value) {
133
+ const id = Number(value);
134
+ if (typeof value !== "number" && typeof value !== "string" || value === "" || !Number.isInteger(id) || id < 0) {
135
+ throw new OyaError("Use a numeric element id from browser.analyze().", 400, null);
136
+ }
137
+ return id;
138
+ }
139
+ async pressKey(key) {
140
+ await this.command("press_key", { key });
141
+ }
142
+ /** `at` aims the wheel at an inner scroller (a results panel, a chat pane) instead of the page. */
143
+ async scroll(direction, amount, at) {
144
+ await this.command("scroll", at ? { direction, amount: amount ?? 500, ...at, smooth: false } : { direction, amount });
145
+ }
146
+ async waitFor(selector, timeout = 3e4) {
147
+ await this.command("wait", { selector, timeout }, timeout + 5e3);
148
+ }
149
+ /** A `data:image/…;base64,` URL. PNG or JPEG depending on the driver. */
150
+ async screenshot() {
151
+ const data = await this.command("screenshot");
152
+ return data.screenshot;
153
+ }
154
+ async url() {
155
+ const tabs = await this.tabs();
156
+ return tabs.find((t) => t.active)?.url || "";
157
+ }
158
+ async tabs() {
159
+ const data = await this.command("list_tabs");
160
+ return data.tabs || [];
161
+ }
162
+ async openTab(url) {
163
+ const data = await this.command("open_tab", { url }, NAVIGATE_TIMEOUT_MS);
164
+ return data.tab_id;
165
+ }
166
+ async switchTab(tabId) {
167
+ await this.command("switch_tab", { tab_id: tabId });
168
+ }
169
+ async closeTab(tabId) {
170
+ await this.command("close_tab", { tab_id: tabId });
171
+ }
172
+ /**
173
+ * Detect and clear a CAPTCHA. Providers that solve natively are left to do
174
+ * it; everything else goes to the configured solver.
175
+ */
176
+ solveCaptcha() {
177
+ return this.http.request("POST", `/api/browsers/${this.id}/captcha`, {}, 18e4);
178
+ }
179
+ /**
180
+ * Answer an MFA prompt with the persona's configured factor. When nothing can
181
+ * answer it, `liveViewUrl` is where a person finishes by hand.
182
+ */
183
+ async completeMfa() {
184
+ const result = await this.http.request("POST", `/api/browsers/${this.id}/mfa`, {}, 18e4);
185
+ if (result.liveViewUrl) result.liveViewUrl = new URL(result.liveViewUrl, this.http.baseUrl).href;
186
+ return result;
187
+ }
188
+ /** Natural-language control, using this key's configured model. */
189
+ async ask(prompt) {
190
+ const res = await this.http.request(
191
+ "POST",
192
+ `/api/browsers/${this.id}/chat`,
193
+ { messages: [{ role: "user", content: prompt }] },
194
+ 6e5
195
+ );
196
+ return res.text;
197
+ }
198
+ /**
199
+ * Watch it work: an SSE stream of JPEG frames. EventSource cannot set
200
+ * headers, so the key travels as a query parameter — treat the URL itself as
201
+ * a credential.
202
+ */
203
+ liveViewUrl() {
204
+ return `${this.http.baseUrl}/api/live/${this.id}?key=${encodeURIComponent(this.http.apiKey)}`;
205
+ }
206
+ /** Counters, health and the last 50 things this browser did. */
207
+ status() {
208
+ return this.http.request("GET", `/api/browsers/${this.id}`);
209
+ }
210
+ /**
211
+ * Stop it, whatever it is: a cloud sandbox is destroyed so billing ends, a
212
+ * CDP session is handed back to its provider, a desktop browser disconnects.
213
+ */
214
+ stop() {
215
+ return this.http.request("POST", `/api/browsers/${this.id}/stop`, {}, 6e4);
216
+ }
217
+ /** `await using browser = await oya.browser.start()` stops it however the block exits, errors included. */
218
+ async [Symbol.asyncDispose]() {
219
+ await this.stop();
220
+ }
221
+ /** @deprecated use stop() — close() only dropped the socket, and a cloud browser redialled. */
222
+ async close() {
223
+ await this.stop();
224
+ }
225
+ };
226
+
227
+ // src/index.ts
228
+ var DEFAULT_BASE_URL = "https://browser.getoya.ai";
229
+ var READY_POLL_MS = 2e3;
230
+ var env = (name) => globalThis.process?.env?.[name];
231
+ var Oya = class {
232
+ http;
233
+ constructor(options = {}) {
234
+ const apiKey = options.apiKey || env("OYA_API_KEY");
235
+ if (!apiKey) {
236
+ throw new Error("No API key. Pass { apiKey } or set OYA_API_KEY \u2014 run `oya login` to get one.");
237
+ }
238
+ const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || DEFAULT_BASE_URL).replace(/\/+$/, "");
239
+ const fetchImpl = options.fetch || globalThis.fetch;
240
+ if (!fetchImpl) throw new Error("No fetch available \u2014 pass { fetch } or use Node 18+.");
241
+ this.http = new Http(baseUrl, apiKey, options.timeoutMs ?? 6e4, fetchImpl.bind(globalThis));
242
+ }
243
+ browser = {
244
+ /** Start a browser and wait until it can take commands. */
245
+ start: async (options = {}) => {
246
+ const started = await this.http.request("POST", "/api/browsers/start", {
247
+ profile: options.profile || options.persona,
248
+ provider: options.provider,
249
+ wsUrl: options.wsUrl,
250
+ name: options.name,
251
+ queueMs: options.queueMs,
252
+ priority: options.priority,
253
+ budgetUsd: options.budgetUsd,
254
+ governed: options.governed,
255
+ policy: options.policy
256
+ }, 12e4, { "Idempotency-Key": options.idempotencyKey || globalThis.crypto.randomUUID() });
257
+ if (started.status === "starting") {
258
+ await this.waitUntilConnected(started.id, options.readyTimeoutMs ?? 12e4 + (options.queueMs || 0));
259
+ const connected = await this.http.request("GET", `/api/browsers/${encodeURIComponent(started.id)}`);
260
+ started.cdpUrl = connected.cdpUrl;
261
+ }
262
+ return new Browser(this.http, started, options.captcha === "auto");
263
+ },
264
+ /** Reattach to a browser that is already running. */
265
+ get: async (id) => {
266
+ const found = await this.http.request("GET", `/api/browsers/${encodeURIComponent(id)}`);
267
+ return new Browser(this.http, {
268
+ id: found.id,
269
+ provider: found.provider || "cdp",
270
+ persona: found.persona || "default",
271
+ status: "ready",
272
+ cdpUrl: found.cdpUrl
273
+ }, false);
274
+ },
275
+ list: () => this.http.request("GET", "/api/browsers"),
276
+ /** Stop some (`ids`) or every browser on this key. Each reports separately. */
277
+ stop: (ids) => this.http.request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, 12e4),
278
+ stopAll: async () => (await this.browser.stop("all")).stopped
279
+ };
280
+ /** Durable operational controls, including disconnected and cleanup-pending sessions. */
281
+ control = {
282
+ overview: () => this.http.request("GET", "/api/control"),
283
+ sessions: () => this.http.request("GET", "/api/control/sessions"),
284
+ session: (id) => this.http.request("GET", `/api/control/sessions/${encodeURIComponent(id)}`),
285
+ settings: (changes) => this.http.request("PATCH", "/api/control/project", changes),
286
+ cancel: (id) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/cancel`, {}),
287
+ stop: (id, force = false) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/stop`, { force }),
288
+ takeover: (id, action) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/control`, { action }),
289
+ input: (id, action, params) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/input`, { action, params }),
290
+ recover: (id, replace = false) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/recover`, { replace }),
291
+ ticket: (id) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/ticket`, {}),
292
+ events: (after = 0) => this.http.request("GET", `/api/control/events?after=${after}`),
293
+ createCredential: (options) => this.http.request("POST", "/api/control/credentials", options),
294
+ revokeCredential: (id) => this.http.request("DELETE", `/api/control/credentials/${encodeURIComponent(id)}`),
295
+ members: () => this.http.request("GET", "/api/control/members"),
296
+ inviteMember: (role = "operator") => this.http.request("POST", "/api/control/members/invite", { role }),
297
+ removeMember: (userId) => this.http.request("DELETE", `/api/control/members/${encodeURIComponent(userId)}`),
298
+ createWebhook: (url, types = []) => this.http.request("POST", "/api/control/webhooks", { url, types }),
299
+ removeWebhook: (id) => this.http.request("DELETE", `/api/control/webhooks/${encodeURIComponent(id)}`),
300
+ replayDelivery: (id) => this.http.request("POST", `/api/control/deliveries/${encodeURIComponent(id)}/replay`, {})
301
+ };
302
+ personas = {
303
+ list: async () => (await this.http.request("GET", "/api/personas")).personas,
304
+ get: (id) => this.http.request("GET", `/api/personas/${id}`),
305
+ /**
306
+ * Create an identity. The device — platform, timezone, locale — is chosen
307
+ * here and fixed for its life; `preview()` shows what a choice produces.
308
+ */
309
+ create: (options = {}) => this.http.request("POST", "/api/personas", options),
310
+ /** Name, concurrency cap and proxy hint. Never the device — clone for that. */
311
+ update: (id, changes) => this.http.request("PUT", `/api/personas/${id}`, changes),
312
+ /** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
313
+ clone: (id, options = {}) => this.http.request("POST", `/api/personas/${id}/clone`, options),
314
+ /** The fingerprint these choices would produce. Persists nothing. */
315
+ preview: async (prefs = {}) => (await this.http.request("POST", "/api/personas/preview", { prefs })).fingerprint,
316
+ /** Platforms, and the timezones and locales each may coherently claim. */
317
+ options: () => this.http.request("GET", "/api/personas/options"),
318
+ /** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
319
+ pinProxy: (id, proxyId) => this.http.request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
320
+ remove: async (id) => {
321
+ await this.http.request("DELETE", `/api/personas/${id}`);
322
+ },
323
+ /** Store the second factor for this identity. Sealed at rest, never read back. */
324
+ setMfa: (id, config) => this.http.request("PUT", `/api/personas/${id}/mfa`, config),
325
+ clearMfa: async (id) => {
326
+ await this.http.request("DELETE", `/api/personas/${id}/mfa`);
327
+ }
328
+ };
329
+ /** This key's settings: LLM credentials, browser provider, solver. */
330
+ config = {
331
+ get: () => this.http.request("GET", "/api/config"),
332
+ set: (values) => this.http.request("POST", "/api/config", values)
333
+ };
334
+ /** Saved profiles. `personas` is retained as an alias for existing clients. */
335
+ profiles = this.personas;
336
+ usage() {
337
+ return this.http.request("GET", "/api/usage");
338
+ }
339
+ async waitUntilConnected(id, timeoutMs) {
340
+ const deadline = Date.now() + timeoutMs;
341
+ while (Date.now() < deadline) {
342
+ const all = await this.browser.list();
343
+ if (all.some((b) => b.id === id && b.health !== "dead")) return;
344
+ try {
345
+ const session = await this.control.session(id);
346
+ if (["failed", "stopped", "unknown_outcome"].includes(session.state)) throw new OyaError(`Browser creation ended in ${session.state}`, 409, session);
347
+ } catch (e) {
348
+ if (!(e instanceof OyaError) || e.status !== 404) throw e;
349
+ }
350
+ await new Promise((r) => setTimeout(r, READY_POLL_MS));
351
+ }
352
+ throw new OyaError(`Browser ${id} did not come up within ${Math.round(timeoutMs / 1e3)}s`, 504, null);
353
+ }
354
+ };
355
+ var index_default = Oya;
356
+ // Annotate the CommonJS export names for ESM import in node:
357
+ 0 && (module.exports = {
358
+ Browser,
359
+ Oya,
360
+ OyaError
361
+ });