@boxline/sdk 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -0
- package/LICENSE +21 -0
- package/README.md +495 -0
- package/dist/client.d.ts +529 -0
- package/dist/client.js +874 -0
- package/dist/client.js.map +1 -0
- package/dist/core.d.ts +77 -0
- package/dist/core.js +223 -0
- package/dist/core.js.map +1 -0
- package/dist/errors.d.ts +300 -0
- package/dist/errors.js +403 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/pagination.d.ts +34 -0
- package/dist/pagination.js +67 -0
- package/dist/pagination.js.map +1 -0
- package/dist/session.d.ts +252 -0
- package/dist/session.js +345 -0
- package/dist/session.js.map +1 -0
- package/dist/streaming.d.ts +7 -0
- package/dist/streaming.js +71 -0
- package/dist/streaming.js.map +1 -0
- package/dist/types.d.ts +2147 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/webhooks.d.ts +27 -0
- package/dist/webhooks.js +184 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +59 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import { queryString, send } from "./core.js";
|
|
2
|
+
import { BoxlineTimeoutError, ErrorCode, makeError, NotFoundError } from "./errors.js";
|
|
3
|
+
import { Page, PagePromise } from "./pagination.js";
|
|
4
|
+
import { Session } from "./session.js";
|
|
5
|
+
import { ndjson, sse } from "./streaming.js";
|
|
6
|
+
const env = (name) => (typeof process !== "undefined" ? process?.env?.[name] : undefined);
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
+
/** Where the SDK goes without baseUrl or BOXLINE_API_URL (the same default as the CLI). */
|
|
9
|
+
export const DEFAULT_BASE_URL = "https://api.boxline.dev";
|
|
10
|
+
/** A plain-English step can wait up to 4 minutes for a person to solve a CAPTCHA (session captcha "ask"). */
|
|
11
|
+
const STEP_TIMEOUT_MS = 420_000;
|
|
12
|
+
/**
|
|
13
|
+
* Client for the Boxline API: isolated cloud sessions with a Chrome browser, a bash shell and a shared disk, plus
|
|
14
|
+
* the web APIs and the AI agent. Works in Node 18+ and modern browsers (it uses the global fetch).
|
|
15
|
+
*
|
|
16
|
+
* const bx = new Boxline(); // BOXLINE_API_KEY; BOXLINE_API_URL (default https://api.boxline.dev)
|
|
17
|
+
* const s = await bx.sessions.create();
|
|
18
|
+
* const browser = await chromium.connectOverCDP(s.connectUrl!);
|
|
19
|
+
*/
|
|
20
|
+
export class Boxline {
|
|
21
|
+
baseUrl;
|
|
22
|
+
auth;
|
|
23
|
+
project;
|
|
24
|
+
apiKeys;
|
|
25
|
+
sessions;
|
|
26
|
+
contexts;
|
|
27
|
+
crawl;
|
|
28
|
+
agent;
|
|
29
|
+
tasks;
|
|
30
|
+
secrets;
|
|
31
|
+
webhooks;
|
|
32
|
+
extensions;
|
|
33
|
+
constructor(opts = {}) {
|
|
34
|
+
this.baseUrl = (opts.baseUrl ?? env("BOXLINE_API_URL") ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
35
|
+
const cfg = {
|
|
36
|
+
apiKey: opts.apiKey ?? env("BOXLINE_API_KEY"),
|
|
37
|
+
baseUrl: this.baseUrl,
|
|
38
|
+
credentials: opts.credentials,
|
|
39
|
+
// Looked up per call, so a fetch patched after the client was made (tests, tracing) is used.
|
|
40
|
+
fetch: opts.fetch ?? ((input, init) => globalThis.fetch(input, init)),
|
|
41
|
+
maxRetries: opts.maxRetries ?? 2,
|
|
42
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
43
|
+
headers: { ...opts.headers },
|
|
44
|
+
};
|
|
45
|
+
// Both hold the API key: not enumerable, so console.log(bx) and JSON.stringify(bx) never show it.
|
|
46
|
+
Object.defineProperty(this, "cfg", { value: cfg, enumerable: false });
|
|
47
|
+
Object.defineProperty(this, "options", { value: opts, enumerable: false });
|
|
48
|
+
this.auth = new Auth(this);
|
|
49
|
+
this.project = new ProjectSettings(this);
|
|
50
|
+
this.apiKeys = new ApiKeys(this);
|
|
51
|
+
this.sessions = new Sessions(this);
|
|
52
|
+
this.contexts = new Contexts(this);
|
|
53
|
+
this.crawl = new Crawl(this);
|
|
54
|
+
this.agent = new Agent(this);
|
|
55
|
+
this.tasks = new Tasks(this);
|
|
56
|
+
this.secrets = new Secrets(this);
|
|
57
|
+
this.webhooks = new Webhooks(this);
|
|
58
|
+
this.extensions = new Extensions(this);
|
|
59
|
+
}
|
|
60
|
+
/** What JSON.stringify shows: where the client points, never its key. */
|
|
61
|
+
toJSON() {
|
|
62
|
+
return { baseUrl: this.baseUrl, maxRetries: this.cfg.maxRetries, timeoutMs: this.cfg.timeoutMs };
|
|
63
|
+
}
|
|
64
|
+
/** A client like this one with some options changed, e.g. `bx.withOptions({ maxRetries: 5 })`. */
|
|
65
|
+
withOptions(opts) {
|
|
66
|
+
return new Boxline({ ...this.options, baseUrl: this.baseUrl, apiKey: this.cfg.apiKey, ...opts });
|
|
67
|
+
}
|
|
68
|
+
/** The calling user/project, its plan's limits and features, and whether it is suspended. */
|
|
69
|
+
me(options) {
|
|
70
|
+
return this.request("GET", "/v1/auth/me", undefined, options);
|
|
71
|
+
}
|
|
72
|
+
/** True when the project's plan includes `feature` (calls using a missing feature fail with 402 feature_not_in_plan). */
|
|
73
|
+
async hasFeature(feature, options) {
|
|
74
|
+
return Boolean((await this.me(options)).project.limits.features[feature]);
|
|
75
|
+
}
|
|
76
|
+
/** Fetch API: open a URL in a real browser (inside a sandbox) and get Markdown, HTML or text back. */
|
|
77
|
+
fetch(url, params = {}, options) {
|
|
78
|
+
return this.request("POST", "/v1/fetch", { url, ...params }, options);
|
|
79
|
+
}
|
|
80
|
+
/** A screenshot of any URL (fresh browser context each time). Returns the image bytes. */
|
|
81
|
+
screenshot(url, params = {}, options) {
|
|
82
|
+
return this.bytes("POST", "/v1/screenshot", { url, ...params }, options);
|
|
83
|
+
}
|
|
84
|
+
/** A PDF of any URL, printed like Chrome's "Save as PDF". Returns the PDF bytes. */
|
|
85
|
+
pdf(url, params = {}, options) {
|
|
86
|
+
return this.bytes("POST", "/v1/pdf", { url, ...params }, options);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Structured data from one or more pages: they are rendered in a real browser, then a model fills `schema`
|
|
90
|
+
* (JSON Schema) and/or follows `prompt`. Defaults to a fast, low-cost model.
|
|
91
|
+
*/
|
|
92
|
+
extract(params, options) {
|
|
93
|
+
return this.request("POST", "/v1/extract", params, options);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Web search (the platform's provider, Brave): titles, URLs, snippets and dates. With `fetch`, the top pages are
|
|
97
|
+
* also opened in a sandboxed browser and returned as Markdown (`content`). The same search within an hour is
|
|
98
|
+
* answered from the cache (`cached: true`, not counted). The query text goes to the provider: keep secrets and
|
|
99
|
+
* personal data out of it. 503 `search_unavailable` (SearchUnavailableError) when search is not set up.
|
|
100
|
+
*/
|
|
101
|
+
search(params, options) {
|
|
102
|
+
return this.request("POST", "/v1/search", params, options);
|
|
103
|
+
}
|
|
104
|
+
/** Usage and cost of the sessions created in a period (default: this month so far). */
|
|
105
|
+
usage(params = {}, options) {
|
|
106
|
+
return this.request("GET", `/v1/usage${queryString(params)}`, undefined, options);
|
|
107
|
+
}
|
|
108
|
+
/** Totals and per-day numbers for the last `days` days (UTC, including today). */
|
|
109
|
+
stats(days = 7, options) {
|
|
110
|
+
return this.request("GET", `/v1/stats${queryString({ days })}`, undefined, options);
|
|
111
|
+
}
|
|
112
|
+
/** The public price table and plans (no API key needed). */
|
|
113
|
+
pricing(options) {
|
|
114
|
+
return this.request("GET", "/v1/pricing", undefined, options);
|
|
115
|
+
}
|
|
116
|
+
/** The OpenAPI 3.1 description of the API. */
|
|
117
|
+
openapi(options) {
|
|
118
|
+
return this.request("GET", "/v1/openapi.json", undefined, options);
|
|
119
|
+
}
|
|
120
|
+
/** `{ok: true}` when the API is up. */
|
|
121
|
+
health(options) {
|
|
122
|
+
return this.request("GET", "/healthz", undefined, options);
|
|
123
|
+
}
|
|
124
|
+
/** Low-level request: parsed JSON (undefined for 204). The SDK's retry and error rules apply. */
|
|
125
|
+
async request(method, path, body, options = {}) {
|
|
126
|
+
return (await send(this.cfg, method, path, { ...options, body })).data;
|
|
127
|
+
}
|
|
128
|
+
/** Low-level request returning the Response with its body unread (after checking for errors). */
|
|
129
|
+
async send(method, path, body, options = {}) {
|
|
130
|
+
return (await send(this.cfg, method, path, { ...options, body, as: "response" })).response;
|
|
131
|
+
}
|
|
132
|
+
/** @internal */
|
|
133
|
+
async bytes(method, path, body, options = {}) {
|
|
134
|
+
return (await send(this.cfg, method, path, { ...options, body, as: "bytes" })).data;
|
|
135
|
+
}
|
|
136
|
+
/** @internal A list whose pages follow `next`; `map` turns each raw item into what the list yields. */
|
|
137
|
+
list(path, query, map, options) {
|
|
138
|
+
const load = async (after) => {
|
|
139
|
+
const body = await this.request("GET", `${path}${queryString({ ...query, after: after ?? query.after })}`, undefined, options);
|
|
140
|
+
return new Page({ ...body, data: (body.data ?? []).map(map) }, (next) => load(next));
|
|
141
|
+
};
|
|
142
|
+
return new PagePromise(() => load());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** The larger of the client's time limit and what a call needs (a long command, a long wait). */
|
|
146
|
+
const atLeast = (client, options, ms) => ({
|
|
147
|
+
...options,
|
|
148
|
+
timeoutMs: options?.timeoutMs ?? Math.max(client.cfg.timeoutMs, ms),
|
|
149
|
+
});
|
|
150
|
+
const sid = (id) => `/v1/sessions/${encodeURIComponent(id)}`;
|
|
151
|
+
// ---------------------------------------------------------------- auth, project, API keys
|
|
152
|
+
/** Sign-up and console logins. Server-side code uses an API key instead of logging in. */
|
|
153
|
+
export class Auth {
|
|
154
|
+
client;
|
|
155
|
+
constructor(client) {
|
|
156
|
+
this.client = client;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Creates a user, a project on the Free plan and a first API key (in the response only). No API key needed.
|
|
160
|
+
* `acceptTerms: true` says the user accepts the terms of service and acceptable use policy (400
|
|
161
|
+
* `terms_not_accepted` without it); `name` is optional.
|
|
162
|
+
*/
|
|
163
|
+
signup(params, options) {
|
|
164
|
+
return this.client.request("POST", "/v1/auth/signup", params, options);
|
|
165
|
+
}
|
|
166
|
+
/** Starts a console login (cookie `bx_session`): for browser apps with `credentials: "include"`. */
|
|
167
|
+
login(params, options) {
|
|
168
|
+
return this.client.request("POST", "/v1/auth/login", params, options);
|
|
169
|
+
}
|
|
170
|
+
/** Ends the console login. */
|
|
171
|
+
logout(options) {
|
|
172
|
+
return this.client.request("POST", "/v1/auth/logout", undefined, options);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Project settings. */
|
|
176
|
+
export class ProjectSettings {
|
|
177
|
+
client;
|
|
178
|
+
constructor(client) {
|
|
179
|
+
this.client = client;
|
|
180
|
+
}
|
|
181
|
+
/** The Trajectories program setting: on by default; see the Terms of Service and Privacy Policy. */
|
|
182
|
+
trajectories(options) {
|
|
183
|
+
return this.client.request("GET", "/v1/project/trajectories", undefined, options);
|
|
184
|
+
}
|
|
185
|
+
/** Turns the Trajectories program on or off for this project (logged). */
|
|
186
|
+
setTrajectories(enabled, opts = {}, options) {
|
|
187
|
+
return this.client.request("PUT", "/v1/project/trajectories", { enabled, ...opts }, options);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The project's settings: `captchaDefault` is what new sessions and agent runs without a `captcha` option get;
|
|
191
|
+
* `captchaDefaultEffective` what they get now (the plan may no longer include solving).
|
|
192
|
+
*/
|
|
193
|
+
settings(options) {
|
|
194
|
+
return this.client.request("GET", "/v1/project/settings", undefined, options);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Changes the fields you send (`captchaDefault: "solve"` needs a plan with CAPTCHA solving: 402 otherwise).
|
|
198
|
+
* `defaultModel` is the model used when a request names none; `null` clears it.
|
|
199
|
+
*/
|
|
200
|
+
setSettings(params, options) {
|
|
201
|
+
return this.client.request("PUT", "/v1/project/settings", params, options);
|
|
202
|
+
}
|
|
203
|
+
/** The four providers with the project's own-key state (never a key: `preview` is its last 4 characters). */
|
|
204
|
+
modelKeys(options) {
|
|
205
|
+
return this.client.request("GET", "/v1/project/model-keys", undefined, options);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Saves a provider key and/or the choice of whose key calls use. The provider checks the key first (400
|
|
209
|
+
* `invalid_model_key` when it refuses it). Runs on your own key have no model charge from Boxline.
|
|
210
|
+
*/
|
|
211
|
+
setModelKey(provider, params, options) {
|
|
212
|
+
return this.client.request("PUT", `/v1/project/model-keys/${provider}`, params, options);
|
|
213
|
+
}
|
|
214
|
+
/** Deletes the provider's key; calls go back to the platform's key where the plan includes it. */
|
|
215
|
+
deleteModelKey(provider, options) {
|
|
216
|
+
return this.client.request("DELETE", `/v1/project/model-keys/${provider}`, undefined, options);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** The project's API keys. */
|
|
220
|
+
export class ApiKeys {
|
|
221
|
+
client;
|
|
222
|
+
constructor(client) {
|
|
223
|
+
this.client = client;
|
|
224
|
+
}
|
|
225
|
+
/** Active keys, oldest first (the keys themselves are never shown again; `prefix` identifies them). */
|
|
226
|
+
list(params = {}, options) {
|
|
227
|
+
return this.client.list("/v1/api-keys", { ...params }, (k) => k, options);
|
|
228
|
+
}
|
|
229
|
+
/** A new key; `key` is in this response only. */
|
|
230
|
+
create(params = {}, options) {
|
|
231
|
+
return this.client.request("POST", "/v1/api-keys", params, options);
|
|
232
|
+
}
|
|
233
|
+
revoke(id, options) {
|
|
234
|
+
return this.client.request("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`, undefined, options);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export class Sessions {
|
|
238
|
+
client;
|
|
239
|
+
files;
|
|
240
|
+
constructor(client) {
|
|
241
|
+
this.client = client;
|
|
242
|
+
this.files = new SessionFiles(client);
|
|
243
|
+
}
|
|
244
|
+
wrap = (data) => new Session(this.client, data);
|
|
245
|
+
async one(method, path, body, options) {
|
|
246
|
+
return this.wrap(await this.client.request(method, path, body, options));
|
|
247
|
+
}
|
|
248
|
+
/** Starts a session (an Idempotency-Key is sent, so a retry never starts a second one). */
|
|
249
|
+
create(params = {}, options) {
|
|
250
|
+
return this.one("POST", "/v1/sessions", params, options);
|
|
251
|
+
}
|
|
252
|
+
get(id, options) {
|
|
253
|
+
return this.one("GET", sid(id), undefined, options);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Sessions, newest first unless `sort` says otherwise. Await it for one page (`total` counts every match), or
|
|
257
|
+
* `for await (const s of bx.sessions.list())` for all of them.
|
|
258
|
+
*/
|
|
259
|
+
list(params = {}, options) {
|
|
260
|
+
return this.client.list("/v1/sessions", { ...params }, this.wrap, options);
|
|
261
|
+
}
|
|
262
|
+
/** @deprecated Use list(): its first page has `total` too. */
|
|
263
|
+
page(params = {}, options) {
|
|
264
|
+
return this.list(params, options).then((p) => p);
|
|
265
|
+
}
|
|
266
|
+
/** Pause, resume or release 1 to 100 sessions at once (session ids; 30 calls per minute per project). */
|
|
267
|
+
bulk(action, ids, options) {
|
|
268
|
+
return this.client.request("POST", "/v1/sessions/bulk", { action, ids }, options);
|
|
269
|
+
}
|
|
270
|
+
/** Changes keepAlive, userMetadata, the proxy, the captcha option or the browser settings. */
|
|
271
|
+
update(id, patch, options) {
|
|
272
|
+
return this.one("PATCH", sid(id), patch, options);
|
|
273
|
+
}
|
|
274
|
+
/** Ends the session and deletes its machine. */
|
|
275
|
+
release(id, options) {
|
|
276
|
+
return this.one("POST", `${sid(id)}/release`, undefined, options);
|
|
277
|
+
}
|
|
278
|
+
/** Saves browser state and files, frees the machine and stops billing. Touching the session resumes it. */
|
|
279
|
+
pause(id, options) {
|
|
280
|
+
return this.one("POST", `${sid(id)}/pause`, undefined, options);
|
|
281
|
+
}
|
|
282
|
+
resume(id, options) {
|
|
283
|
+
return this.one("POST", `${sid(id)}/resume`, undefined, options);
|
|
284
|
+
}
|
|
285
|
+
/** Moves the live session to a fresh machine; clients reconnect to the same connectUrl. */
|
|
286
|
+
async move(id, options) {
|
|
287
|
+
const r = await this.client.request("POST", `${sid(id)}/move`, undefined, options);
|
|
288
|
+
return { session: this.wrap(r.session), timings: r.timings, shell: r.shell ?? null };
|
|
289
|
+
}
|
|
290
|
+
/** Adds time (60–3600 s), up to the plan's maximum session length. */
|
|
291
|
+
extend(id, seconds, options) {
|
|
292
|
+
return this.one("POST", `${sid(id)}/extend`, { seconds }, options);
|
|
293
|
+
}
|
|
294
|
+
/** A new IP for the session's proxy. */
|
|
295
|
+
rotateProxy(id, options) {
|
|
296
|
+
return this.one("POST", `${sid(id)}/proxy/rotate`, undefined, options);
|
|
297
|
+
}
|
|
298
|
+
/** Revokes the session's connect, live and terminal URLs and returns the session with fresh ones. */
|
|
299
|
+
rotateUrls(id, options) {
|
|
300
|
+
return this.one("POST", `${sid(id)}/rotate-urls`, undefined, options);
|
|
301
|
+
}
|
|
302
|
+
/** Fresh signed URLs (treat them like passwords). */
|
|
303
|
+
live(id, options) {
|
|
304
|
+
return this.client.request("GET", `${sid(id)}/live`, undefined, options);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Runs browser actions in order, next to the browser; stops at the first failure. A bare string is a plain-English
|
|
308
|
+
* step: `["click Sign in", {action: "fill", selector: "#q", value: "x"}]`. Each result's `text` says what happened.
|
|
309
|
+
*/
|
|
310
|
+
async actions(id, actions, opts = {}, options) {
|
|
311
|
+
const list = Array.isArray(actions) ? actions : [actions];
|
|
312
|
+
const wait = list.some((a) => typeof a === "string" || a.action === "step") ? STEP_TIMEOUT_MS : 0;
|
|
313
|
+
const r = await this.client.request("POST", `${sid(id)}/actions`, { actions: list, ...opts }, atLeast(this.client, options, wait));
|
|
314
|
+
return r.results;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Runs ONE computer-use action exactly as the model's tool gave it (Anthropic `computer` tool input, or one OpenAI
|
|
318
|
+
* `computer_call` action) on the session's page, and returns the screen after it. With `maxWidth` the screenshot is
|
|
319
|
+
* scaled down and the action's coordinates are read in its pixels. A failure in the page is `ok: false`; input that
|
|
320
|
+
* cannot be mapped, or a point off the screen, throws (400 `invalid_request` / `out_of_viewport`).
|
|
321
|
+
*/
|
|
322
|
+
computer(id, action, opts = {}, options) {
|
|
323
|
+
return this.client.request("POST", `${sid(id)}/computer`, { ...action, ...opts }, options);
|
|
324
|
+
}
|
|
325
|
+
/** Runs a shell command (persistent bash by default: cd/export survive between calls). */
|
|
326
|
+
/**
|
|
327
|
+
* Runs a command and returns its output. It is read as a stream (headers at once, the API keeps the connection
|
|
328
|
+
* alive), so neither a long command nor a wait for the session's setup runs into an HTTP client's own limits
|
|
329
|
+
* (Node's fetch gives up after 300 s without headers or data). Output past 64 KB is cut in the middle, as
|
|
330
|
+
* `truncated` says.
|
|
331
|
+
*/
|
|
332
|
+
async exec(id, command, opts = {}, options) {
|
|
333
|
+
const out = { stdout: new Capture(), stderr: new Capture() };
|
|
334
|
+
// An API from before streamed exec sent headers early answers only once setup and the command are done.
|
|
335
|
+
const limit = atLeast(this.client, options, (opts.timeoutMs ?? 120_000) + 30_000 + 600_000);
|
|
336
|
+
const exit = await this.execStream(id, command, (stream, data) => out[stream].push(data), opts, limit);
|
|
337
|
+
return { stdout: out.stdout.text, stderr: out.stderr.text, ...exit, truncated: exit.truncated || out.stdout.truncated || out.stderr.truncated };
|
|
338
|
+
}
|
|
339
|
+
/** Runs a command and streams its output as it happens; resolves with the exit information. */
|
|
340
|
+
async execStream(id, command, onData, opts = {}, options) {
|
|
341
|
+
const { signal, ...rest } = opts;
|
|
342
|
+
const res = await this.client.send("POST", `${sid(id)}/exec`, { command, ...rest, stream: true }, { signal, ...options });
|
|
343
|
+
let exit = null;
|
|
344
|
+
for await (const msg of ndjson(res.body)) {
|
|
345
|
+
if (msg.type === "exit")
|
|
346
|
+
exit = { exitCode: msg.exitCode ?? null, timedOut: Boolean(msg.timedOut), durationMs: msg.durationMs ?? 0, truncated: Boolean(msg.truncated) };
|
|
347
|
+
else if (msg.type === "stdout" || msg.type === "stderr")
|
|
348
|
+
onData(msg.type, msg.data ?? "");
|
|
349
|
+
else if (msg.type === "error")
|
|
350
|
+
throw makeError(msg.status ?? 500, msg.code ?? "internal", msg.message ?? "the command failed", { requestId: msg.requestId ?? null });
|
|
351
|
+
// "waiting" (for the session's setup) and "ping" lines only keep the connection open.
|
|
352
|
+
}
|
|
353
|
+
return exit ?? { exitCode: null, timedOut: false, durationMs: 0, truncated: false };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Runs Playwright code inside the session's own sandbox (needs a shell), streaming its output to `onData`. In
|
|
357
|
+
* scope: `page`, `context`, `browser`, `env`, and `step()`, `extract()`, `useModel(model)` / `useModel(provider,
|
|
358
|
+
* model)`. A step or extract uses the call's own `{provider, model}`, else the last `useModel()`, else `ai`.
|
|
359
|
+
*/
|
|
360
|
+
async runScript(id, code, opts = {}, options) {
|
|
361
|
+
const { signal, onData: _onData, ...body } = opts;
|
|
362
|
+
const res = await this.client.send("POST", `${sid(id)}/scripts/run`, { code, ...body }, { signal, ...options });
|
|
363
|
+
let stdout = "";
|
|
364
|
+
let stderr = "";
|
|
365
|
+
let exit = { exitCode: null, timedOut: false, durationMs: 0 };
|
|
366
|
+
for await (const msg of ndjson(res.body)) {
|
|
367
|
+
if (msg.type === "exit")
|
|
368
|
+
exit = { exitCode: msg.exitCode ?? null, timedOut: Boolean(msg.timedOut), durationMs: msg.durationMs ?? 0 };
|
|
369
|
+
else if (msg.type === "stdout" || msg.type === "stderr") {
|
|
370
|
+
if (msg.type === "stdout")
|
|
371
|
+
stdout += msg.data ?? "";
|
|
372
|
+
else
|
|
373
|
+
stderr += msg.data ?? "";
|
|
374
|
+
opts.onData?.(msg.type, msg.data ?? "");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { stdout, stderr, ...exit };
|
|
378
|
+
}
|
|
379
|
+
/** Restarts the persistent shell (or the named one). */
|
|
380
|
+
restartShell(id, name, options) {
|
|
381
|
+
return this.client.request("POST", `${sid(id)}/shell/restart`, name === undefined ? {} : { shell: name }, options);
|
|
382
|
+
}
|
|
383
|
+
/** Writes the browser's cookies as a Netscape cookie file in the workspace (for curl -b / wget). */
|
|
384
|
+
exportCookies(id, path, options) {
|
|
385
|
+
return this.client.request("POST", `${sid(id)}/browser/cookies/export`, path === undefined ? {} : { path }, options);
|
|
386
|
+
}
|
|
387
|
+
/** Console, network, navigation, error, lifecycle, action, exec and captcha events, oldest first. */
|
|
388
|
+
events(id, params = {}, options) {
|
|
389
|
+
const { types, after, limit } = params;
|
|
390
|
+
return this.client.list(`${sid(id)}/events`, { types, after: after === undefined ? undefined : String(after), limit }, (e) => e, options);
|
|
391
|
+
}
|
|
392
|
+
/** Events as they happen: first the backlog after `after`, then live. Stop with `break` or `options.signal`. */
|
|
393
|
+
async *streamEvents(id, params = {}, options) {
|
|
394
|
+
const res = await this.client.send("GET", `${sid(id)}/events/stream${queryString(params)}`, undefined, options);
|
|
395
|
+
yield* sse(res.body);
|
|
396
|
+
}
|
|
397
|
+
/** Pages visited, grouped by tab and URL, in the order they were first visited. */
|
|
398
|
+
pages(id, params = {}, options) {
|
|
399
|
+
return this.client.list(`${sid(id)}/pages`, { ...params }, (p) => p, options);
|
|
400
|
+
}
|
|
401
|
+
/** Replay frames kept while the session ran. */
|
|
402
|
+
recording(id, options) {
|
|
403
|
+
return this.client.request("GET", `${sid(id)}/recording`, undefined, options);
|
|
404
|
+
}
|
|
405
|
+
/** One replay frame as JPEG bytes. */
|
|
406
|
+
recordingFrame(id, index, options) {
|
|
407
|
+
return this.client.bytes("GET", `${sid(id)}/recording/frames/${index}`, undefined, options);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/** Files in a session's /workspace (shared by the shell and the browser's downloads folder). */
|
|
411
|
+
/** Output kept like the machine keeps it for a plain exec: the first 64 KB and the last 448 KB, the middle marked. */
|
|
412
|
+
class Capture {
|
|
413
|
+
head = "";
|
|
414
|
+
tail = "";
|
|
415
|
+
total = 0;
|
|
416
|
+
truncated = false;
|
|
417
|
+
push(chunk) {
|
|
418
|
+
this.total += chunk.length;
|
|
419
|
+
const take = Math.max(0, Math.min(64 * 1024 - this.head.length, chunk.length));
|
|
420
|
+
this.head += chunk.slice(0, take);
|
|
421
|
+
this.tail += chunk.slice(take);
|
|
422
|
+
if (this.tail.length > 2 * 448 * 1024)
|
|
423
|
+
this.trim();
|
|
424
|
+
}
|
|
425
|
+
trim() {
|
|
426
|
+
if (this.tail.length <= 448 * 1024)
|
|
427
|
+
return;
|
|
428
|
+
this.tail = this.tail.slice(-448 * 1024);
|
|
429
|
+
this.truncated = true;
|
|
430
|
+
}
|
|
431
|
+
get text() {
|
|
432
|
+
this.trim();
|
|
433
|
+
return this.truncated ? `${this.head}\n[… ${this.total - this.head.length - this.tail.length} characters not shown …]\n${this.tail}` : this.head + this.tail;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
export class SessionFiles {
|
|
437
|
+
client;
|
|
438
|
+
constructor(client) {
|
|
439
|
+
this.client = client;
|
|
440
|
+
}
|
|
441
|
+
url(id, q, suffix = "") {
|
|
442
|
+
return `${sid(id)}/files${suffix}${queryString(q)}`;
|
|
443
|
+
}
|
|
444
|
+
list(id, path = ".", options) {
|
|
445
|
+
return this.client.request("GET", this.url(id, { path, list: "1" }), undefined, options);
|
|
446
|
+
}
|
|
447
|
+
/** A file's bytes. */
|
|
448
|
+
read(id, path, options) {
|
|
449
|
+
return this.client.bytes("GET", this.url(id, { path }), undefined, options);
|
|
450
|
+
}
|
|
451
|
+
async readText(id, path, options) {
|
|
452
|
+
return new TextDecoder().decode(await this.read(id, path, options));
|
|
453
|
+
}
|
|
454
|
+
/** Writes a file (folders are made as needed). */
|
|
455
|
+
write(id, path, data, options) {
|
|
456
|
+
return this.client.request("PUT", this.url(id, { path }), undefined, {
|
|
457
|
+
...options,
|
|
458
|
+
rawBody: data,
|
|
459
|
+
headers: { "content-type": "application/octet-stream", ...options?.headers },
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
delete(id, path, options) {
|
|
463
|
+
return this.client.request("DELETE", this.url(id, { path }), undefined, options);
|
|
464
|
+
}
|
|
465
|
+
/** Waits for a file matching a glob (e.g. `downloads/*.csv`) that has finished writing. */
|
|
466
|
+
waitFor(id, pattern, timeoutMs = 30_000, options) {
|
|
467
|
+
return this.client.request("GET", this.url(id, { pattern, timeoutMs }, "/wait"), undefined, atLeast(this.client, options, timeoutMs + 30_000));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// ---------------------------------------------------------------- contexts
|
|
471
|
+
/** Saved logins: cookies and local storage to start sessions with (`context: {id, persist: true}` fills one). */
|
|
472
|
+
export class Contexts {
|
|
473
|
+
client;
|
|
474
|
+
constructor(client) {
|
|
475
|
+
this.client = client;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* A new saved login: empty, or with `fromSession` holding that working session's current cookies and site storage
|
|
479
|
+
* (sign in there first, e.g. in its live view). `attach: true` also makes the session save to it from now on (at its
|
|
480
|
+
* checkpoints and when it ends); a session that already has a saved login refuses that (409 `conflict`). 413
|
|
481
|
+
* `context_too_large` over 16 MB; PlanLimitError (402) past the plan's `maxContexts` or `maxContextBytes`.
|
|
482
|
+
*/
|
|
483
|
+
create(params = {}, options) {
|
|
484
|
+
return this.client.request("POST", "/v1/contexts", params, options);
|
|
485
|
+
}
|
|
486
|
+
get(id, options) {
|
|
487
|
+
return this.client.request("GET", `/v1/contexts/${encodeURIComponent(id)}`, undefined, options);
|
|
488
|
+
}
|
|
489
|
+
/** Newest first; the first page's `total` counts them all. */
|
|
490
|
+
list(params = {}, options) {
|
|
491
|
+
return this.client.list("/v1/contexts", { ...params }, (c) => c, options);
|
|
492
|
+
}
|
|
493
|
+
rename(id, name, options) {
|
|
494
|
+
return this.client.request("PATCH", `/v1/contexts/${encodeURIComponent(id)}`, { name }, options);
|
|
495
|
+
}
|
|
496
|
+
delete(id, options) {
|
|
497
|
+
return this.client.request("DELETE", `/v1/contexts/${encodeURIComponent(id)}`, undefined, options);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Keeps sign-in details on the saved login (plan feature `loginDetails`), replacing earlier ones: the site, user name,
|
|
501
|
+
* password and optionally the 2FA setup key, sealed like secrets. Returns the context, whose `login` shows
|
|
502
|
+
* `{origin, username, hasPassword, hasTotp}`, never the password or the 2FA secret.
|
|
503
|
+
*/
|
|
504
|
+
setLogin(id, details, options) {
|
|
505
|
+
return this.client.request("PUT", `/v1/contexts/${encodeURIComponent(id)}/login`, details, options);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Changes some of the login details and keeps the rest (setLogin replaces them all); `totpSecret: null` removes 2FA.
|
|
509
|
+
* A password never moves to another site on its own: a new `origin` needs `password` in the same call, and
|
|
510
|
+
* `totpSecret` (a new one or null) when the login has 2FA (400 otherwise). A BoxlineError with code `conflict` (409)
|
|
511
|
+
* when the login changed meanwhile: send it again.
|
|
512
|
+
*/
|
|
513
|
+
updateLogin(id, changes, options) {
|
|
514
|
+
return this.client.request("PATCH", `/v1/contexts/${encodeURIComponent(id)}/login`, changes, options);
|
|
515
|
+
}
|
|
516
|
+
/** Removes the login details (the saved cookies and storage stay). */
|
|
517
|
+
deleteLogin(id, options) {
|
|
518
|
+
return this.client.request("DELETE", `/v1/contexts/${encodeURIComponent(id)}/login`, undefined, options);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// ---------------------------------------------------------------- secrets
|
|
522
|
+
const secretPath = (name) => `/v1/secrets/${encodeURIComponent(name)}`;
|
|
523
|
+
/**
|
|
524
|
+
* Project secrets: write-only values the AI uses as %NAME% placeholders (`secrets` on agent runs, steps and scripts) and
|
|
525
|
+
* shells get as environment variables (`secrets` on sessions.create and exec), depending on each secret's `scope`. The
|
|
526
|
+
* value is never returned, logged or shown; every change and use is audited.
|
|
527
|
+
*/
|
|
528
|
+
export class Secrets {
|
|
529
|
+
client;
|
|
530
|
+
constructor(client) {
|
|
531
|
+
this.client = client;
|
|
532
|
+
}
|
|
533
|
+
/** The project's secrets, in name order, without their values. */
|
|
534
|
+
list(params = {}, options) {
|
|
535
|
+
return this.client.list("/v1/secrets", { ...params }, (s) => s, options);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Stores a secret, sealed; the answer never has the value. SecretExistsError for a name the project has (change it
|
|
539
|
+
* with update), PlanLimitError beyond the plan's `maxSecrets`. Not retried by the SDK (the API takes no
|
|
540
|
+
* Idempotency-Key here): a retry after a lost answer may meet SecretExistsError.
|
|
541
|
+
*/
|
|
542
|
+
create(params, options) {
|
|
543
|
+
return this.client.request("POST", "/v1/secrets", params, options);
|
|
544
|
+
}
|
|
545
|
+
get(name, options) {
|
|
546
|
+
return this.client.request("GET", secretPath(name), undefined, options);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Changes the fields you send. A running agent run keeps the value it started with; a session that exports the secret
|
|
550
|
+
* gets the new value on its next machine (move, resume, recovery).
|
|
551
|
+
*/
|
|
552
|
+
update(name, patch, options) {
|
|
553
|
+
return this.client.request("PATCH", secretPath(name), patch, options);
|
|
554
|
+
}
|
|
555
|
+
/** Deletes it; sessions that exported it no longer get it on their next machine. */
|
|
556
|
+
delete(name, options) {
|
|
557
|
+
return this.client.request("DELETE", secretPath(name), undefined, options);
|
|
558
|
+
}
|
|
559
|
+
/** Changes to secrets and saved login details, and each use (once per session, command, run or script), newest first. */
|
|
560
|
+
audit(params = {}, options) {
|
|
561
|
+
return this.client.list("/v1/secrets/audit", { ...params }, (e) => e, options);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// ---------------------------------------------------------------- crawl
|
|
565
|
+
/** Crawls: follow links from a start URL in the background (robots.txt respected); poll with get() or wait(). */
|
|
566
|
+
export class Crawl {
|
|
567
|
+
client;
|
|
568
|
+
constructor(client) {
|
|
569
|
+
this.client = client;
|
|
570
|
+
}
|
|
571
|
+
start(params, options) {
|
|
572
|
+
return this.client.request("POST", "/v1/crawl", params, options);
|
|
573
|
+
}
|
|
574
|
+
/** The job and one page of its pages (`limit: 0` for the job only); `next` is the cursor of the following pages. */
|
|
575
|
+
get(id, params = {}, options) {
|
|
576
|
+
return this.client.request("GET", `/v1/crawl/${encodeURIComponent(id)}${queryString({ ...params })}`, undefined, options);
|
|
577
|
+
}
|
|
578
|
+
/** Jobs, newest first (without their pages). */
|
|
579
|
+
list(params = {}, options) {
|
|
580
|
+
return this.client.list("/v1/crawl", { ...params }, (j) => j, options);
|
|
581
|
+
}
|
|
582
|
+
cancel(id, options) {
|
|
583
|
+
return this.client.request("POST", `/v1/crawl/${encodeURIComponent(id)}/cancel`, undefined, options);
|
|
584
|
+
}
|
|
585
|
+
/** Every page crawled so far, in index order: `for await (const p of bx.crawl.pages(id))`. */
|
|
586
|
+
async *pages(id, params = {}, options) {
|
|
587
|
+
let after;
|
|
588
|
+
do {
|
|
589
|
+
const job = await this.get(id, { limit: params.limit ?? 100, after }, options);
|
|
590
|
+
yield* job.data;
|
|
591
|
+
after = job.next ?? undefined;
|
|
592
|
+
} while (after);
|
|
593
|
+
}
|
|
594
|
+
/** Waits for the crawl to finish, then returns the job with every page in `data`. */
|
|
595
|
+
async wait(id, opts = {}, options) {
|
|
596
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30 * 60_000);
|
|
597
|
+
let job = await this.get(id, { limit: 0 }, options);
|
|
598
|
+
while (job.status === "running") {
|
|
599
|
+
if (Date.now() > deadline)
|
|
600
|
+
throw new BoxlineTimeoutError("the crawl did not finish in time");
|
|
601
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 1000));
|
|
602
|
+
job = await this.get(id, { limit: 0 }, options);
|
|
603
|
+
}
|
|
604
|
+
const data = [];
|
|
605
|
+
for await (const p of this.pages(id, {}, options))
|
|
606
|
+
data.push(p);
|
|
607
|
+
return { ...job, data, next: null };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// ---------------------------------------------------------------- webhooks
|
|
611
|
+
const whid = (id) => `/v1/webhooks/${encodeURIComponent(id)}`;
|
|
612
|
+
/**
|
|
613
|
+
* Webhook endpoints: signed HTTPS callbacks when something finishes or needs a person. Verify each delivery with
|
|
614
|
+
* verifyWebhook(rawBody, header, secret) and drop event ids you have handled already.
|
|
615
|
+
*/
|
|
616
|
+
export class Webhooks {
|
|
617
|
+
client;
|
|
618
|
+
constructor(client) {
|
|
619
|
+
this.client = client;
|
|
620
|
+
}
|
|
621
|
+
/** A new endpoint (public HTTPS only); `secret` ("whsec_…") is in this response only. */
|
|
622
|
+
create(params, options) {
|
|
623
|
+
return this.client.request("POST", "/v1/webhooks", params, options);
|
|
624
|
+
}
|
|
625
|
+
/** The project's endpoints, oldest first (never their secrets). */
|
|
626
|
+
list(params = {}, options) {
|
|
627
|
+
return this.client.list("/v1/webhooks", { ...params }, (w) => w, options);
|
|
628
|
+
}
|
|
629
|
+
get(id, options) {
|
|
630
|
+
return this.client.request("GET", whid(id), undefined, options);
|
|
631
|
+
}
|
|
632
|
+
/** Changes the URL, events, description, or switches it on or off (`enabled: true` also forgets its failures). */
|
|
633
|
+
update(id, patch, options) {
|
|
634
|
+
return this.client.request("PATCH", whid(id), patch, options);
|
|
635
|
+
}
|
|
636
|
+
/** Deletes the endpoint with its queue and delivery log. */
|
|
637
|
+
delete(id, options) {
|
|
638
|
+
return this.client.request("DELETE", whid(id), undefined, options);
|
|
639
|
+
}
|
|
640
|
+
/** A new secret (in this response only); the old one keeps signing too for 24 hours (a second v1). */
|
|
641
|
+
rotateSecret(id, options) {
|
|
642
|
+
return this.client.request("POST", `${whid(id)}/rotate-secret`, undefined, options);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Sends a `webhook.test` event to this endpoint now, or with `type` a made-up sample of that event type (marked
|
|
646
|
+
* `test: true`); one attempt, waits up to 12 s for the answer.
|
|
647
|
+
*/
|
|
648
|
+
test(id, params = {}, options) {
|
|
649
|
+
return this.client.request("POST", `${whid(id)}/test`, params.type ? { type: params.type } : undefined, options);
|
|
650
|
+
}
|
|
651
|
+
/** Every event type an endpoint can subscribe to, with its group and description (subscribe to "*" for all). */
|
|
652
|
+
eventTypes(options) {
|
|
653
|
+
return this.client.request("GET", "/v1/webhooks/events", undefined, options);
|
|
654
|
+
}
|
|
655
|
+
/** The endpoint's deliveries, newest first, each with its attempt history; `status` filters them. */
|
|
656
|
+
deliveries(id, params = {}, options) {
|
|
657
|
+
return this.client.list(`${whid(id)}/deliveries`, { ...params }, (d) => d, options);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Sends a finished delivery again now, with the same event id and body (409 invalid_state while it is still queued,
|
|
661
|
+
* PayloadExpiredError after 7 days, WebhookDisabledError while the endpoint is switched off).
|
|
662
|
+
*/
|
|
663
|
+
retryDelivery(id, deliveryId, options) {
|
|
664
|
+
return this.client.request("POST", `${whid(id)}/deliveries/${encodeURIComponent(deliveryId)}/retry`, undefined, options);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// ---------------------------------------------------------------- tasks
|
|
668
|
+
const tid = (id) => `/v1/tasks/${encodeURIComponent(id)}`;
|
|
669
|
+
/** The task-run statuses of a run that has not finished (queued: a scheduled run waiting for its turn). */
|
|
670
|
+
const TASK_RUN_OPEN = ["queued", "running", "paused"];
|
|
671
|
+
/**
|
|
672
|
+
* Tasks: saved agent runs (an instruction with %name% variables, an optional output schema, browser settings, a saved
|
|
673
|
+
* login and a model), run by hand or on a schedule. Every run is an agent run tagged with the task. Needs the plan's
|
|
674
|
+
* `agentRuns`; the plan limits tasks and schedules switched on (PlanLimitError).
|
|
675
|
+
*/
|
|
676
|
+
export class Tasks {
|
|
677
|
+
client;
|
|
678
|
+
constructor(client) {
|
|
679
|
+
this.client = client;
|
|
680
|
+
}
|
|
681
|
+
/** Saves a task (an Idempotency-Key is sent, so a retry never saves it twice). */
|
|
682
|
+
create(params, options) {
|
|
683
|
+
return this.client.request("POST", "/v1/tasks", params, options);
|
|
684
|
+
}
|
|
685
|
+
/** The project's tasks, newest first. */
|
|
686
|
+
list(params = {}, options) {
|
|
687
|
+
return this.client.list("/v1/tasks", { ...params }, (t) => t, options);
|
|
688
|
+
}
|
|
689
|
+
get(id, options) {
|
|
690
|
+
return this.client.request("GET", tid(id), undefined, options);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Changes the fields you send; `null` removes an optional one. `schedule` fields are merged into the current schedule
|
|
694
|
+
* (`{schedule: {enabled: false}}` pauses it); switching it on, or changing cron or timezone, counts from now.
|
|
695
|
+
*/
|
|
696
|
+
update(id, patch, options) {
|
|
697
|
+
return this.client.request("PATCH", tid(id), patch, options);
|
|
698
|
+
}
|
|
699
|
+
/** Deletes the task with its run history (its agent runs stay, and runs in progress go on). */
|
|
700
|
+
delete(id, options) {
|
|
701
|
+
return this.client.request("DELETE", tid(id), undefined, options);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Runs the task now and returns its task run (status running) right away; waitForRun() waits for the result. Plain
|
|
705
|
+
* values are written into the instruction; secret ones must come with every run and go in as agent variables (the
|
|
706
|
+
* model sees only %name%). MissingVariablesError when a variable without a default has no value. Counts as an agent
|
|
707
|
+
* run (rate, plan, spend cap). An Idempotency-Key is sent, so a retry never starts a second run. `T`: the type of the
|
|
708
|
+
* result (the JSON answer when the task has an output schema).
|
|
709
|
+
*/
|
|
710
|
+
run(id, params = {}, options) {
|
|
711
|
+
return this.client.request("POST", `${tid(id)}/runs`, params, options);
|
|
712
|
+
}
|
|
713
|
+
/** The task's runs, newest first: by hand, on the schedule, and skipped or missed times (kept 30 days); `status` filters. */
|
|
714
|
+
runs(id, params = {}, options) {
|
|
715
|
+
return this.client.list(`${tid(id)}/runs`, { ...params }, (r) => r, options);
|
|
716
|
+
}
|
|
717
|
+
async waitForRun(first, second, third, fourth) {
|
|
718
|
+
const byIds = typeof first === "string";
|
|
719
|
+
const taskId = byIds ? first : first.taskId;
|
|
720
|
+
const runId = byIds ? second : first.id;
|
|
721
|
+
const opts = ((byIds ? third : second) ?? {});
|
|
722
|
+
const options = (byIds ? fourth : third);
|
|
723
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30 * 60_000);
|
|
724
|
+
for (;;) {
|
|
725
|
+
// There is no GET for one task run: the unfinished ones are a short list, and a run missing from it has finished.
|
|
726
|
+
let open = false;
|
|
727
|
+
for await (const r of this.runs(taskId, { status: TASK_RUN_OPEN, limit: 100 }, options)) {
|
|
728
|
+
if (r.id === runId) {
|
|
729
|
+
open = true;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (!open) {
|
|
734
|
+
for await (const r of this.runs(taskId, { limit: 100 }, options))
|
|
735
|
+
if (r.id === runId)
|
|
736
|
+
return r;
|
|
737
|
+
throw new NotFoundError(404, ErrorCode.notFound, `task run ${runId} is not in the run history of task ${taskId}`);
|
|
738
|
+
}
|
|
739
|
+
if (Date.now() > deadline)
|
|
740
|
+
throw new BoxlineTimeoutError("the task run did not finish in time");
|
|
741
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 1000));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// ---------------------------------------------------------------- extensions
|
|
746
|
+
/** A file's bytes (Node, Deno, Bun); kept out of static imports so browser bundles never see node:fs. */
|
|
747
|
+
async function readFileBytes(path) {
|
|
748
|
+
const fs = (await import(["node", "fs/promises"].join(":")));
|
|
749
|
+
return new Uint8Array(await fs.readFile(path));
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Chrome extensions (Manifest V3) to start sessions with (`extensions: [id]`; plan feature `extensions`). An extension
|
|
753
|
+
* sees every page and every typed value in the sessions that use it: upload only extensions you trust.
|
|
754
|
+
*/
|
|
755
|
+
export class Extensions {
|
|
756
|
+
client;
|
|
757
|
+
constructor(client) {
|
|
758
|
+
this.client = client;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Uploads an unpacked extension as a zip, at most 10 MB: its bytes, a Blob, or a file path (Node). It is checked
|
|
762
|
+
* before it is stored (InvalidExtensionError says why; PayloadTooLargeError over 10 MB; LimitReachedError beyond 100
|
|
763
|
+
* extensions). An Idempotency-Key is sent, so a retry never stores it twice.
|
|
764
|
+
*/
|
|
765
|
+
async upload(zip, options) {
|
|
766
|
+
const body = typeof zip === "string" ? await readFileBytes(zip) : zip instanceof ArrayBuffer ? new Uint8Array(zip) : zip;
|
|
767
|
+
return this.client.request("POST", "/v1/extensions", undefined, {
|
|
768
|
+
...options,
|
|
769
|
+
rawBody: body,
|
|
770
|
+
headers: { "content-type": "application/zip", ...options?.headers },
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
/** The project's extensions, newest first. */
|
|
774
|
+
list(params = {}, options) {
|
|
775
|
+
return this.client.list("/v1/extensions", { ...params }, (e) => e, options);
|
|
776
|
+
}
|
|
777
|
+
get(id, options) {
|
|
778
|
+
return this.client.request("GET", `/v1/extensions/${encodeURIComponent(id)}`, undefined, options);
|
|
779
|
+
}
|
|
780
|
+
/** Deletes it; sessions already running with it keep it until they move or resume. */
|
|
781
|
+
delete(id, options) {
|
|
782
|
+
return this.client.request("DELETE", `/v1/extensions/${encodeURIComponent(id)}`, undefined, options);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// ---------------------------------------------------------------- agent
|
|
786
|
+
/** Agent runs: a model (Claude or GPT) drives the session's browser and shell to finish a task. */
|
|
787
|
+
export class Agent {
|
|
788
|
+
client;
|
|
789
|
+
constructor(client) {
|
|
790
|
+
this.client = client;
|
|
791
|
+
}
|
|
792
|
+
/** Providers and models customers can choose, with prices and whether each is configured on the server. */
|
|
793
|
+
models(options) {
|
|
794
|
+
return this.client.request("GET", "/v1/agent/models", undefined, options);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Starts a run and returns right away (an Idempotency-Key is sent, so a retry never starts a second run). With
|
|
798
|
+
* `output` (a JSON Schema) the answer is JSON matching it: read it with `wait<T>(id)` or `get<T>(id)`.
|
|
799
|
+
*/
|
|
800
|
+
run(params, options) {
|
|
801
|
+
return this.client.request("POST", "/v1/agent/runs", params, options);
|
|
802
|
+
}
|
|
803
|
+
/** The run with its steps. `T`: the type of `result` (text by default; the JSON answer's type with an output schema). */
|
|
804
|
+
get(id, options) {
|
|
805
|
+
return this.client.request("GET", `/v1/agent/runs/${encodeURIComponent(id)}`, undefined, options);
|
|
806
|
+
}
|
|
807
|
+
/** Runs, newest first. */
|
|
808
|
+
list(params = {}, options) {
|
|
809
|
+
return this.client.list("/v1/agent/runs", { ...params }, (r) => r, options);
|
|
810
|
+
}
|
|
811
|
+
/** Take the browser from the agent (it finishes its current action, then waits). RunNotLiveError when its server stopped. */
|
|
812
|
+
takeover(id, reason, options) {
|
|
813
|
+
return this.client.request("POST", `/v1/agent/runs/${encodeURIComponent(id)}/takeover`, reason === undefined ? {} : { reason }, options);
|
|
814
|
+
}
|
|
815
|
+
/** Give the browser back; the agent reads `note` before it continues. RunNotLiveError when its server stopped: continueRun. */
|
|
816
|
+
handBack(id, note, options) {
|
|
817
|
+
return this.client.request("POST", `/v1/agent/runs/${encodeURIComponent(id)}/handback`, note === undefined ? {} : { note }, options);
|
|
818
|
+
}
|
|
819
|
+
/** Stops the run for good. */
|
|
820
|
+
cancel(id, options) {
|
|
821
|
+
return this.client.request("POST", `/v1/agent/runs/${encodeURIComponent(id)}/cancel`, undefined, options);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Continues a run that stopped at one of its limits (errorCode max_steps, max_cost, too_many_errors or no_progress)
|
|
825
|
+
* while its `continuable` is set: a new run in the same session, with the same model, mode, output schema, secrets
|
|
826
|
+
* and saved login, and a compact record of what the previous run did. Returns the new run (`continuedFrom` links
|
|
827
|
+
* back); wait for it with `wait(run.id)` or `stream(run.id)` like any run. A run that had `variables` needs them again
|
|
828
|
+
* (MissingVariablesError otherwise). NotContinuableError: it did not stop at a limit, was continued already, or its
|
|
829
|
+
* window passed. An Idempotency-Key is sent, so a retry never starts a second run.
|
|
830
|
+
*
|
|
831
|
+
* `const next = await bx.agent.continueRun(run.id, { maxSteps: 30, instruction: "The CSV is downloaded already" })`
|
|
832
|
+
*/
|
|
833
|
+
continueRun(id, params = {}, options) {
|
|
834
|
+
return this.client.request("POST", `/v1/agent/runs/${encodeURIComponent(id)}/continue`, params, options);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Tells a working run something (1–2000 characters) without taking the browser: the agent reads it at its next step
|
|
838
|
+
* (a model call or tool under way is not interrupted), and it shows as a `message` step. A run waiting for your help
|
|
839
|
+
* (ask_user_for_help) takes it as the answer and goes on. At most 50 per run; InvalidStateError-like 409
|
|
840
|
+
* (a BoxlineError with code `invalid_state`) once the run has finished, TooManyMessagesError after 50.
|
|
841
|
+
*/
|
|
842
|
+
sendMessage(id, text, options) {
|
|
843
|
+
return this.client.request("POST", `/v1/agent/runs/${encodeURIComponent(id)}/messages`, { text }, options);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* The run as it happens: its steps so far, then new steps, `status` changes, live shell `exec`/`output`, and a
|
|
847
|
+
* final `done` event, after which the iteration ends.
|
|
848
|
+
*/
|
|
849
|
+
async *stream(id, options) {
|
|
850
|
+
const res = await this.client.send("GET", `/v1/agent/runs/${encodeURIComponent(id)}/events`, undefined, options);
|
|
851
|
+
for await (const event of sse(res.body)) {
|
|
852
|
+
yield event;
|
|
853
|
+
if (event.type === "done")
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Polls until the run finishes (a paused run keeps waiting for the user). `T` as on get():
|
|
859
|
+
* `const run = await bx.agent.wait<{ books: Book[] }>(id)`.
|
|
860
|
+
*/
|
|
861
|
+
async wait(id, opts = {}, options) {
|
|
862
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30 * 60_000);
|
|
863
|
+
for (;;) {
|
|
864
|
+
const run = await this.get(id, options);
|
|
865
|
+
if (run.status !== "running" && run.status !== "paused")
|
|
866
|
+
return run;
|
|
867
|
+
if (Date.now() > deadline)
|
|
868
|
+
throw new BoxlineTimeoutError("the agent run did not finish in time");
|
|
869
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 1000));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
export default Boxline;
|
|
874
|
+
//# sourceMappingURL=client.js.map
|