@oya-ai/browser 1.0.110 → 1.0.113
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 +49 -33
- package/dist/index.cjs +206 -105
- package/dist/index.d.cts +28 -8
- package/dist/index.d.ts +28 -8
- package/dist/index.js +206 -105
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,6 +166,21 @@ await browser.ask('Attach my resume to the application and submit it', {
|
|
|
166
166
|
|
|
167
167
|
A string argument is a path on disk (Node only); a `Blob`, a `File`, or a `Uint8Array` works anywhere. `name` sets the filename the site sees and `type` overrides the MIME guessed from the extension. The ceiling is 10MB per file, and the bytes travel inline with the run. Nothing is stored server-side after it ends.
|
|
168
168
|
|
|
169
|
+
### Data instead of a sentence
|
|
170
|
+
|
|
171
|
+
`extract()` runs the same agent and answers in the shape you ask for, so you do not
|
|
172
|
+
parse prose:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const { title, price } = await browser.extract('What is the product on this page?', {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: { title: { type: 'string' }, price: { type: 'string' } },
|
|
178
|
+
required: ['title', 'price'],
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
It throws when the agent reports it could not do the task.
|
|
183
|
+
|
|
169
184
|
Files work in `data` for `ask()`, `submit()`, and `play()`; `secrets` rejects them, because a file is never typed through a placeholder. A run recorded with `toPlaybook()` keeps the upload as a variable, so the replay takes a different file:
|
|
170
185
|
|
|
171
186
|
```js
|
|
@@ -310,7 +325,7 @@ await browser.goto('https://github.com/trending');
|
|
|
310
325
|
|
|
311
326
|
// Analyze page: returns markdown and visible numbered elements
|
|
312
327
|
const { markdown, elements } = await browser.analyze();
|
|
313
|
-
console.log(markdown.slice(0, 300));
|
|
328
|
+
console.log((markdown ?? '').slice(0, 300));
|
|
314
329
|
|
|
315
330
|
// Interact using numbered element IDs:
|
|
316
331
|
const firstRepo = elements.find((el) => el.tag === 'a' && el.href?.includes('/stargazers'));
|
|
@@ -422,34 +437,35 @@ const oya = new Oya({
|
|
|
422
437
|
|
|
423
438
|
### Browser Instance Methods (`browser.*`)
|
|
424
439
|
|
|
425
|
-
| Method
|
|
426
|
-
|
|
|
427
|
-
| `goto(url)`
|
|
428
|
-
| `ask(prompt, { data?, secrets? }?)`
|
|
429
|
-
| `
|
|
430
|
-
| `
|
|
431
|
-
| `
|
|
432
|
-
| `
|
|
433
|
-
| `
|
|
434
|
-
| `
|
|
435
|
-
| `
|
|
436
|
-
| `
|
|
437
|
-
| `
|
|
438
|
-
| `
|
|
439
|
-
| `
|
|
440
|
-
| `
|
|
441
|
-
| `
|
|
442
|
-
| `
|
|
443
|
-
| `
|
|
444
|
-
| `
|
|
445
|
-
| `
|
|
446
|
-
| `
|
|
447
|
-
| `
|
|
448
|
-
| `
|
|
449
|
-
| `
|
|
450
|
-
| `
|
|
451
|
-
| `
|
|
452
|
-
| `
|
|
440
|
+
| Method | Returns | Description |
|
|
441
|
+
| :---------------------------------------------- | :------------------------------------------- | :------------------------------------------------------- |
|
|
442
|
+
| `goto(url)` | `Promise<void>` | Navigate to URL (with optional auto-CAPTCHA) |
|
|
443
|
+
| `ask(prompt, { data?, secrets? }?)` | `Promise<string>` | Natural-language AI driving using key's configured model |
|
|
444
|
+
| `extract(prompt, schema, { data?, secrets? }?)` | `Promise<T>` | The same, answered as data in the shape of a JSON schema |
|
|
445
|
+
| `analyze()` | `Promise<Analysis>` | Returns markdown representation and numbered elements |
|
|
446
|
+
| `elements()` | `Promise<Element[]>` | Returns only visible interactable elements |
|
|
447
|
+
| `click(elementId)` | `Promise<void>` | Click element by numeric ID from `analyze()` |
|
|
448
|
+
| `type(elementId, text)` | `Promise<{ suggestions_visible?: boolean }>` | Type text into specified element |
|
|
449
|
+
| `pressKey(key)` | `Promise<void>` | Dispatch keyboard key event (e.g. `'Enter'`) |
|
|
450
|
+
| `scroll(dir, amount?, at?)` | `Promise<void>` | Scroll `'up' \| 'down' \| 'top' \| 'bottom'` |
|
|
451
|
+
| `waitFor(selector, timeout?)` | `Promise<void>` | Wait for DOM selector |
|
|
452
|
+
| `screenshot()` | `Promise<string>` | Capture page as base64 image data URL |
|
|
453
|
+
| `url()` | `Promise<string>` | Current active tab URL |
|
|
454
|
+
| `tabs()` | `Promise<Tab[]>` | List open tabs |
|
|
455
|
+
| `openTab(url?)` | `Promise<string>` | Open a new tab |
|
|
456
|
+
| `switchTab(tabId)` | `Promise<void>` | Switch active tab |
|
|
457
|
+
| `closeTab(tabId)` | `Promise<void>` | Close target tab |
|
|
458
|
+
| `solveCaptcha()` | `Promise<CaptchaResult>` | Detect and solve on-screen CAPTCHA |
|
|
459
|
+
| `completeMfa()` | `Promise<MfaResult>` | Resolve TOTP/SMS MFA or return `liveViewUrl` |
|
|
460
|
+
| `liveViewUrl()` | `string` | Dashboard link for this browser |
|
|
461
|
+
| `liveStreamUrl()` | `Promise<string>` | SSE frame stream URL with a single-use ticket |
|
|
462
|
+
| `shareUrl(options?)` | `Promise<{ url, id, expiresAt }>` | Expiring browser share link; optional control access |
|
|
463
|
+
| `revokeShare(id)` | `Promise<void>` | Revoke a share link |
|
|
464
|
+
| `submit(task, options?)` | `Promise<Run>` | Background prompt or playbook with callbacks |
|
|
465
|
+
| `toPlaybook(name)` | `Promise<Playbook>` | Save the latest agent flow and export Playwright code |
|
|
466
|
+
| `play(name, data?, { autoHeal? }?)` | `Promise<PlayResult>` | Replay a saved flow |
|
|
467
|
+
| `status()` | `Promise<BrowserDetail>` | Instance metrics, health, and recent activity log |
|
|
468
|
+
| `stop()` | `Promise<StopResult>` | Tear down sandbox and release CDP session |
|
|
453
469
|
|
|
454
470
|
### Profile and persona management (`oya.profiles`, `oya.personas`)
|
|
455
471
|
|
|
@@ -468,10 +484,10 @@ const oya = new Oya({
|
|
|
468
484
|
| `remove(id)` | Delete persona and associated cookie jar |
|
|
469
485
|
| `setMfa(id, config)` | Store TOTP secret (sealed at rest with AES-256-GCM) |
|
|
470
486
|
| `clearMfa(id)` | Remove MFA secret from persona |
|
|
471
|
-
| `cookies(id, format?)`
|
|
472
|
-
| `importCookies(id, cookies)`
|
|
473
|
-
| `copyCookies(from, to)`
|
|
474
|
-
| `clearCookies(id)`
|
|
487
|
+
| `cookies(id, format?)` | Export the persona's logins; `'playwright'` fits `addCookies()` |
|
|
488
|
+
| `importCookies(id, cookies)` | Merge cookies into the jar (from a file, a script, anywhere) |
|
|
489
|
+
| `copyCookies(from, to)` | Copy one persona's logins into another |
|
|
490
|
+
| `clearCookies(id)` | Forget every cookie: signs the persona out everywhere |
|
|
475
491
|
|
|
476
492
|
### Proxies (`oya.proxies`)
|
|
477
493
|
|
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,47 @@ __export(index_exports, {
|
|
|
30
30
|
});
|
|
31
31
|
module.exports = __toCommonJS(index_exports);
|
|
32
32
|
|
|
33
|
+
// src/constants.ts
|
|
34
|
+
var DEFAULT_BASE_URL = "https://oyabrowser.com";
|
|
35
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
36
|
+
var START_TIMEOUT_MS = 12e4;
|
|
37
|
+
var READY_TIMEOUT_MS = 12e4;
|
|
38
|
+
var READY_POLL_MS = 2e3;
|
|
39
|
+
var NAVIGATE_TIMEOUT_MS = 12e4;
|
|
40
|
+
var CHALLENGE_TIMEOUT_MS = 18e4;
|
|
41
|
+
var AGENT_TIMEOUT_MS = 6e5;
|
|
42
|
+
var PLAYBOOK_TIMEOUT_MS = 12e4;
|
|
43
|
+
var STOP_TIMEOUT_MS = 6e4;
|
|
44
|
+
var WAIT_FOR_DEFAULT_MS = 3e4;
|
|
45
|
+
var WAIT_FOR_GRACE_MS = 5e3;
|
|
46
|
+
var AIMED_SCROLL_AMOUNT = 500;
|
|
47
|
+
var RUN_POLL_MS = 2e3;
|
|
48
|
+
var MAX_RUN_POLL_ERRORS = 5;
|
|
49
|
+
var BYTES_PER_MB = 1048576;
|
|
50
|
+
var MAX_FILE_MB = 10;
|
|
51
|
+
var BASE64_CHUNK_BYTES = 8192;
|
|
52
|
+
var MS_PER_SECOND = 1e3;
|
|
53
|
+
var LATIN1_MAX = 255;
|
|
54
|
+
var SPACE = 32;
|
|
55
|
+
var TAB = 9;
|
|
56
|
+
var DEL = 127;
|
|
57
|
+
var HEX = 16;
|
|
58
|
+
var CODE_POINT_DIGITS = 4;
|
|
59
|
+
var Status = {
|
|
60
|
+
/** The caller passed something unusable, such as a non-numeric element id. */
|
|
61
|
+
BAD_REQUEST: 400,
|
|
62
|
+
/** The server does not know that session. */
|
|
63
|
+
NOT_FOUND: 404,
|
|
64
|
+
/** The browser is in a state that needs attention first. */
|
|
65
|
+
CONFLICT: 409,
|
|
66
|
+
/** A browser command ran and failed. */
|
|
67
|
+
UNPROCESSABLE: 422,
|
|
68
|
+
/** A run failed without saying why. */
|
|
69
|
+
SERVER_ERROR: 500,
|
|
70
|
+
/** A browser did not come up in time. */
|
|
71
|
+
GATEWAY_TIMEOUT: 504
|
|
72
|
+
};
|
|
73
|
+
|
|
33
74
|
// src/errors.ts
|
|
34
75
|
var OyaError = class extends Error {
|
|
35
76
|
/** The HTTP status, or the SDK's own status for errors it raises itself. */
|
|
@@ -44,6 +85,10 @@ var OyaError = class extends Error {
|
|
|
44
85
|
this.body = body;
|
|
45
86
|
}
|
|
46
87
|
};
|
|
88
|
+
function refusal(message, field, suggestion) {
|
|
89
|
+
const body = { error: message, code: "invalid_request", field, ...suggestion ? { suggestion } : {} };
|
|
90
|
+
return new OyaError(message, Status.BAD_REQUEST, body);
|
|
91
|
+
}
|
|
47
92
|
|
|
48
93
|
// src/cli-config.ts
|
|
49
94
|
var node = () => globalThis.process;
|
|
@@ -67,58 +112,26 @@ function savedConfig() {
|
|
|
67
112
|
}
|
|
68
113
|
}
|
|
69
114
|
|
|
70
|
-
// src/constants.ts
|
|
71
|
-
var DEFAULT_BASE_URL = "https://oyabrowser.com";
|
|
72
|
-
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
73
|
-
var START_TIMEOUT_MS = 12e4;
|
|
74
|
-
var READY_TIMEOUT_MS = 12e4;
|
|
75
|
-
var READY_POLL_MS = 2e3;
|
|
76
|
-
var NAVIGATE_TIMEOUT_MS = 12e4;
|
|
77
|
-
var CHALLENGE_TIMEOUT_MS = 18e4;
|
|
78
|
-
var AGENT_TIMEOUT_MS = 6e5;
|
|
79
|
-
var PLAYBOOK_TIMEOUT_MS = 12e4;
|
|
80
|
-
var STOP_TIMEOUT_MS = 6e4;
|
|
81
|
-
var WAIT_FOR_DEFAULT_MS = 3e4;
|
|
82
|
-
var WAIT_FOR_GRACE_MS = 5e3;
|
|
83
|
-
var AIMED_SCROLL_AMOUNT = 500;
|
|
84
|
-
var RUN_POLL_MS = 2e3;
|
|
85
|
-
var MAX_RUN_POLL_ERRORS = 5;
|
|
86
|
-
var BYTES_PER_MB = 1048576;
|
|
87
|
-
var MAX_FILE_MB = 10;
|
|
88
|
-
var BASE64_CHUNK_BYTES = 8192;
|
|
89
|
-
var MS_PER_SECOND = 1e3;
|
|
90
|
-
var Status = {
|
|
91
|
-
/** The caller passed something unusable, such as a non-numeric element id. */
|
|
92
|
-
BAD_REQUEST: 400,
|
|
93
|
-
/** The server does not know that session. */
|
|
94
|
-
NOT_FOUND: 404,
|
|
95
|
-
/** The browser is in a state that needs attention first. */
|
|
96
|
-
CONFLICT: 409,
|
|
97
|
-
/** A browser command ran and failed. */
|
|
98
|
-
UNPROCESSABLE: 422,
|
|
99
|
-
/** A run failed without saying why. */
|
|
100
|
-
SERVER_ERROR: 500,
|
|
101
|
-
/** A browser did not come up in time. */
|
|
102
|
-
GATEWAY_TIMEOUT: 504
|
|
103
|
-
};
|
|
104
|
-
|
|
105
115
|
// src/client.ts
|
|
116
|
+
var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
|
|
106
117
|
var Http = class {
|
|
107
|
-
/** Stores where to call, with which key, how long to wait
|
|
108
|
-
constructor(baseUrl, apiKey, timeoutMs, fetchImpl) {
|
|
118
|
+
/** Stores where to call, with which key, how long to wait, which fetch to use, and where the key and address came from. */
|
|
119
|
+
constructor(baseUrl, apiKey, timeoutMs, fetchImpl, origin = OPTIONS_ORIGIN) {
|
|
109
120
|
this.baseUrl = baseUrl;
|
|
110
121
|
this.apiKey = apiKey;
|
|
111
122
|
this.timeoutMs = timeoutMs;
|
|
112
123
|
this.fetchImpl = fetchImpl;
|
|
124
|
+
this.origin = origin;
|
|
113
125
|
}
|
|
114
126
|
baseUrl;
|
|
115
127
|
apiKey;
|
|
116
128
|
timeoutMs;
|
|
117
129
|
fetchImpl;
|
|
130
|
+
origin;
|
|
118
131
|
/** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
|
|
119
132
|
async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
|
|
120
133
|
const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
|
|
121
|
-
return readAnswer(res, `${method} ${path}`, this
|
|
134
|
+
return readAnswer(res, `${method} ${path}`, this);
|
|
122
135
|
}
|
|
123
136
|
/** The fetch options for one call, with this client's key on them. */
|
|
124
137
|
init(method, body, timeoutMs, headers) {
|
|
@@ -133,23 +146,34 @@ var Http = class {
|
|
|
133
146
|
try {
|
|
134
147
|
return await this.fetchImpl(url, init);
|
|
135
148
|
} catch (err) {
|
|
136
|
-
throw
|
|
149
|
+
throw noAnswer(this, timeoutMs, err);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return
|
|
153
|
+
var timedOut = (err) => err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
|
|
154
|
+
function causeOf(err) {
|
|
155
|
+
const cause = err?.cause;
|
|
156
|
+
return cause?.code ?? cause?.errors?.[0]?.code;
|
|
157
|
+
}
|
|
158
|
+
function noAnswer(http, timeoutMs, err) {
|
|
159
|
+
const code = timedOut(err) ? "timeout" : "unreachable";
|
|
160
|
+
const body = { error: String(err?.message), code, ...causeOf(err) ? { cause: causeOf(err) } : {} };
|
|
161
|
+
return new OyaError(unreachable(http, timeoutMs, code), 0, body);
|
|
162
|
+
}
|
|
163
|
+
function unreachable({ baseUrl, origin }, timeoutMs, code) {
|
|
164
|
+
if (code === "timeout")
|
|
165
|
+
return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
|
|
166
|
+
if (origin.baseUrlFrom === "default") return `Could not reach ${baseUrl}. Check your connection.`;
|
|
167
|
+
return `Could not reach ${baseUrl}. Is the server running, and is ${origin.baseUrlFrom} right?`;
|
|
144
168
|
}
|
|
145
169
|
function requestInit(method, body, timeoutMs, headers) {
|
|
146
170
|
const json = body === void 0 ? {} : { "Content-Type": "application/json" };
|
|
147
171
|
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
148
172
|
return { method, headers: { ...headers, ...json }, body: payload, signal: AbortSignal.timeout(timeoutMs) };
|
|
149
173
|
}
|
|
150
|
-
async function readAnswer(res, call2,
|
|
174
|
+
async function readAnswer(res, call2, http) {
|
|
151
175
|
const payload = parseBody(await res.text());
|
|
152
|
-
if (!res.ok) throw failure(call2, res.status, payload,
|
|
176
|
+
if (!res.ok) throw failure(call2, res.status, payload, http);
|
|
153
177
|
return payload;
|
|
154
178
|
}
|
|
155
179
|
function parseBody(text) {
|
|
@@ -159,22 +183,74 @@ function parseBody(text) {
|
|
|
159
183
|
return text;
|
|
160
184
|
}
|
|
161
185
|
}
|
|
162
|
-
function failure(call2, status, payload,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
function failure(call2, status, payload, http) {
|
|
187
|
+
const message = payload?.error || `${call2} failed (${status})`;
|
|
188
|
+
return new OyaError(message === "Invalid API key" ? invalidKey(http) : message, status, payload);
|
|
189
|
+
}
|
|
190
|
+
function invalidKey({ baseUrl, origin }) {
|
|
191
|
+
const overrides = origin.apiKeyFrom === "OYA_API_KEY" && origin.savedKey;
|
|
192
|
+
const rest = overrides ? ", which overrides the one saved in ~/.oya/config.json" : "";
|
|
193
|
+
return `Invalid API key for ${baseUrl}. The key came from ${origin.apiKeyFrom}${rest}.`;
|
|
168
194
|
}
|
|
169
195
|
var env = (name) => globalThis.process?.env?.[name];
|
|
196
|
+
function firstOf(option, name, saved, optionName) {
|
|
197
|
+
if (option) return { value: option, from: `the ${optionName} option` };
|
|
198
|
+
if (env(name)) return { value: env(name), from: name };
|
|
199
|
+
return { value: saved, from: "~/.oya/config.json" };
|
|
200
|
+
}
|
|
201
|
+
function addressOf(options, saved) {
|
|
202
|
+
const found = firstOf(options.baseUrl, "OYA_BASE_URL", saved, "baseUrl");
|
|
203
|
+
return found.value ? found : { value: DEFAULT_BASE_URL, from: "default" };
|
|
204
|
+
}
|
|
205
|
+
function keyOf(options, saved) {
|
|
206
|
+
const found = firstOf(options.apiKey, "OYA_API_KEY", saved, "apiKey");
|
|
207
|
+
if (!found.value) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
|
|
208
|
+
return { ...found, value: headerSafe(found.value, "apiKey", "Copy the key again.") };
|
|
209
|
+
}
|
|
170
210
|
function createHttp(options) {
|
|
171
211
|
const saved = savedConfig();
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || saved.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
212
|
+
const key = keyOf(options, saved.apiKey);
|
|
213
|
+
const address = addressOf(options, saved.baseUrl);
|
|
175
214
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
176
215
|
if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
|
|
177
|
-
|
|
216
|
+
const origin = { apiKeyFrom: key.from, baseUrlFrom: address.from, savedKey: !!saved.apiKey };
|
|
217
|
+
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
218
|
+
return new Http(webAddress(address), key.value, timeout, fetchImpl.bind(globalThis), origin);
|
|
219
|
+
}
|
|
220
|
+
var LOCAL_HOST = /^(localhost|127\.)/i;
|
|
221
|
+
function webAddress({ value, from }) {
|
|
222
|
+
const url = value.trim().replace(/\/+$/, "");
|
|
223
|
+
if (/^https?:\/\//i.test(url)) return url;
|
|
224
|
+
const name = from.startsWith("the ") ? "baseUrl" : from === "OYA_BASE_URL" ? from : `baseUrl in ${from}`;
|
|
225
|
+
const suggestion = suggestionFor(url);
|
|
226
|
+
const tryIt = suggestion ? ` Try ${suggestion}.` : "";
|
|
227
|
+
throw refusal(`${name} must start with http:// or https://, not "${url}".${tryIt}`, "baseUrl", suggestion);
|
|
228
|
+
}
|
|
229
|
+
function suggestionFor(url) {
|
|
230
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !/^[^:/]+:\d+(\/|$)/.test(url)) return void 0;
|
|
231
|
+
return `${LOCAL_HOST.test(url) ? "http" : "https"}://${url}`;
|
|
232
|
+
}
|
|
233
|
+
function notInAHeader(ch) {
|
|
234
|
+
const code = ch.codePointAt(0);
|
|
235
|
+
return code < SPACE && code !== TAB || code === DEL || code > LATIN1_MAX;
|
|
236
|
+
}
|
|
237
|
+
function headerSafe(value, field, ending) {
|
|
238
|
+
const trimmed = value.trim();
|
|
239
|
+
const chars = Array.from(trimmed);
|
|
240
|
+
const at = chars.findIndex(notInAHeader);
|
|
241
|
+
if (at < 0) return trimmed;
|
|
242
|
+
const leading = Array.from(value).length - Array.from(value.trimStart()).length;
|
|
243
|
+
const where = `position ${leading + at + 1} (${codePoint(chars[at])})`;
|
|
244
|
+
throw refusal(`${field} has a character HTTP headers cannot carry at ${where}. ${ending}`, field);
|
|
245
|
+
}
|
|
246
|
+
var codePoint = (ch) => `U+${ch.codePointAt(0).toString(HEX).toUpperCase().padStart(CODE_POINT_DIGITS, "0")}`;
|
|
247
|
+
var shown = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
248
|
+
function segment(value, field = "id") {
|
|
249
|
+
if (typeof value !== "string" || !value)
|
|
250
|
+
throw refusal(`${field} must be a non-empty string, not ${shown(value)}`, field);
|
|
251
|
+
if (value.includes("/") || value === "." || value === "..")
|
|
252
|
+
throw refusal(`${field} must be a single path segment (no "/" and not "." or ".."), not ${shown(value)}`, field);
|
|
253
|
+
return encodeURIComponent(value);
|
|
178
254
|
}
|
|
179
255
|
|
|
180
256
|
// src/run-watch.ts
|
|
@@ -396,6 +472,17 @@ var Browser = class {
|
|
|
396
472
|
const path = `/api/browsers/${this.id}/chat`;
|
|
397
473
|
return agentAnswer(await this.http.request("POST", path, body, AGENT_TIMEOUT_MS)).text;
|
|
398
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* `ask()` for data: the agent does the task and answers in the shape of `schema`
|
|
477
|
+
* (a JSON schema), instead of in words. Throws when the agent reports it could not.
|
|
478
|
+
*/
|
|
479
|
+
async extract(prompt, schema, values = {}) {
|
|
480
|
+
const body = { messages: [{ role: "user", content: prompt }], ...values, schema };
|
|
481
|
+
const path = `/api/browsers/${this.id}/chat`;
|
|
482
|
+
const res = agentAnswer(await this.http.request("POST", path, body, AGENT_TIMEOUT_MS));
|
|
483
|
+
if (res.failed || res.data === void 0) throw new OyaError(res.text, Status.UNPROCESSABLE, res);
|
|
484
|
+
return res.data;
|
|
485
|
+
}
|
|
399
486
|
/**
|
|
400
487
|
* Save the last `ask()` on this browser as a named playbook. Every value that was
|
|
401
488
|
* typed, picked or clicked becomes a variable, with what the run used kept in
|
|
@@ -413,7 +500,7 @@ var Browser = class {
|
|
|
413
500
|
* Play `'<name>:draft'` to try a draft before promoting it.
|
|
414
501
|
*/
|
|
415
502
|
async play(name, data = {}, { autoHeal = true } = {}) {
|
|
416
|
-
const path = `/api/browsers/${this.id}/playbooks/${
|
|
503
|
+
const path = `/api/browsers/${this.id}/playbooks/${segment(name, "name")}/play`;
|
|
417
504
|
return agentAnswer(
|
|
418
505
|
await this.http.request("POST", path, { variables: data, autoHeal }, AGENT_TIMEOUT_MS)
|
|
419
506
|
);
|
|
@@ -464,7 +551,7 @@ var Browser = class {
|
|
|
464
551
|
}
|
|
465
552
|
/** Revoke a link from `shareUrl()` before it expires, by the id it returned. */
|
|
466
553
|
async revokeShare(id) {
|
|
467
|
-
await this.http.request("DELETE", `/api/control/credentials/${
|
|
554
|
+
await this.http.request("DELETE", `/api/control/credentials/${segment(id)}`);
|
|
468
555
|
}
|
|
469
556
|
/** Counters, health and the last 50 things this browser did. */
|
|
470
557
|
status() {
|
|
@@ -520,8 +607,9 @@ function startBody(options) {
|
|
|
520
607
|
const { profile, persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy } = options;
|
|
521
608
|
return { profile: profile || persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy };
|
|
522
609
|
}
|
|
610
|
+
var idempotencyKeyOf = ({ idempotencyKey }) => idempotencyKey ? headerSafe(idempotencyKey, "idempotencyKey", 'Use letters, digits, "-" and "_".') : globalThis.crypto.randomUUID();
|
|
523
611
|
async function start(http, wait, options) {
|
|
524
|
-
const headers = { "Idempotency-Key": options
|
|
612
|
+
const headers = { "Idempotency-Key": idempotencyKeyOf(options) };
|
|
525
613
|
const path = "/api/browsers/start";
|
|
526
614
|
const started = await http().request("POST", path, startBody(options), START_TIMEOUT_MS, headers);
|
|
527
615
|
if (started.status === "starting") {
|
|
@@ -530,7 +618,7 @@ async function start(http, wait, options) {
|
|
|
530
618
|
}
|
|
531
619
|
return new Browser(http(), started, options.captcha === "auto");
|
|
532
620
|
}
|
|
533
|
-
var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${
|
|
621
|
+
var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${segment(id)}`);
|
|
534
622
|
async function reattach(http, id) {
|
|
535
623
|
const found = await fetchBrowser(http, id);
|
|
536
624
|
return new Browser(http(), asStarted(found), false);
|
|
@@ -542,69 +630,79 @@ function asStarted(found) {
|
|
|
542
630
|
var stopBrowsers = (http, ids) => http().request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, START_TIMEOUT_MS);
|
|
543
631
|
var browserApi = (http, wait) => ({
|
|
544
632
|
/** Start a browser and wait until it can take commands. */
|
|
545
|
-
start: (options = {}) => start(http, wait, options),
|
|
633
|
+
start: async (options = {}) => start(http, wait, options),
|
|
546
634
|
/** Reattach to a browser that is already running. */
|
|
547
|
-
get: (id) => reattach(http, id),
|
|
635
|
+
get: async (id) => reattach(http, id),
|
|
548
636
|
/** Every browser on this key. */
|
|
549
|
-
list: () => http().request("GET", "/api/browsers"),
|
|
637
|
+
list: async () => http().request("GET", "/api/browsers"),
|
|
550
638
|
/** Stop some (`ids`) or every browser on this key. Each reports separately. */
|
|
551
|
-
stop: (ids) => stopBrowsers(http, ids),
|
|
639
|
+
stop: async (ids) => stopBrowsers(http, ids),
|
|
552
640
|
/** Stop every browser on this key; returns how many stopped. */
|
|
553
641
|
stopAll: async () => (await stopBrowsers(http, "all")).stopped
|
|
554
642
|
});
|
|
555
643
|
|
|
556
644
|
// src/api/control.ts
|
|
557
|
-
var session = (id, action = "") => `/api/control/sessions/${
|
|
558
|
-
var item = (kind, id) => `/api/control/${kind}/${
|
|
645
|
+
var session = (id, action = "") => `/api/control/sessions/${segment(id)}${action}`;
|
|
646
|
+
var item = (kind, id) => `/api/control/${kind}/${segment(id)}`;
|
|
559
647
|
var sessionCalls = (http) => ({
|
|
560
648
|
/** Settings, sessions and recent events at a glance. */
|
|
561
|
-
overview: () => http().request("GET", "/api/control"),
|
|
649
|
+
overview: async () => http().request("GET", "/api/control"),
|
|
562
650
|
/** Every session, including disconnected and cleanup-pending ones. */
|
|
563
|
-
sessions: () => http().request("GET", "/api/control/sessions"),
|
|
651
|
+
sessions: async () => http().request("GET", "/api/control/sessions"),
|
|
564
652
|
/** One session. */
|
|
565
|
-
session: (id) => http().request("GET", session(id)),
|
|
653
|
+
session: async (id) => http().request("GET", session(id)),
|
|
566
654
|
/** Update limits, rate cards and retention. */
|
|
567
|
-
settings: (changes) => http().request("PATCH", "/api/control/project", changes)
|
|
655
|
+
settings: async (changes) => http().request("PATCH", "/api/control/project", changes)
|
|
568
656
|
});
|
|
569
657
|
var lifecycleCalls = (http) => ({
|
|
570
|
-
/**
|
|
571
|
-
cancel: (id) => http().request("POST", session(id, "/cancel"), {}),
|
|
658
|
+
/** Stop a session in any state; answers its final state. */
|
|
659
|
+
cancel: async (id) => http().request("POST", session(id, "/cancel"), {}),
|
|
572
660
|
/** Stop a session; `force` stops despite a profile-save error, or reconciles. */
|
|
573
|
-
stop: (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
|
|
661
|
+
stop: async (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
|
|
574
662
|
/** Acquire or release human control, or acknowledge the agent's resume. */
|
|
575
|
-
takeover: (id, action) => http().request("POST", session(id, "/control"), { action }),
|
|
663
|
+
takeover: async (id, action) => http().request("POST", session(id, "/control"), { action }),
|
|
576
664
|
/** Send one input as the human holding the control lease. */
|
|
577
|
-
input: (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
|
|
665
|
+
input: async (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
|
|
578
666
|
});
|
|
667
|
+
function recoverBody(options) {
|
|
668
|
+
const { replace = false, wsUrl } = typeof options === "boolean" ? { replace: options } : options ?? {};
|
|
669
|
+
if (wsUrl && !replace)
|
|
670
|
+
throw refusal("wsUrl is only used with replace: true; recovering in place keeps the endpoint it had.", "wsUrl");
|
|
671
|
+
return wsUrl ? { replace, wsUrl } : { replace };
|
|
672
|
+
}
|
|
579
673
|
var recoveryCalls = (http) => ({
|
|
580
|
-
/**
|
|
581
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Explicitly recover a session, or replace it with a fresh one. A cdp
|
|
676
|
+
* session's replacement needs the Chrome it runs on: `{ replace: true, wsUrl }`.
|
|
677
|
+
* `recover(id, true)` still means replace.
|
|
678
|
+
*/
|
|
679
|
+
recover: async (id, options = {}) => http().request("POST", session(id, "/recover"), recoverBody(options)),
|
|
582
680
|
/** A single-use ticket for the live stream. */
|
|
583
|
-
ticket: (id) => http().request("POST", session(id, "/ticket"), {}),
|
|
681
|
+
ticket: async (id) => http().request("POST", session(id, "/ticket"), {}),
|
|
584
682
|
/** Durable lifecycle events after a cursor. */
|
|
585
|
-
events: (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
|
|
683
|
+
events: async (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
|
|
586
684
|
});
|
|
587
685
|
var credentialCalls = (http) => ({
|
|
588
686
|
/** Mint a service credential. Its token is returned this once. */
|
|
589
|
-
createCredential: (options) => http().request("POST", "/api/control/credentials", options),
|
|
687
|
+
createCredential: async (options) => http().request("POST", "/api/control/credentials", options),
|
|
590
688
|
/** Revoke a service credential. */
|
|
591
|
-
revokeCredential: (id) => http().request("DELETE", item("credentials", id))
|
|
689
|
+
revokeCredential: async (id) => http().request("DELETE", item("credentials", id))
|
|
592
690
|
});
|
|
593
691
|
var memberCalls = (http) => ({
|
|
594
692
|
/** The owner and every member. */
|
|
595
|
-
members: () => http().request("GET", "/api/control/members"),
|
|
693
|
+
members: async () => http().request("GET", "/api/control/members"),
|
|
596
694
|
/** An invitation code for a new member. */
|
|
597
|
-
inviteMember: (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
|
|
695
|
+
inviteMember: async (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
|
|
598
696
|
/** Remove a member. */
|
|
599
|
-
removeMember: (userId) => http().request("DELETE", item("members", userId))
|
|
697
|
+
removeMember: async (userId) => http().request("DELETE", item("members", userId))
|
|
600
698
|
});
|
|
601
699
|
var webhookCalls = (http) => ({
|
|
602
700
|
/** Register a webhook; `types` limits which events it receives. */
|
|
603
|
-
createWebhook: (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
|
|
701
|
+
createWebhook: async (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
|
|
604
702
|
/** Disable a webhook. */
|
|
605
|
-
removeWebhook: (id) => http().request("DELETE", item("webhooks", id)),
|
|
703
|
+
removeWebhook: async (id) => http().request("DELETE", item("webhooks", id)),
|
|
606
704
|
/** Send a delivery again. */
|
|
607
|
-
replayDelivery: (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
|
|
705
|
+
replayDelivery: async (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
|
|
608
706
|
});
|
|
609
707
|
var controlApi = (http) => ({
|
|
610
708
|
...sessionCalls(http),
|
|
@@ -616,7 +714,7 @@ var controlApi = (http) => ({
|
|
|
616
714
|
});
|
|
617
715
|
|
|
618
716
|
// src/api/playbooks.ts
|
|
619
|
-
var playbook = (name) => `/api/playbooks/${
|
|
717
|
+
var playbook = (name) => `/api/playbooks/${segment(name, "name")}`;
|
|
620
718
|
var playbookApi = (http) => ({
|
|
621
719
|
/** Every saved playbook, with any draft waiting on it. */
|
|
622
720
|
list: async () => (await http().request("GET", "/api/playbooks")).playbooks,
|
|
@@ -625,7 +723,7 @@ var playbookApi = (http) => ({
|
|
|
625
723
|
await http().request("DELETE", playbook(name));
|
|
626
724
|
},
|
|
627
725
|
/** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
|
|
628
|
-
promote: (name) => http().request("POST", `${playbook(name)}/promote`, {})
|
|
726
|
+
promote: async (name) => http().request("POST", `${playbook(name)}/promote`, {})
|
|
629
727
|
});
|
|
630
728
|
|
|
631
729
|
// src/api/proxies.ts
|
|
@@ -633,10 +731,10 @@ var proxyApi = (http) => ({
|
|
|
633
731
|
/** Every proxy this key can use, shared ones included. */
|
|
634
732
|
list: async () => (await http().request("GET", "/api/proxies")).proxies,
|
|
635
733
|
/** Add a proxy. Its credentials are never read back. */
|
|
636
|
-
create: (proxy) => http().request("POST", "/api/proxies", proxy),
|
|
734
|
+
create: async (proxy) => http().request("POST", "/api/proxies", proxy),
|
|
637
735
|
/** Remove one of this key's proxies. */
|
|
638
736
|
remove: async (id) => {
|
|
639
|
-
await http().request("DELETE", `/api/proxies/${
|
|
737
|
+
await http().request("DELETE", `/api/proxies/${segment(id)}`);
|
|
640
738
|
},
|
|
641
739
|
/** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
|
|
642
740
|
check: async () => (await http().request("POST", "/api/proxies/check", {})).results
|
|
@@ -647,35 +745,38 @@ var identityCalls = (http) => ({
|
|
|
647
745
|
/** Every persona on this key. */
|
|
648
746
|
list: async () => (await http().request("GET", "/api/personas")).personas,
|
|
649
747
|
/** One persona. */
|
|
650
|
-
get: (id) => http().request("GET", `/api/personas/${id}`),
|
|
748
|
+
get: async (id) => http().request("GET", `/api/personas/${segment(id)}`),
|
|
651
749
|
/**
|
|
652
750
|
* Create an identity. The device, platform, timezone, locale, is chosen
|
|
653
751
|
* here and fixed for its life; `preview()` shows what a choice produces.
|
|
654
752
|
*/
|
|
655
|
-
create: (options = {}) => http().request("POST", "/api/personas", options),
|
|
753
|
+
create: async (options = {}) => http().request("POST", "/api/personas", options),
|
|
656
754
|
/** Name, concurrency cap and proxy hint. Never the device, clone for that. */
|
|
657
|
-
update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
|
|
755
|
+
update: async (id, changes) => http().request("PUT", `/api/personas/${segment(id)}`, changes),
|
|
658
756
|
/** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
|
|
659
|
-
clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
|
|
757
|
+
clone: async (id, options = {}) => http().request("POST", `/api/personas/${segment(id)}/clone`, options)
|
|
660
758
|
});
|
|
661
759
|
var deviceCalls = (http) => ({
|
|
662
760
|
/** The fingerprint these choices would produce. Persists nothing. */
|
|
663
761
|
preview: async (prefs = {}) => (await http().request("POST", "/api/personas/preview", { prefs })).fingerprint,
|
|
664
762
|
/** Platforms, and the timezones and locales each may coherently claim. */
|
|
665
|
-
options: () => http().request("GET", "/api/personas/options"),
|
|
763
|
+
options: async () => http().request("GET", "/api/personas/options"),
|
|
666
764
|
/** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
|
|
667
|
-
pinProxy: (id, proxyId) => http().request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
|
|
765
|
+
pinProxy: async (id, proxyId) => http().request("PUT", `/api/personas/${segment(id)}/proxy`, { proxyId }),
|
|
668
766
|
/** Delete a persona. */
|
|
669
767
|
remove: async (id) => {
|
|
670
|
-
await http().request("DELETE", `/api/personas/${id}`);
|
|
768
|
+
await http().request("DELETE", `/api/personas/${segment(id)}`);
|
|
671
769
|
}
|
|
672
770
|
});
|
|
673
771
|
var mfaCalls = (http) => ({
|
|
674
772
|
/** Store the second factor for this identity. Sealed at rest, never read back. */
|
|
675
|
-
setMfa: (id, config) => http().request("PUT", `/api/personas/${id}/mfa`, config),
|
|
773
|
+
setMfa: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/mfa`, config),
|
|
676
774
|
/** Remove the persona-wide factor, or the one filed against `domain`. */
|
|
677
775
|
clearMfa: async (id, domain) => {
|
|
678
|
-
await http().request(
|
|
776
|
+
await http().request(
|
|
777
|
+
"DELETE",
|
|
778
|
+
`/api/personas/${segment(id)}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`
|
|
779
|
+
);
|
|
679
780
|
}
|
|
680
781
|
});
|
|
681
782
|
var loginCalls = (http) => ({
|
|
@@ -686,20 +787,20 @@ var loginCalls = (http) => ({
|
|
|
686
787
|
* better path. This is for portals that expire a session server-side
|
|
687
788
|
* between runs, where an unattended run has nothing else to recover with.
|
|
688
789
|
*/
|
|
689
|
-
setCredentials: (id, config) => http().request("PUT", `/api/personas/${id}/credentials`, config),
|
|
790
|
+
setCredentials: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/credentials`, config),
|
|
690
791
|
/** Which sites this identity can sign in to. Usernames only. */
|
|
691
|
-
credentials: (id) => http().request("GET", `/api/personas/${id}/credentials`),
|
|
792
|
+
credentials: async (id) => http().request("GET", `/api/personas/${segment(id)}/credentials`),
|
|
692
793
|
/** Remove the login stored for one site. */
|
|
693
794
|
clearCredentials: async (id, domain) => {
|
|
694
|
-
await http().request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
|
|
795
|
+
await http().request("DELETE", `/api/personas/${segment(id)}/credentials?domain=${encodeURIComponent(domain)}`);
|
|
695
796
|
}
|
|
696
797
|
});
|
|
697
|
-
var jarPath = (id) => `/api/pool/cookies?persona=${
|
|
798
|
+
var jarPath = (id) => `/api/pool/cookies?persona=${segment(id)}`;
|
|
698
799
|
var jarCalls = (http) => ({
|
|
699
800
|
/** Every cookie in the jar; `format: 'playwright'` is ready for `context.addCookies()`. */
|
|
700
|
-
cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${format}`)).cookies,
|
|
801
|
+
cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${encodeURIComponent(format)}`)).cookies,
|
|
701
802
|
/** Merge cookies into the jar. The persona's browsers pick them up on their next visit to each site. */
|
|
702
|
-
importCookies: (id, list) => http().request("PUT", jarPath(id), { cookies: list })
|
|
803
|
+
importCookies: async (id, list) => http().request("PUT", jarPath(id), { cookies: list })
|
|
703
804
|
});
|
|
704
805
|
var cookieCalls = (http) => ({
|
|
705
806
|
...jarCalls(http),
|