@oya-ai/browser 1.0.83 → 1.0.85

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/README.md CHANGED
@@ -26,81 +26,191 @@ Get an API key at [browser.getoya.ai](https://browser.getoya.ai) or self-host yo
26
26
 
27
27
  ---
28
28
 
29
- ## ⚡ Quickstart
29
+ ## Quickstart
30
30
 
31
- ### Natural Language Driving
31
+ Requires Node.js 18+; examples use ES modules. Set `OYA_API_KEY` in your environment. `OYA_BASE_URL` optionally points to a self-hosted control plane.
32
32
 
33
- ```ts
33
+ ```js
34
34
  import { Oya } from "@oya-ai/browser";
35
35
 
36
- const oya = new Oya(); // Reads process.env.OYA_API_KEY
36
+ const oya = new Oya();
37
+ const browser = await oya.browser.start({ captcha: "auto" });
38
+ try {
39
+ await browser.goto("https://example.com");
40
+ console.log(await browser.ask("What is the main heading on this page?"));
41
+ } finally {
42
+ await browser.stop();
43
+ }
44
+ ```
45
+
46
+ On Node.js 24+, `await using browser = await oya.browser.start()` also stops the browser when its scope exits, including on error.
37
47
 
38
- // Explicit resource management (Node 24+ / TS 5.2+)
39
- // Stops the browser automatically when the scope exits, even on error
40
- await using browser = await oya.browser.start({ persona: "auto", captcha: "auto" });
48
+ ## Portal automation: record once, replay with new inputs
41
49
 
42
- await browser.goto("https://news.ycombinator.com");
43
- const answer = await browser.ask("What are the top 3 stories and their points?");
44
- console.log(answer);
45
- ```
50
+ This example adapts the portal-automation project's workflow: reuse a persona, attach to an existing browser or start one, run a prompt the first time, then replay its saved playbook. The portal, workflow names, and request values below are fictional. Supply your own test portal and credentials through environment variables; adapt the task to its actual pages.
46
51
 
47
- ### Data and secrets
52
+ Save as `portal.mjs` and run `node portal.mjs` after setting `OYA_API_KEY`, `PORTAL_URL`, `PORTAL_USERNAME`, and `PORTAL_PASSWORD`. Set `OYA_BROWSER_ID` only to reuse an already running browser.
48
53
 
49
- ```ts
50
- await browser.ask("Log in as {{user}} with {{password}}, then book {{patient}} born {{dob}}", {
51
- data: { patient: "John Smith", dob: "Jan 5, 1970" }, // the agent reads these
52
- secrets: { user: "ops", password: process.env.PORTAL_PASSWORD! }, // the agent never sees these
53
- });
54
+ ```js
55
+ import { createInterface } from "node:readline/promises";
56
+ import { Oya } from "@oya-ai/browser";
57
+
58
+ function requiredEnv(name) {
59
+ const value = process.env[name];
60
+ if (!value) throw new Error(`Set ${name} before running this example.`);
61
+ return value;
62
+ }
63
+
64
+ const oya = new Oya({ apiKey: requiredEnv("OYA_API_KEY") });
65
+ const portalUrl = requiredEnv("PORTAL_URL");
66
+ const playbookName = "portal-request-review";
67
+ const secrets = {
68
+ username: requiredEnv("PORTAL_USERNAME"),
69
+ password: requiredEnv("PORTAL_PASSWORD"),
70
+ };
71
+ // Fictional test inputs. data is visible to the agent.
72
+ const data = {
73
+ customerName: "Alex Example",
74
+ requestId: "DEMO-0001",
75
+ requestedDate: "2030-01-15",
76
+ };
77
+ const task = [
78
+ "If not logged in, log in with {{username}} and {{password}}.",
79
+ "Open New Request and enter {{requestId}} as the reference.",
80
+ "Fill first name {{customerName|first}} and last name {{customerName|last}}.",
81
+ "Set the requested date to {{requestedDate|date:MM/DD/YYYY}}.",
82
+ "If a field is already correct, do not type its value again.",
83
+ "If an action times out, inspect the page before retrying it.",
84
+ "If information is missing, ask the person instead of guessing.",
85
+ "Stop on the review page. Do not submit the request.",
86
+ ].join("\n");
87
+
88
+ const existingId = process.env.OYA_BROWSER_ID;
89
+ let browser;
90
+ if (existingId) {
91
+ browser = await oya.browser.get(existingId);
92
+ } else {
93
+ const persona = (await oya.personas.list())
94
+ .find((p) => p.name === "portal-demo")
95
+ ?? await oya.personas.create({ name: "portal-demo" });
96
+ browser = await oya.browser.start({ persona: persona.id, captcha: "auto" });
97
+ }
98
+
99
+ try {
100
+ console.log("Watch in your Oya dashboard:", browser.liveViewUrl());
101
+ await browser.goto(portalUrl);
102
+ const exists = (await oya.playbooks.list()).some((p) => p.name === playbookName);
103
+ const run = await browser.submit(
104
+ exists ? { playbook: playbookName } : { prompt: task },
105
+ {
106
+ // Replay accepts all variables in data; the playbook remembers secret names.
107
+ ...(exists ? { data: { ...data, ...secrets } } : { data, secrets }),
108
+ onSuccess: () => console.log("Run succeeded."),
109
+ onFailure: (error) => console.error("Run failed with status:", error.status),
110
+ onHealed: (result) => {
111
+ console.log("A repair draft is ready for review:", result.draft);
112
+ },
113
+ onHumanAttention: async (request) => {
114
+ console.log("Attention needed:", request.reason);
115
+ console.log(request.message);
116
+ console.log("Open:", request.liveViewUrl ?? browser.liveViewUrl());
117
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
118
+ try {
119
+ const answer = await terminal.question(request.reason === "agent"
120
+ ? "Answer the agent: "
121
+ : "Handle this in the live view, then press Enter: ");
122
+ await request.respond(answer || "done");
123
+ } finally {
124
+ terminal.close();
125
+ }
126
+ },
127
+ },
128
+ );
129
+
130
+ await run.done; // Rejects on failure; do not save a failed run as a playbook.
131
+ const info = await run.status();
132
+ console.log("Run status:", info.status);
133
+ if (!exists) {
134
+ const playbook = await browser.toPlaybook(playbookName);
135
+ console.log("Saved:", playbook.name, "Steps:", playbook.steps);
136
+ console.log("Variables:", playbook.variables);
137
+ // playbook.code contains the flow as an exported Playwright module.
138
+ }
139
+ } finally {
140
+ // Leave an attached browser open; stop only the browser this script started.
141
+ if (!existingId) await browser.stop();
142
+ }
54
143
  ```
55
144
 
56
- The agent types every value as a placeholder, transforming it with filters when a form needs a piece or another format: `{{patient|first}}`, `{{patient|last}}`, `{{dob|date:MM/DD/YYYY}}`, `{{dob|date:YYYY}}`, `{{phone|digits}}`. A playbook saved from the run stores the placeholders, never the values.
145
+ `submit()` starts a background run and returns a `Run` handle. The SDK polls every two seconds by default (`pollMs` overrides this). `run.done` resolves with the result or rejects with an error; `run.status()` reads the run record. Attention reasons are `captcha`, `mfa`, `agent`, and `heal_failed`. A run waits up to 30 minutes for `request.respond()` or `run.respond()`. Run records are held in server memory for one hour after completion; they do not survive a server restart.
57
146
 
58
- ### Playbooks: ask once, replay without the LLM
147
+ ### Data, secrets, and reusable placeholders
59
148
 
60
- ```ts
61
- const pb = await browser.toPlaybook("download-invoice");
62
- console.log(pb.variables); // ["orderNumber"]
63
- console.log(pb.code); // the same flow as Playwright
64
-
65
- // Later, on any browser. If the site changed, the agent finishes the task
66
- // and saves its fix as a draft ("download-invoice:draft").
67
- const result = await browser.play("download-invoice", { orderNumber: "2077" });
68
- if (result.healed) await oya.playbooks.promote("download-invoice"); // after reviewing it
69
- // autoHeal: false throws at the broken step instead.
149
+ Use `{{name}}` references in prompts instead of interpolating values into the prompt text. `data` is available to the agent for reasoning; `secrets` supplies values for typing without including them as readable task inputs. Filters include `first`, `last`, `digits`, and `date:MM/DD/YYYY`.
150
+
151
+ For prompt runs, pass credentials in `secrets`. For replay, pass all variables in `data`: the saved playbook tracks which variables are secret, including during healing. Placeholder-based inputs remain variables in the saved flow. This is not a blanket redaction guarantee for page content, screenshots, agent replies, or application logs; inspect exported code before sharing it.
152
+
153
+ ### Review a repaired playbook
154
+
155
+ Replay normally runs recorded steps without an LLM. With `autoHeal: true` (the default), a broken step can hand over to the agent, which saves a repair as `<name>:draft`. Promotion replaces the saved playbook with that draft.
156
+
157
+ The following continues with an active `browser` and the inputs above. Replaying a draft performs its actions, so review its code and use test inputs before promoting it.
158
+
159
+ ```js
160
+ const saved = (await oya.playbooks.list())
161
+ .find((p) => p.name === playbookName);
162
+ if (saved?.draft) {
163
+ // Review saved.draft.code before executing it.
164
+ await browser.play(`${playbookName}:draft`, { ...data, ...secrets }, { autoHeal: false });
165
+ await oya.playbooks.promote(playbookName);
166
+ }
70
167
  ```
71
168
 
72
- ### Submit and get called back
169
+ Use `autoHeal: false` with `play()` or `submit({ playbook: name }, options)` to fail at a broken step without agent repair. `oya.playbooks.remove(name)` removes a playbook and its draft; `remove("<name>:draft")` removes only the draft.
73
170
 
74
- ```ts
75
- const run = await browser.submit({ playbook: "download-invoice" }, {
76
- data: { orderNumber: "2077" },
77
- onSuccess: (result) => console.log("done", result),
78
- onFailure: (error) => console.error("failed", error.message),
79
- // CAPTCHA or MFA it could not clear, the agent asking a question, or a replay it could not heal.
80
- onHumanAttention: async (req) => {
81
- console.log(req.reason, req.message, req.liveViewUrl);
82
- await req.respond("done"); // or your answer, when req.reason === "agent"
83
- },
84
- onHealed: (result) => console.log("fix saved as", result.draft),
171
+ ## Use your own model key
172
+
173
+ As in the portal-automation project's model setup script, configure the model on your Oya API key once for subsequent agent runs:
174
+
175
+ ```js
176
+ import { Oya } from "@oya-ai/browser";
177
+
178
+ const modelKey = process.env.GEMINI_API_KEY;
179
+ if (!modelKey) throw new Error("Set GEMINI_API_KEY first.");
180
+ const oya = new Oya();
181
+ await oya.config.set({
182
+ llm_provider: "gemini", // "openai" | "anthropic" | "gemini"
183
+ openai_api_key: modelKey, // Shared field name for every supported provider.
184
+ // chat_model: process.env.OYA_CHAT_MODEL, // Optional provider model override.
85
185
  });
86
- await run.done;
87
186
  ```
88
187
 
89
- > **Universal Lifecycle:** If you are not using `await using`, manage lifecycle with `try / finally`:
90
- > ```ts
91
- > const browser = await oya.browser.start();
92
- > try {
93
- > await browser.goto("https://example.com");
94
- > } finally {
95
- > await browser.stop();
96
- > }
97
- > ```
188
+ Omit `chat_model` to use the configured provider's default. `oya.config.get()` reads configuration. To return to the shared model configuration:
189
+
190
+ ```js
191
+ await oya.config.set({ llm_provider: null, openai_api_key: null, chat_model: null });
192
+ ```
193
+
194
+ ## Live view, sharing, and embedded streams
195
+
196
+ `browser.liveViewUrl()` returns a dashboard link without an API key; the viewer signs into Oya. For a scoped handoff to someone else, create an expiring share link:
197
+
198
+ ```js
199
+ const share = await browser.shareUrl({ control: true, expiresInSeconds: 900 });
200
+ // Deliver share.url privately to the intended operator.
201
+ // Once the handoff is finished:
202
+ await browser.revokeShare(share.id);
203
+ ```
204
+
205
+ Share links are view-only by default. `control: true` permits browser interaction. Anyone holding the link has its access until expiry or revocation.
206
+
207
+ For an embedded SSE stream of JPEG frames, use `await browser.liveStreamUrl()`. It mints a single-use connection ticket valid for 60 seconds; request a new URL for each connection.
98
208
 
99
209
  ---
100
210
 
101
211
  ## 🛡️ Deterministic Personas (Anti-Ban Identity)
102
212
 
103
- A persona is a permanent, mathematically seeded device identity: **fingerprint + cookie jar + residential proxy**, identical on every run to eliminate bot-farm and device-farm flags.
213
+ A persona groups a stable device fingerprint, saved login cookies, and a proxy assignment for reuse across browser sessions.
104
214
 
105
215
  ```ts
106
216
  import { Oya } from "@oya-ai/browser";
@@ -112,7 +222,7 @@ const persona = await oya.personas.create({
112
222
  name: "us-shopper",
113
223
  prefs: { platform: "MacIntel", timezone: "America/New_York", locale: "en-US" },
114
224
  proxy: { geo: "US" },
115
- maxConcurrent: 2, // Concurrency cap prevents device-farm detection
225
+ maxConcurrent: 2, // Limit simultaneous sessions for this identity
116
226
  });
117
227
 
118
228
  // Launch a browser with this persona (or persona: 'auto' for least-recently-used)
@@ -208,7 +318,7 @@ console.log("Page Title:", await page.title());
208
318
 
209
319
  ---
210
320
 
211
- ## 📚 Complete API Reference
321
+ ## API reference
212
322
 
213
323
  ### Initialization
214
324
 
@@ -243,14 +353,18 @@ const oya = new Oya({
243
353
  - `queueMs?: number` — Wait duration for fleet capacity (ms)
244
354
  - `budgetUsd?: number` — Enforce budget limit for session
245
355
  - `idempotencyKey?: string` — Safe retry token
246
- - `governed?: boolean` — Enforce strict isolation policies
356
+ - `governed?: boolean` — Enable governed session controls
357
+ - `profile?: string` — Saved login profile (takes precedence over `persona`)
358
+ - `priority?: 'low' | 'normal' | 'high'` — Queue priority
359
+ - `policy?: { allowedHosts?, humanHosts?, region?, redactRecording? }` — Session policy
360
+ - `readyTimeoutMs?: number` — Wait budget for a starting browser to connect
247
361
 
248
362
  ### Browser Instance Methods (`browser.*`)
249
363
 
250
364
  | Method | Returns | Description |
251
365
  |:---|:---|:---|
252
366
  | `goto(url)` | `Promise<void>` | Navigate to URL (with optional auto-CAPTCHA) |
253
- | `ask(prompt)` | `Promise<string>` | Natural-language AI driving using key's configured model |
367
+ | `ask(prompt, { data?, secrets? }?)` | `Promise<string>` | Natural-language AI driving using key's configured model |
254
368
  | `analyze()` | `Promise<Analysis>` | Returns markdown representation and numbered elements |
255
369
  | `elements()` | `Promise<Element[]>` | Returns only visible interactable elements |
256
370
  | `click(elementId)` | `Promise<void>` | Click element by numeric ID from `analyze()` |
@@ -266,11 +380,19 @@ const oya = new Oya({
266
380
  | `closeTab(tabId)` | `Promise<void>` | Close target tab |
267
381
  | `solveCaptcha()` | `Promise<CaptchaResult>` | Detect and solve on-screen CAPTCHA |
268
382
  | `completeMfa()` | `Promise<MfaResult>` | Resolve TOTP/SMS MFA or return `liveViewUrl` |
269
- | `liveViewUrl()` | `string` | SSE JPEG stream URL for sub-second human takeover |
383
+ | `liveViewUrl()` | `string` | Dashboard link for this browser |
384
+ | `liveStreamUrl()` | `Promise<string>` | SSE frame stream URL with a single-use ticket |
385
+ | `shareUrl(options?)` | `Promise<{ url, id, expiresAt }>` | Expiring browser share link; optional control access |
386
+ | `revokeShare(id)` | `Promise<void>` | Revoke a share link |
387
+ | `submit(task, options?)` | `Promise<Run>` | Background prompt or playbook with callbacks |
388
+ | `toPlaybook(name)` | `Promise<Playbook>` | Save the latest agent flow and export Playwright code |
389
+ | `play(name, data?, { autoHeal? }?)` | `Promise<PlayResult>` | Replay a saved flow |
270
390
  | `status()` | `Promise<BrowserDetail>` | Instance metrics, health, and recent activity log |
271
391
  | `stop()` | `Promise<StopResult>` | Tear down sandbox and release CDP session |
272
392
 
273
- ### Persona Management (`oya.personas`)
393
+ ### Profile and persona management (`oya.profiles`, `oya.personas`)
394
+
395
+ `oya.profiles` exposes the same methods as `oya.personas`; the persona name remains available for existing integrations.
274
396
 
275
397
  | Method | Description |
276
398
  |:---|:---|
@@ -312,7 +434,7 @@ await oya.personas.pinProxy(persona.id, proxy.id);
312
434
  | `session(id)` | Get detailed session execution state |
313
435
  | `takeover(id, 'acquire' \| 'release' \| 'resume')` | Manage human control leases |
314
436
  | `ticket(id)` | Generate single-use connection ticket for secure handoff |
315
- | `events(after?)` | Stream append-only audit event log |
437
+ | `events(after?)` | Read audit events and a pagination cursor |
316
438
  | `createCredential(options)` | Mint scoped service credential (`viewer` / `operator` / `administrator`) |
317
439
  | `createWebhook(url, types)` | Register HMAC-signed webhook for fleet lifecycle events |
318
440
 
@@ -320,7 +442,7 @@ await oya.personas.pinProxy(persona.id, proxy.id);
320
442
 
321
443
  ## 🚨 Error Handling
322
444
 
323
- All failed API and command operations throw an `OyaError`:
445
+ API error responses and failed browser commands throw `OyaError`. Network failures, request timeouts, and configuration errors may throw other error types:
324
446
 
325
447
  ```ts
326
448
  import { Oya, OyaError } from "@oya-ai/browser";
@@ -331,7 +453,9 @@ try {
331
453
  } catch (err) {
332
454
  if (err instanceof OyaError) {
333
455
  console.error(`Oya API Error (${err.status}):`, err.message);
334
- console.error("Payload:", err.body);
456
+ // err.body contains response details; inspect privately if needed.
457
+ } else {
458
+ throw err;
335
459
  }
336
460
  }
337
461
  ```
package/dist/index.cjs CHANGED
@@ -171,7 +171,7 @@ var Browser = class {
171
171
  await this.command("close_tab", { tab_id: tabId });
172
172
  }
173
173
  /**
174
- * Detect and clear a CAPTCHA. Providers that solve natively are left to do
174
+ x * Detect and clear a CAPTCHA. Providers that solve natively are left to do
175
175
  * it; everything else goes to the configured solver.
176
176
  */
177
177
  solveCaptcha() {
package/dist/index.d.cts CHANGED
@@ -424,7 +424,7 @@ declare class Browser {
424
424
  switchTab(tabId: string): Promise<void>;
425
425
  closeTab(tabId: string): Promise<void>;
426
426
  /**
427
- * Detect and clear a CAPTCHA. Providers that solve natively are left to do
427
+ x * Detect and clear a CAPTCHA. Providers that solve natively are left to do
428
428
  * it; everything else goes to the configured solver.
429
429
  */
430
430
  solveCaptcha(): Promise<CaptchaResult>;
package/dist/index.d.ts CHANGED
@@ -424,7 +424,7 @@ declare class Browser {
424
424
  switchTab(tabId: string): Promise<void>;
425
425
  closeTab(tabId: string): Promise<void>;
426
426
  /**
427
- * Detect and clear a CAPTCHA. Providers that solve natively are left to do
427
+ x * Detect and clear a CAPTCHA. Providers that solve natively are left to do
428
428
  * it; everything else goes to the configured solver.
429
429
  */
430
430
  solveCaptcha(): Promise<CaptchaResult>;
package/dist/index.js CHANGED
@@ -141,7 +141,7 @@ var Browser = class {
141
141
  await this.command("close_tab", { tab_id: tabId });
142
142
  }
143
143
  /**
144
- * Detect and clear a CAPTCHA. Providers that solve natively are left to do
144
+ x * Detect and clear a CAPTCHA. Providers that solve natively are left to do
145
145
  * it; everything else goes to the configured solver.
146
146
  */
147
147
  solveCaptcha() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oya-ai/browser",
3
- "version": "1.0.83",
3
+ "version": "1.0.85",
4
4
  "description": "Rotate thousands of browsers behind one API — personas, proxies, stealth, CAPTCHA and MFA.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://browser.getoya.ai",