@oya-ai/browser 1.0.73 → 1.0.75

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
@@ -44,6 +44,45 @@ const answer = await browser.ask("What are the top 3 stories and their points?")
44
44
  console.log(answer);
45
45
  ```
46
46
 
47
+ ### Data pass-through: the model never sees your values
48
+
49
+ ```ts
50
+ await browser.ask("Search for order {{orderNumber}} and download its invoice", {
51
+ data: { orderNumber: "1042" }, // typed into the page; the LLM only reads {{orderNumber}}
52
+ });
53
+ ```
54
+
55
+ ### Playbooks: ask once, replay without the LLM
56
+
57
+ ```ts
58
+ const pb = await browser.toPlaybook("download-invoice");
59
+ console.log(pb.variables); // ["orderNumber"]
60
+ console.log(pb.code); // the same flow as Playwright
61
+
62
+ // Later, on any browser. If the site changed, the agent finishes the task
63
+ // and saves its fix as a draft ("download-invoice:draft").
64
+ const result = await browser.play("download-invoice", { orderNumber: "2077" });
65
+ if (result.healed) await oya.playbooks.promote("download-invoice"); // after reviewing it
66
+ // autoHeal: false throws at the broken step instead.
67
+ ```
68
+
69
+ ### Submit and get called back
70
+
71
+ ```ts
72
+ const run = await browser.submit({ playbook: "download-invoice" }, {
73
+ data: { orderNumber: "2077" },
74
+ onSuccess: (result) => console.log("done", result),
75
+ onFailure: (error) => console.error("failed", error.message),
76
+ // CAPTCHA or MFA it could not clear, the agent asking a question, or a replay it could not heal.
77
+ onHumanAttention: async (req) => {
78
+ console.log(req.reason, req.message, req.liveViewUrl);
79
+ await req.respond("done"); // or your answer, when req.reason === "agent"
80
+ },
81
+ onHealed: (result) => console.log("fix saved as", result.draft),
82
+ });
83
+ await run.done;
84
+ ```
85
+
47
86
  > **Universal Lifecycle:** If you are not using `await using`, manage lifecycle with `try / finally`:
48
87
  > ```ts
49
88
  > const browser = await oya.browser.start();
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  Browser: () => Browser,
24
24
  Oya: () => Oya,
25
25
  OyaError: () => OyaError,
26
+ Run: () => Run,
26
27
  default: () => index_default
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
@@ -185,16 +186,57 @@ var Browser = class {
185
186
  if (result.liveViewUrl) result.liveViewUrl = new URL(result.liveViewUrl, this.http.baseUrl).href;
186
187
  return result;
187
188
  }
188
- /** Natural-language control, using this key's configured model. */
189
- async ask(prompt) {
189
+ /**
190
+ * Natural-language control, using this key's configured model. Refer to `data`
191
+ * as `{{name}}` in the prompt: the agent types the placeholder, the page gets the
192
+ * value, and the model never sees it.
193
+ */
194
+ async ask(prompt, { data } = {}) {
190
195
  const res = await this.http.request(
191
196
  "POST",
192
197
  `/api/browsers/${this.id}/chat`,
193
- { messages: [{ role: "user", content: prompt }] },
198
+ { messages: [{ role: "user", content: prompt }], data },
194
199
  6e5
195
200
  );
201
+ if (res.error) throw new OyaError(res.error, 500, res);
196
202
  return res.text;
197
203
  }
204
+ /**
205
+ * Save the last `ask()` on this browser as a named playbook. Values that came
206
+ * from the prompt (names, IDs, dates) become variables; `code` is the same
207
+ * flow as a Playwright module, to read or run yourself.
208
+ */
209
+ toPlaybook(name) {
210
+ return this.http.request("POST", `/api/browsers/${this.id}/playbooks`, { name }, 12e4);
211
+ }
212
+ /**
213
+ * Replay a playbook with no LLM in the loop. Variables left out reuse the
214
+ * recorded values where there are any. If a step no longer fits the page and
215
+ * `autoHeal` is on (the default), the agent finishes the task and its fix is
216
+ * saved as a draft (`healed`, `draft`); off, the step's error is thrown.
217
+ * Play `'<name>:draft'` to try a draft before promoting it.
218
+ */
219
+ async play(name, data = {}, { autoHeal = true } = {}) {
220
+ const res = await this.http.request(
221
+ "POST",
222
+ `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`,
223
+ { variables: data, autoHeal },
224
+ 6e5
225
+ );
226
+ if (res.error) throw new OyaError(res.error, 500, res);
227
+ return res;
228
+ }
229
+ /**
230
+ * Start a prompt or playbook in the background and hear back through callbacks.
231
+ * `onHumanAttention` fires for an unsolved CAPTCHA, an unfinished MFA, the agent
232
+ * asking for help, or a replay the agent could not heal; the run waits (up to 30
233
+ * minutes) until you call `respond()`.
234
+ */
235
+ async submit(task, options = {}) {
236
+ const { onSuccess, onFailure, onHumanAttention, onHealed, pollMs, ...body } = options;
237
+ const started = await this.http.request("POST", `/api/browsers/${this.id}/runs`, { ...task, ...body });
238
+ return new Run(this.http, started.id, { onSuccess, onFailure, onHumanAttention, onHealed }, pollMs);
239
+ }
198
240
  /**
199
241
  * Watch it work: the console, opened on this browser.
200
242
  *
@@ -266,6 +308,72 @@ var Browser = class {
266
308
  await this.stop();
267
309
  }
268
310
  };
311
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
312
+ var Run = class {
313
+ constructor(http, id, callbacks, pollMs = 2e3) {
314
+ this.http = http;
315
+ this.id = id;
316
+ this.done = this.watch(callbacks, pollMs);
317
+ this.done.catch(() => {
318
+ });
319
+ }
320
+ http;
321
+ id;
322
+ done;
323
+ status() {
324
+ return this.http.request("GET", `/api/runs/${encodeURIComponent(this.id)}`);
325
+ }
326
+ /** Answer the open attention request: `'done'` after handling it by hand, or your reply to the agent. */
327
+ async respond(response = "done") {
328
+ await this.http.request("POST", `/api/runs/${encodeURIComponent(this.id)}/respond`, { response });
329
+ }
330
+ async watch(cb, pollMs) {
331
+ const call = async (fn) => {
332
+ try {
333
+ await fn();
334
+ } catch (err) {
335
+ console.error("[oya] run callback threw:", err);
336
+ }
337
+ };
338
+ let seen;
339
+ for (let errors = 0; ; ) {
340
+ let run;
341
+ try {
342
+ run = await this.status();
343
+ errors = 0;
344
+ } catch (err) {
345
+ if (++errors < 5) {
346
+ await sleep(pollMs);
347
+ continue;
348
+ }
349
+ const failure = err instanceof OyaError ? err : new OyaError(String(err), 0, null);
350
+ await call(() => cb.onFailure?.(failure));
351
+ throw failure;
352
+ }
353
+ if (run.status === "needs_attention" && run.attention && run.attention.id !== seen) {
354
+ seen = run.attention.id;
355
+ const request = {
356
+ ...run.attention,
357
+ liveViewUrl: run.attention.liveViewUrl && new URL(run.attention.liveViewUrl, this.http.baseUrl).href,
358
+ respond: (response) => this.respond(response)
359
+ };
360
+ void call(() => cb.onHumanAttention?.(request));
361
+ }
362
+ if (run.status === "succeeded") {
363
+ const result = run.result || {};
364
+ if (result.healed) await call(() => cb.onHealed?.(result));
365
+ await call(() => cb.onSuccess?.(result));
366
+ return result;
367
+ }
368
+ if (run.status === "failed") {
369
+ const failure = new OyaError(run.error || "Run failed", 500, run);
370
+ await call(() => cb.onFailure?.(failure));
371
+ throw failure;
372
+ }
373
+ await sleep(pollMs);
374
+ }
375
+ }
376
+ };
269
377
 
270
378
  // src/index.ts
271
379
  var DEFAULT_BASE_URL = "https://browser.getoya.ai";
@@ -342,6 +450,16 @@ var Oya = class {
342
450
  removeWebhook: (id) => this.http.request("DELETE", `/api/control/webhooks/${encodeURIComponent(id)}`),
343
451
  replayDelivery: (id) => this.http.request("POST", `/api/control/deliveries/${encodeURIComponent(id)}/replay`, {})
344
452
  };
453
+ /** Playbooks saved with `browser.toPlaybook()`. */
454
+ playbooks = {
455
+ list: async () => (await this.http.request("GET", "/api/playbooks")).playbooks,
456
+ /** Delete a playbook and its draft, or only the draft with `'<name>:draft'`. */
457
+ remove: async (name) => {
458
+ await this.http.request("DELETE", `/api/playbooks/${encodeURIComponent(name)}`);
459
+ },
460
+ /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
461
+ promote: (name) => this.http.request("POST", `/api/playbooks/${encodeURIComponent(name)}/promote`, {})
462
+ };
345
463
  /**
346
464
  * Proxy exits for your personas. A persona takes one at first connect (by its
347
465
  * geo hint) or by `personas.pinProxy`, and keeps it.
@@ -413,5 +531,6 @@ var index_default = Oya;
413
531
  0 && (module.exports = {
414
532
  Browser,
415
533
  Oya,
416
- OyaError
534
+ OyaError,
535
+ Run
417
536
  });
package/dist/index.d.cts CHANGED
@@ -54,6 +54,74 @@ interface StartResult {
54
54
  cdpUrl?: string;
55
55
  note?: string;
56
56
  }
57
+ interface Playbook {
58
+ name: string;
59
+ /** Inputs `play()` accepts; any left out reuse the recorded value. */
60
+ variables: string[];
61
+ steps: number;
62
+ /** The same flow as a Playwright module: `export default async function run(page, vars)`. */
63
+ code: string;
64
+ }
65
+ interface PlayResult {
66
+ /** Steps replayed before finishing or handing over to the agent. */
67
+ steps: number;
68
+ total: number;
69
+ /** A step no longer fit the page and the agent finished the task. */
70
+ fellBack: boolean;
71
+ /** The agent's fix was saved as `draft`; promote it with `oya.playbooks.promote(name)`. */
72
+ healed?: boolean;
73
+ draft?: string;
74
+ /** The agent's summary, when it fell back. */
75
+ text?: string;
76
+ }
77
+ interface PlaybookSummary extends Playbook {
78
+ createdAt: string | null;
79
+ promotedAt: string | null;
80
+ /** A healed replay's fix, waiting for `promote()` or `remove('<name>:draft')`. */
81
+ draft: (Playbook & {
82
+ healedAt: string;
83
+ healedFrom: number;
84
+ }) | null;
85
+ }
86
+ /** Values passed through to the page as `{{name}}` placeholders; the model never sees them. */
87
+ type RunData = Record<string, string | number>;
88
+ interface AttentionRequest {
89
+ id: string;
90
+ /** captcha / mfa: finish it in the live view. agent: the agent's question. heal_failed: replay and the agent both gave up. */
91
+ reason: 'captcha' | 'mfa' | 'agent' | 'heal_failed';
92
+ message: string;
93
+ liveViewUrl?: string;
94
+ at: number;
95
+ }
96
+ type RunResult = Partial<PlayResult> & {
97
+ text?: string;
98
+ };
99
+ interface RunInfo {
100
+ id: string;
101
+ browserId: string;
102
+ status: 'running' | 'needs_attention' | 'succeeded' | 'failed';
103
+ createdAt: number;
104
+ endedAt?: number;
105
+ attention: AttentionRequest | null;
106
+ result?: RunResult;
107
+ error?: string;
108
+ }
109
+ interface SubmitOptions {
110
+ /** Passed through as `{{name}}` placeholders; for a playbook, its variables. */
111
+ data?: RunData;
112
+ /** Playbooks only: let the agent finish a broken replay and save its fix as a draft. Default true. */
113
+ autoHeal?: boolean;
114
+ onSuccess?: (result: RunResult) => unknown;
115
+ onFailure?: (error: OyaError) => unknown;
116
+ /** Call `respond()` once it is handled: `'done'` after finishing by hand, or your answer to the agent. */
117
+ onHumanAttention?: (request: AttentionRequest & {
118
+ respond(response?: string): Promise<void>;
119
+ }) => unknown;
120
+ /** Fires before onSuccess when a replay was healed; `result.draft` names the draft. */
121
+ onHealed?: (result: RunResult) => unknown;
122
+ /** How often to check on the run. Default 2000. */
123
+ pollMs?: number;
124
+ }
57
125
  interface Element {
58
126
  id: number;
59
127
  type: string;
@@ -357,8 +425,41 @@ declare class Browser {
357
425
  * answer it, `liveViewUrl` is where a person finishes by hand.
358
426
  */
359
427
  completeMfa(): Promise<MfaResult>;
360
- /** Natural-language control, using this key's configured model. */
361
- ask(prompt: string): Promise<string>;
428
+ /**
429
+ * Natural-language control, using this key's configured model. Refer to `data`
430
+ * as `{{name}}` in the prompt: the agent types the placeholder, the page gets the
431
+ * value, and the model never sees it.
432
+ */
433
+ ask(prompt: string, { data }?: {
434
+ data?: RunData;
435
+ }): Promise<string>;
436
+ /**
437
+ * Save the last `ask()` on this browser as a named playbook. Values that came
438
+ * from the prompt (names, IDs, dates) become variables; `code` is the same
439
+ * flow as a Playwright module, to read or run yourself.
440
+ */
441
+ toPlaybook(name: string): Promise<Playbook>;
442
+ /**
443
+ * Replay a playbook with no LLM in the loop. Variables left out reuse the
444
+ * recorded values where there are any. If a step no longer fits the page and
445
+ * `autoHeal` is on (the default), the agent finishes the task and its fix is
446
+ * saved as a draft (`healed`, `draft`); off, the step's error is thrown.
447
+ * Play `'<name>:draft'` to try a draft before promoting it.
448
+ */
449
+ play(name: string, data?: RunData, { autoHeal }?: {
450
+ autoHeal?: boolean;
451
+ }): Promise<PlayResult>;
452
+ /**
453
+ * Start a prompt or playbook in the background and hear back through callbacks.
454
+ * `onHumanAttention` fires for an unsolved CAPTCHA, an unfinished MFA, the agent
455
+ * asking for help, or a replay the agent could not heal; the run waits (up to 30
456
+ * minutes) until you call `respond()`.
457
+ */
458
+ submit(task: {
459
+ prompt: string;
460
+ } | {
461
+ playbook: string;
462
+ }, options?: SubmitOptions): Promise<Run>;
362
463
  /**
363
464
  * Watch it work: the console, opened on this browser.
364
465
  *
@@ -411,6 +512,18 @@ declare class Browser {
411
512
  /** @deprecated use stop() — close() only dropped the socket, and a cloud browser redialled. */
412
513
  close(): Promise<void>;
413
514
  }
515
+ type RunCallbacks = Pick<SubmitOptions, 'onSuccess' | 'onFailure' | 'onHumanAttention' | 'onHealed'>;
516
+ /** A submitted task. Callbacks fire as it changes; `done` settles when it ends. */
517
+ declare class Run {
518
+ private readonly http;
519
+ readonly id: string;
520
+ readonly done: Promise<RunResult>;
521
+ constructor(http: Http, id: string, callbacks: RunCallbacks, pollMs?: number);
522
+ status(): Promise<RunInfo>;
523
+ /** Answer the open attention request: `'done'` after handling it by hand, or your reply to the agent. */
524
+ respond(response?: string): Promise<void>;
525
+ private watch;
526
+ }
414
527
 
415
528
  /**
416
529
  * @oya-ai/browser — thousands of browsers, one API.
@@ -496,6 +609,14 @@ declare class Oya {
496
609
  ok: boolean;
497
610
  }>;
498
611
  };
612
+ /** Playbooks saved with `browser.toPlaybook()`. */
613
+ readonly playbooks: {
614
+ list: () => Promise<PlaybookSummary[]>;
615
+ /** Delete a playbook and its draft, or only the draft with `'<name>:draft'`. */
616
+ remove: (name: string) => Promise<void>;
617
+ /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
618
+ promote: (name: string) => Promise<Playbook>;
619
+ };
499
620
  /**
500
621
  * Proxy exits for your personas. A persona takes one at first connect (by its
501
622
  * geo hint) or by `personas.pinProxy`, and keeps it.
@@ -624,4 +745,4 @@ declare class Oya {
624
745
  private waitUntilConnected;
625
746
  }
626
747
 
627
- export { type Activity, type Analysis, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Element, type Fingerprint, type Health, type HumanInputAction, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PersonaInfo, type PersonaPrefs, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type StartOptions, type StartResult, type StopResult, Oya as default };
748
+ export { type Activity, type Analysis, type AttentionRequest, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Element, type Fingerprint, type Health, type HumanInputAction, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, Run, type RunData, type RunInfo, type RunResult, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default };
package/dist/index.d.ts CHANGED
@@ -54,6 +54,74 @@ interface StartResult {
54
54
  cdpUrl?: string;
55
55
  note?: string;
56
56
  }
57
+ interface Playbook {
58
+ name: string;
59
+ /** Inputs `play()` accepts; any left out reuse the recorded value. */
60
+ variables: string[];
61
+ steps: number;
62
+ /** The same flow as a Playwright module: `export default async function run(page, vars)`. */
63
+ code: string;
64
+ }
65
+ interface PlayResult {
66
+ /** Steps replayed before finishing or handing over to the agent. */
67
+ steps: number;
68
+ total: number;
69
+ /** A step no longer fit the page and the agent finished the task. */
70
+ fellBack: boolean;
71
+ /** The agent's fix was saved as `draft`; promote it with `oya.playbooks.promote(name)`. */
72
+ healed?: boolean;
73
+ draft?: string;
74
+ /** The agent's summary, when it fell back. */
75
+ text?: string;
76
+ }
77
+ interface PlaybookSummary extends Playbook {
78
+ createdAt: string | null;
79
+ promotedAt: string | null;
80
+ /** A healed replay's fix, waiting for `promote()` or `remove('<name>:draft')`. */
81
+ draft: (Playbook & {
82
+ healedAt: string;
83
+ healedFrom: number;
84
+ }) | null;
85
+ }
86
+ /** Values passed through to the page as `{{name}}` placeholders; the model never sees them. */
87
+ type RunData = Record<string, string | number>;
88
+ interface AttentionRequest {
89
+ id: string;
90
+ /** captcha / mfa: finish it in the live view. agent: the agent's question. heal_failed: replay and the agent both gave up. */
91
+ reason: 'captcha' | 'mfa' | 'agent' | 'heal_failed';
92
+ message: string;
93
+ liveViewUrl?: string;
94
+ at: number;
95
+ }
96
+ type RunResult = Partial<PlayResult> & {
97
+ text?: string;
98
+ };
99
+ interface RunInfo {
100
+ id: string;
101
+ browserId: string;
102
+ status: 'running' | 'needs_attention' | 'succeeded' | 'failed';
103
+ createdAt: number;
104
+ endedAt?: number;
105
+ attention: AttentionRequest | null;
106
+ result?: RunResult;
107
+ error?: string;
108
+ }
109
+ interface SubmitOptions {
110
+ /** Passed through as `{{name}}` placeholders; for a playbook, its variables. */
111
+ data?: RunData;
112
+ /** Playbooks only: let the agent finish a broken replay and save its fix as a draft. Default true. */
113
+ autoHeal?: boolean;
114
+ onSuccess?: (result: RunResult) => unknown;
115
+ onFailure?: (error: OyaError) => unknown;
116
+ /** Call `respond()` once it is handled: `'done'` after finishing by hand, or your answer to the agent. */
117
+ onHumanAttention?: (request: AttentionRequest & {
118
+ respond(response?: string): Promise<void>;
119
+ }) => unknown;
120
+ /** Fires before onSuccess when a replay was healed; `result.draft` names the draft. */
121
+ onHealed?: (result: RunResult) => unknown;
122
+ /** How often to check on the run. Default 2000. */
123
+ pollMs?: number;
124
+ }
57
125
  interface Element {
58
126
  id: number;
59
127
  type: string;
@@ -357,8 +425,41 @@ declare class Browser {
357
425
  * answer it, `liveViewUrl` is where a person finishes by hand.
358
426
  */
359
427
  completeMfa(): Promise<MfaResult>;
360
- /** Natural-language control, using this key's configured model. */
361
- ask(prompt: string): Promise<string>;
428
+ /**
429
+ * Natural-language control, using this key's configured model. Refer to `data`
430
+ * as `{{name}}` in the prompt: the agent types the placeholder, the page gets the
431
+ * value, and the model never sees it.
432
+ */
433
+ ask(prompt: string, { data }?: {
434
+ data?: RunData;
435
+ }): Promise<string>;
436
+ /**
437
+ * Save the last `ask()` on this browser as a named playbook. Values that came
438
+ * from the prompt (names, IDs, dates) become variables; `code` is the same
439
+ * flow as a Playwright module, to read or run yourself.
440
+ */
441
+ toPlaybook(name: string): Promise<Playbook>;
442
+ /**
443
+ * Replay a playbook with no LLM in the loop. Variables left out reuse the
444
+ * recorded values where there are any. If a step no longer fits the page and
445
+ * `autoHeal` is on (the default), the agent finishes the task and its fix is
446
+ * saved as a draft (`healed`, `draft`); off, the step's error is thrown.
447
+ * Play `'<name>:draft'` to try a draft before promoting it.
448
+ */
449
+ play(name: string, data?: RunData, { autoHeal }?: {
450
+ autoHeal?: boolean;
451
+ }): Promise<PlayResult>;
452
+ /**
453
+ * Start a prompt or playbook in the background and hear back through callbacks.
454
+ * `onHumanAttention` fires for an unsolved CAPTCHA, an unfinished MFA, the agent
455
+ * asking for help, or a replay the agent could not heal; the run waits (up to 30
456
+ * minutes) until you call `respond()`.
457
+ */
458
+ submit(task: {
459
+ prompt: string;
460
+ } | {
461
+ playbook: string;
462
+ }, options?: SubmitOptions): Promise<Run>;
362
463
  /**
363
464
  * Watch it work: the console, opened on this browser.
364
465
  *
@@ -411,6 +512,18 @@ declare class Browser {
411
512
  /** @deprecated use stop() — close() only dropped the socket, and a cloud browser redialled. */
412
513
  close(): Promise<void>;
413
514
  }
515
+ type RunCallbacks = Pick<SubmitOptions, 'onSuccess' | 'onFailure' | 'onHumanAttention' | 'onHealed'>;
516
+ /** A submitted task. Callbacks fire as it changes; `done` settles when it ends. */
517
+ declare class Run {
518
+ private readonly http;
519
+ readonly id: string;
520
+ readonly done: Promise<RunResult>;
521
+ constructor(http: Http, id: string, callbacks: RunCallbacks, pollMs?: number);
522
+ status(): Promise<RunInfo>;
523
+ /** Answer the open attention request: `'done'` after handling it by hand, or your reply to the agent. */
524
+ respond(response?: string): Promise<void>;
525
+ private watch;
526
+ }
414
527
 
415
528
  /**
416
529
  * @oya-ai/browser — thousands of browsers, one API.
@@ -496,6 +609,14 @@ declare class Oya {
496
609
  ok: boolean;
497
610
  }>;
498
611
  };
612
+ /** Playbooks saved with `browser.toPlaybook()`. */
613
+ readonly playbooks: {
614
+ list: () => Promise<PlaybookSummary[]>;
615
+ /** Delete a playbook and its draft, or only the draft with `'<name>:draft'`. */
616
+ remove: (name: string) => Promise<void>;
617
+ /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
618
+ promote: (name: string) => Promise<Playbook>;
619
+ };
499
620
  /**
500
621
  * Proxy exits for your personas. A persona takes one at first connect (by its
501
622
  * geo hint) or by `personas.pinProxy`, and keeps it.
@@ -624,4 +745,4 @@ declare class Oya {
624
745
  private waitUntilConnected;
625
746
  }
626
747
 
627
- export { type Activity, type Analysis, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Element, type Fingerprint, type Health, type HumanInputAction, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PersonaInfo, type PersonaPrefs, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type StartOptions, type StartResult, type StopResult, Oya as default };
748
+ export { type Activity, type Analysis, type AttentionRequest, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Element, type Fingerprint, type Health, type HumanInputAction, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, Run, type RunData, type RunInfo, type RunResult, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default };
package/dist/index.js CHANGED
@@ -156,16 +156,57 @@ var Browser = class {
156
156
  if (result.liveViewUrl) result.liveViewUrl = new URL(result.liveViewUrl, this.http.baseUrl).href;
157
157
  return result;
158
158
  }
159
- /** Natural-language control, using this key's configured model. */
160
- async ask(prompt) {
159
+ /**
160
+ * Natural-language control, using this key's configured model. Refer to `data`
161
+ * as `{{name}}` in the prompt: the agent types the placeholder, the page gets the
162
+ * value, and the model never sees it.
163
+ */
164
+ async ask(prompt, { data } = {}) {
161
165
  const res = await this.http.request(
162
166
  "POST",
163
167
  `/api/browsers/${this.id}/chat`,
164
- { messages: [{ role: "user", content: prompt }] },
168
+ { messages: [{ role: "user", content: prompt }], data },
165
169
  6e5
166
170
  );
171
+ if (res.error) throw new OyaError(res.error, 500, res);
167
172
  return res.text;
168
173
  }
174
+ /**
175
+ * Save the last `ask()` on this browser as a named playbook. Values that came
176
+ * from the prompt (names, IDs, dates) become variables; `code` is the same
177
+ * flow as a Playwright module, to read or run yourself.
178
+ */
179
+ toPlaybook(name) {
180
+ return this.http.request("POST", `/api/browsers/${this.id}/playbooks`, { name }, 12e4);
181
+ }
182
+ /**
183
+ * Replay a playbook with no LLM in the loop. Variables left out reuse the
184
+ * recorded values where there are any. If a step no longer fits the page and
185
+ * `autoHeal` is on (the default), the agent finishes the task and its fix is
186
+ * saved as a draft (`healed`, `draft`); off, the step's error is thrown.
187
+ * Play `'<name>:draft'` to try a draft before promoting it.
188
+ */
189
+ async play(name, data = {}, { autoHeal = true } = {}) {
190
+ const res = await this.http.request(
191
+ "POST",
192
+ `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`,
193
+ { variables: data, autoHeal },
194
+ 6e5
195
+ );
196
+ if (res.error) throw new OyaError(res.error, 500, res);
197
+ return res;
198
+ }
199
+ /**
200
+ * Start a prompt or playbook in the background and hear back through callbacks.
201
+ * `onHumanAttention` fires for an unsolved CAPTCHA, an unfinished MFA, the agent
202
+ * asking for help, or a replay the agent could not heal; the run waits (up to 30
203
+ * minutes) until you call `respond()`.
204
+ */
205
+ async submit(task, options = {}) {
206
+ const { onSuccess, onFailure, onHumanAttention, onHealed, pollMs, ...body } = options;
207
+ const started = await this.http.request("POST", `/api/browsers/${this.id}/runs`, { ...task, ...body });
208
+ return new Run(this.http, started.id, { onSuccess, onFailure, onHumanAttention, onHealed }, pollMs);
209
+ }
169
210
  /**
170
211
  * Watch it work: the console, opened on this browser.
171
212
  *
@@ -237,6 +278,72 @@ var Browser = class {
237
278
  await this.stop();
238
279
  }
239
280
  };
281
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
282
+ var Run = class {
283
+ constructor(http, id, callbacks, pollMs = 2e3) {
284
+ this.http = http;
285
+ this.id = id;
286
+ this.done = this.watch(callbacks, pollMs);
287
+ this.done.catch(() => {
288
+ });
289
+ }
290
+ http;
291
+ id;
292
+ done;
293
+ status() {
294
+ return this.http.request("GET", `/api/runs/${encodeURIComponent(this.id)}`);
295
+ }
296
+ /** Answer the open attention request: `'done'` after handling it by hand, or your reply to the agent. */
297
+ async respond(response = "done") {
298
+ await this.http.request("POST", `/api/runs/${encodeURIComponent(this.id)}/respond`, { response });
299
+ }
300
+ async watch(cb, pollMs) {
301
+ const call = async (fn) => {
302
+ try {
303
+ await fn();
304
+ } catch (err) {
305
+ console.error("[oya] run callback threw:", err);
306
+ }
307
+ };
308
+ let seen;
309
+ for (let errors = 0; ; ) {
310
+ let run;
311
+ try {
312
+ run = await this.status();
313
+ errors = 0;
314
+ } catch (err) {
315
+ if (++errors < 5) {
316
+ await sleep(pollMs);
317
+ continue;
318
+ }
319
+ const failure = err instanceof OyaError ? err : new OyaError(String(err), 0, null);
320
+ await call(() => cb.onFailure?.(failure));
321
+ throw failure;
322
+ }
323
+ if (run.status === "needs_attention" && run.attention && run.attention.id !== seen) {
324
+ seen = run.attention.id;
325
+ const request = {
326
+ ...run.attention,
327
+ liveViewUrl: run.attention.liveViewUrl && new URL(run.attention.liveViewUrl, this.http.baseUrl).href,
328
+ respond: (response) => this.respond(response)
329
+ };
330
+ void call(() => cb.onHumanAttention?.(request));
331
+ }
332
+ if (run.status === "succeeded") {
333
+ const result = run.result || {};
334
+ if (result.healed) await call(() => cb.onHealed?.(result));
335
+ await call(() => cb.onSuccess?.(result));
336
+ return result;
337
+ }
338
+ if (run.status === "failed") {
339
+ const failure = new OyaError(run.error || "Run failed", 500, run);
340
+ await call(() => cb.onFailure?.(failure));
341
+ throw failure;
342
+ }
343
+ await sleep(pollMs);
344
+ }
345
+ }
346
+ };
240
347
 
241
348
  // src/index.ts
242
349
  var DEFAULT_BASE_URL = "https://browser.getoya.ai";
@@ -313,6 +420,16 @@ var Oya = class {
313
420
  removeWebhook: (id) => this.http.request("DELETE", `/api/control/webhooks/${encodeURIComponent(id)}`),
314
421
  replayDelivery: (id) => this.http.request("POST", `/api/control/deliveries/${encodeURIComponent(id)}/replay`, {})
315
422
  };
423
+ /** Playbooks saved with `browser.toPlaybook()`. */
424
+ playbooks = {
425
+ list: async () => (await this.http.request("GET", "/api/playbooks")).playbooks,
426
+ /** Delete a playbook and its draft, or only the draft with `'<name>:draft'`. */
427
+ remove: async (name) => {
428
+ await this.http.request("DELETE", `/api/playbooks/${encodeURIComponent(name)}`);
429
+ },
430
+ /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
431
+ promote: (name) => this.http.request("POST", `/api/playbooks/${encodeURIComponent(name)}/promote`, {})
432
+ };
316
433
  /**
317
434
  * Proxy exits for your personas. A persona takes one at first connect (by its
318
435
  * geo hint) or by `personas.pinProxy`, and keeps it.
@@ -384,5 +501,6 @@ export {
384
501
  Browser,
385
502
  Oya,
386
503
  OyaError,
504
+ Run,
387
505
  index_default as default
388
506
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oya-ai/browser",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
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",