@oya-ai/cli 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.
Files changed (2) hide show
  1. package/dist/index.js +120 -3
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -157,16 +157,57 @@ var Browser = class {
157
157
  if (result.liveViewUrl) result.liveViewUrl = new URL(result.liveViewUrl, this.http.baseUrl).href;
158
158
  return result;
159
159
  }
160
- /** Natural-language control, using this key's configured model. */
161
- async ask(prompt) {
160
+ /**
161
+ * Natural-language control, using this key's configured model. Refer to `data`
162
+ * as `{{name}}` in the prompt: the agent types the placeholder, the page gets the
163
+ * value, and the model never sees it.
164
+ */
165
+ async ask(prompt, { data } = {}) {
162
166
  const res = await this.http.request(
163
167
  "POST",
164
168
  `/api/browsers/${this.id}/chat`,
165
- { messages: [{ role: "user", content: prompt }] },
169
+ { messages: [{ role: "user", content: prompt }], data },
166
170
  6e5
167
171
  );
172
+ if (res.error) throw new OyaError(res.error, 500, res);
168
173
  return res.text;
169
174
  }
175
+ /**
176
+ * Save the last `ask()` on this browser as a named playbook. Values that came
177
+ * from the prompt (names, IDs, dates) become variables; `code` is the same
178
+ * flow as a Playwright module, to read or run yourself.
179
+ */
180
+ toPlaybook(name) {
181
+ return this.http.request("POST", `/api/browsers/${this.id}/playbooks`, { name }, 12e4);
182
+ }
183
+ /**
184
+ * Replay a playbook with no LLM in the loop. Variables left out reuse the
185
+ * recorded values where there are any. If a step no longer fits the page and
186
+ * `autoHeal` is on (the default), the agent finishes the task and its fix is
187
+ * saved as a draft (`healed`, `draft`); off, the step's error is thrown.
188
+ * Play `'<name>:draft'` to try a draft before promoting it.
189
+ */
190
+ async play(name, data = {}, { autoHeal = true } = {}) {
191
+ const res = await this.http.request(
192
+ "POST",
193
+ `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`,
194
+ { variables: data, autoHeal },
195
+ 6e5
196
+ );
197
+ if (res.error) throw new OyaError(res.error, 500, res);
198
+ return res;
199
+ }
200
+ /**
201
+ * Start a prompt or playbook in the background and hear back through callbacks.
202
+ * `onHumanAttention` fires for an unsolved CAPTCHA, an unfinished MFA, the agent
203
+ * asking for help, or a replay the agent could not heal; the run waits (up to 30
204
+ * minutes) until you call `respond()`.
205
+ */
206
+ async submit(task, options = {}) {
207
+ const { onSuccess, onFailure, onHumanAttention, onHealed, pollMs, ...body } = options;
208
+ const started = await this.http.request("POST", `/api/browsers/${this.id}/runs`, { ...task, ...body });
209
+ return new Run(this.http, started.id, { onSuccess, onFailure, onHumanAttention, onHealed }, pollMs);
210
+ }
170
211
  /**
171
212
  * Watch it work: the console, opened on this browser.
172
213
  *
@@ -238,6 +279,72 @@ var Browser = class {
238
279
  await this.stop();
239
280
  }
240
281
  };
282
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
283
+ var Run = class {
284
+ constructor(http, id, callbacks, pollMs = 2e3) {
285
+ this.http = http;
286
+ this.id = id;
287
+ this.done = this.watch(callbacks, pollMs);
288
+ this.done.catch(() => {
289
+ });
290
+ }
291
+ http;
292
+ id;
293
+ done;
294
+ status() {
295
+ return this.http.request("GET", `/api/runs/${encodeURIComponent(this.id)}`);
296
+ }
297
+ /** Answer the open attention request: `'done'` after handling it by hand, or your reply to the agent. */
298
+ async respond(response = "done") {
299
+ await this.http.request("POST", `/api/runs/${encodeURIComponent(this.id)}/respond`, { response });
300
+ }
301
+ async watch(cb, pollMs) {
302
+ const call = async (fn) => {
303
+ try {
304
+ await fn();
305
+ } catch (err) {
306
+ console.error("[oya] run callback threw:", err);
307
+ }
308
+ };
309
+ let seen;
310
+ for (let errors = 0; ; ) {
311
+ let run2;
312
+ try {
313
+ run2 = await this.status();
314
+ errors = 0;
315
+ } catch (err) {
316
+ if (++errors < 5) {
317
+ await sleep(pollMs);
318
+ continue;
319
+ }
320
+ const failure = err instanceof OyaError ? err : new OyaError(String(err), 0, null);
321
+ await call(() => cb.onFailure?.(failure));
322
+ throw failure;
323
+ }
324
+ if (run2.status === "needs_attention" && run2.attention && run2.attention.id !== seen) {
325
+ seen = run2.attention.id;
326
+ const request = {
327
+ ...run2.attention,
328
+ liveViewUrl: run2.attention.liveViewUrl && new URL(run2.attention.liveViewUrl, this.http.baseUrl).href,
329
+ respond: (response) => this.respond(response)
330
+ };
331
+ void call(() => cb.onHumanAttention?.(request));
332
+ }
333
+ if (run2.status === "succeeded") {
334
+ const result = run2.result || {};
335
+ if (result.healed) await call(() => cb.onHealed?.(result));
336
+ await call(() => cb.onSuccess?.(result));
337
+ return result;
338
+ }
339
+ if (run2.status === "failed") {
340
+ const failure = new OyaError(run2.error || "Run failed", 500, run2);
341
+ await call(() => cb.onFailure?.(failure));
342
+ throw failure;
343
+ }
344
+ await sleep(pollMs);
345
+ }
346
+ }
347
+ };
241
348
  var DEFAULT_BASE_URL = "https://browser.getoya.ai";
242
349
  var READY_POLL_MS = 2e3;
243
350
  var env = (name) => globalThis.process?.env?.[name];
@@ -312,6 +419,16 @@ var Oya = class {
312
419
  removeWebhook: (id) => this.http.request("DELETE", `/api/control/webhooks/${encodeURIComponent(id)}`),
313
420
  replayDelivery: (id) => this.http.request("POST", `/api/control/deliveries/${encodeURIComponent(id)}/replay`, {})
314
421
  };
422
+ /** Playbooks saved with `browser.toPlaybook()`. */
423
+ playbooks = {
424
+ list: async () => (await this.http.request("GET", "/api/playbooks")).playbooks,
425
+ /** Delete a playbook and its draft, or only the draft with `'<name>:draft'`. */
426
+ remove: async (name) => {
427
+ await this.http.request("DELETE", `/api/playbooks/${encodeURIComponent(name)}`);
428
+ },
429
+ /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
430
+ promote: (name) => this.http.request("POST", `/api/playbooks/${encodeURIComponent(name)}/promote`, {})
431
+ };
315
432
  /**
316
433
  * Proxy exits for your personas. A persona takes one at first connect (by its
317
434
  * geo hint) or by `personas.pinProxy`, and keeps it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oya-ai/cli",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "Command line for the Oya browser control plane.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://browser.getoya.ai",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "devDependencies": {
42
- "@oya-ai/browser": "1.0.73",
42
+ "@oya-ai/browser": "1.0.75",
43
43
  "@types/node": "^20.19.43",
44
44
  "tsup": "^8.0.0",
45
45
  "typescript": "^5.4.0"