@banata-boxes/sdk 0.1.0 → 0.2.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/dist/index.d.ts +141 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +493 -27
- package/package.json +1 -1
- package/src/index.ts +799 -54
package/src/index.ts
CHANGED
|
@@ -84,6 +84,18 @@ export interface BillingInfo {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export interface LaunchedBrowserSession {
|
|
88
|
+
cdpUrl: string;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
previewViewerUrl: string | null;
|
|
91
|
+
close: () => Promise<void>;
|
|
92
|
+
getPreviewConnection: () => Promise<PreviewConnectionInfo | null>;
|
|
93
|
+
getPreviewViewerUrl: () => Promise<string | null>;
|
|
94
|
+
startPreview: () => Promise<PreviewCommandResponse>;
|
|
95
|
+
navigatePreview: (url: string) => Promise<PreviewCommandResponse>;
|
|
96
|
+
resizePreview: (size: { width?: number; height?: number }) => Promise<PreviewCommandResponse>;
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
export type WebhookEventType =
|
|
88
100
|
| "session.created"
|
|
89
101
|
| "session.assigned"
|
|
@@ -152,6 +164,76 @@ export interface RetryConfig {
|
|
|
152
164
|
maxDelayMs?: number;
|
|
153
165
|
}
|
|
154
166
|
|
|
167
|
+
export type JsonValue =
|
|
168
|
+
| string
|
|
169
|
+
| number
|
|
170
|
+
| boolean
|
|
171
|
+
| null
|
|
172
|
+
| JsonValue[]
|
|
173
|
+
| { [key: string]: JsonValue };
|
|
174
|
+
|
|
175
|
+
export interface PreviewConnectionInfo {
|
|
176
|
+
rawUrl: string;
|
|
177
|
+
token: string;
|
|
178
|
+
sessionId: string;
|
|
179
|
+
wsUrl: string;
|
|
180
|
+
startUrl: string;
|
|
181
|
+
navigateUrl: string;
|
|
182
|
+
resizeUrl: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function buildPreviewViewerUrl(
|
|
186
|
+
connectionUrl: string | null | undefined,
|
|
187
|
+
appUrl: string = "https://boxes.banata.dev",
|
|
188
|
+
): string | null {
|
|
189
|
+
if (!connectionUrl) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const parsed = new URL(connectionUrl);
|
|
195
|
+
const token = parsed.searchParams.get("token");
|
|
196
|
+
const session = parsed.searchParams.get("session");
|
|
197
|
+
const machine = parsed.searchParams.get("machine");
|
|
198
|
+
if (!token || !session) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const backendProtocol =
|
|
203
|
+
parsed.protocol === "wss:"
|
|
204
|
+
? "https:"
|
|
205
|
+
: parsed.protocol === "ws:"
|
|
206
|
+
? "http:"
|
|
207
|
+
: parsed.protocol;
|
|
208
|
+
const viewerUrl = new URL("/preview", appUrl);
|
|
209
|
+
viewerUrl.searchParams.set("backend", `${backendProtocol}//${parsed.host}`);
|
|
210
|
+
viewerUrl.searchParams.set("token", token);
|
|
211
|
+
viewerUrl.searchParams.set("session", session);
|
|
212
|
+
if (machine) {
|
|
213
|
+
viewerUrl.searchParams.set("machine", machine);
|
|
214
|
+
}
|
|
215
|
+
return viewerUrl.toString();
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface PreviewCommandResponse<TPreview = unknown> {
|
|
222
|
+
ok: boolean;
|
|
223
|
+
preview?: TPreview;
|
|
224
|
+
[key: string]: unknown;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface OpenCodeConnectionInfo {
|
|
228
|
+
rawUrl: string;
|
|
229
|
+
token: string;
|
|
230
|
+
sessionId: string;
|
|
231
|
+
stateUrl: string;
|
|
232
|
+
messagesUrl: string;
|
|
233
|
+
promptUrl: string;
|
|
234
|
+
eventsUrl: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
155
237
|
export class BanataError extends Error {
|
|
156
238
|
/** HTTP status code (0 for network errors) */
|
|
157
239
|
status: number;
|
|
@@ -189,15 +271,239 @@ function sleep(ms: number): Promise<void> {
|
|
|
189
271
|
return new Promise((r) => setTimeout(r, ms));
|
|
190
272
|
}
|
|
191
273
|
|
|
274
|
+
function derivePreviewConnection(
|
|
275
|
+
rawUrl: string | null | undefined,
|
|
276
|
+
): PreviewConnectionInfo | null {
|
|
277
|
+
if (!rawUrl) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const parsed = new URL(rawUrl);
|
|
283
|
+
const token = parsed.searchParams.get("token");
|
|
284
|
+
const sessionId = parsed.searchParams.get("session");
|
|
285
|
+
if (!token || !sessionId) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const httpProtocol =
|
|
290
|
+
parsed.protocol === "https:" || parsed.protocol === "wss:"
|
|
291
|
+
? "https:"
|
|
292
|
+
: "http:";
|
|
293
|
+
const wsProtocol =
|
|
294
|
+
parsed.protocol === "https:"
|
|
295
|
+
? "wss:"
|
|
296
|
+
: parsed.protocol === "http:"
|
|
297
|
+
? "ws:"
|
|
298
|
+
: parsed.protocol;
|
|
299
|
+
const search = `token=${encodeURIComponent(token)}&session=${encodeURIComponent(sessionId)}`;
|
|
300
|
+
const isDirectPreviewUrl =
|
|
301
|
+
parsed.pathname.endsWith("/ws") && parsed.pathname.includes("/preview/");
|
|
302
|
+
|
|
303
|
+
if (isDirectPreviewUrl) {
|
|
304
|
+
const basePath = parsed.pathname.slice(0, -3);
|
|
305
|
+
return {
|
|
306
|
+
rawUrl,
|
|
307
|
+
token,
|
|
308
|
+
sessionId,
|
|
309
|
+
startUrl: `${httpProtocol}//${parsed.host}${basePath}/start?${search}`,
|
|
310
|
+
navigateUrl: `${httpProtocol}//${parsed.host}${basePath}/navigate?${search}`,
|
|
311
|
+
resizeUrl: `${httpProtocol}//${parsed.host}${basePath}/resize?${search}`,
|
|
312
|
+
wsUrl: `${wsProtocol}//${parsed.host}${parsed.pathname}?${search}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
rawUrl,
|
|
318
|
+
token,
|
|
319
|
+
sessionId,
|
|
320
|
+
startUrl: `${httpProtocol}//${parsed.host}/preview/start?${search}`,
|
|
321
|
+
navigateUrl: `${httpProtocol}//${parsed.host}/preview/navigate?${search}`,
|
|
322
|
+
resizeUrl: `${httpProtocol}//${parsed.host}/preview/resize?${search}`,
|
|
323
|
+
wsUrl: `${wsProtocol}//${parsed.host}/preview/ws?${search}`,
|
|
324
|
+
};
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function deriveOpenCodeConnection(
|
|
331
|
+
rawUrl: string | null | undefined,
|
|
332
|
+
): OpenCodeConnectionInfo | null {
|
|
333
|
+
if (!rawUrl) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const parsed = new URL(rawUrl);
|
|
339
|
+
const token = parsed.searchParams.get("token");
|
|
340
|
+
const sessionId = parsed.searchParams.get("session");
|
|
341
|
+
if (!token || !sessionId) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const httpProtocol =
|
|
346
|
+
parsed.protocol === "https:" || parsed.protocol === "wss:"
|
|
347
|
+
? "https:"
|
|
348
|
+
: "http:";
|
|
349
|
+
const search = `token=${encodeURIComponent(token)}&session=${encodeURIComponent(sessionId)}`;
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
rawUrl,
|
|
353
|
+
token,
|
|
354
|
+
sessionId,
|
|
355
|
+
stateUrl: `${httpProtocol}//${parsed.host}/opencode/state?${search}`,
|
|
356
|
+
messagesUrl: `${httpProtocol}//${parsed.host}/opencode/messages?${search}`,
|
|
357
|
+
promptUrl: `${httpProtocol}//${parsed.host}/opencode/prompt-async?${search}`,
|
|
358
|
+
eventsUrl: `${httpProtocol}//${parsed.host}/opencode/events?${search}`,
|
|
359
|
+
};
|
|
360
|
+
} catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function parseJsonResponse(response: Response): Promise<unknown> {
|
|
366
|
+
const text = await response.text();
|
|
367
|
+
if (!text) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
return JSON.parse(text);
|
|
373
|
+
} catch {
|
|
374
|
+
return text;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function requestPreviewEndpoint<T>(
|
|
379
|
+
url: string,
|
|
380
|
+
init: RequestInit,
|
|
381
|
+
): Promise<T> {
|
|
382
|
+
const response = await fetch(url, init);
|
|
383
|
+
const payload = await parseJsonResponse(response);
|
|
384
|
+
|
|
385
|
+
if (!response.ok) {
|
|
386
|
+
const message =
|
|
387
|
+
payload && typeof payload === "object" && !Array.isArray(payload) && typeof (payload as Record<string, unknown>).error === "string"
|
|
388
|
+
? String((payload as Record<string, unknown>).error)
|
|
389
|
+
: typeof payload === "string"
|
|
390
|
+
? payload
|
|
391
|
+
: `Preview request failed (${response.status})`;
|
|
392
|
+
throw new BanataError(message, response.status);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return payload as T;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function shouldFallbackFromDirectConnection(error: unknown): boolean {
|
|
399
|
+
if (error instanceof BanataError) {
|
|
400
|
+
return error.status === 0 || error.status >= 500;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (error instanceof Error) {
|
|
404
|
+
const maybeCause = error.cause as
|
|
405
|
+
| { code?: string; name?: string }
|
|
406
|
+
| undefined;
|
|
407
|
+
const code = maybeCause?.code;
|
|
408
|
+
return (
|
|
409
|
+
code === "ECONNRESET" ||
|
|
410
|
+
code === "ECONNREFUSED" ||
|
|
411
|
+
code === "ENOTFOUND" ||
|
|
412
|
+
code === "ETIMEDOUT" ||
|
|
413
|
+
code === "UND_ERR_CONNECT_TIMEOUT" ||
|
|
414
|
+
error.name === "TypeError"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function parseSseBlock(block: string): { type: string; data: JsonValue | string | null; raw: string } | null {
|
|
422
|
+
const trimmed = block.trim();
|
|
423
|
+
if (!trimmed) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let eventType = "message";
|
|
428
|
+
const dataLines: string[] = [];
|
|
429
|
+
|
|
430
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
431
|
+
if (line.startsWith("event:")) {
|
|
432
|
+
eventType = line.slice("event:".length).trim() || "message";
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (line.startsWith("data:")) {
|
|
436
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const rawData = dataLines.join("\n");
|
|
441
|
+
if (!rawData) {
|
|
442
|
+
return { type: eventType, data: null, raw: trimmed };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
return {
|
|
447
|
+
type: eventType,
|
|
448
|
+
data: JSON.parse(rawData) as JsonValue,
|
|
449
|
+
raw: trimmed,
|
|
450
|
+
};
|
|
451
|
+
} catch {
|
|
452
|
+
return {
|
|
453
|
+
type: eventType,
|
|
454
|
+
data: rawData,
|
|
455
|
+
raw: trimmed,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function isBrowserSessionOperationallyReady(
|
|
461
|
+
session: BrowserSession,
|
|
462
|
+
): session is BrowserSession & { cdpUrl: string } {
|
|
463
|
+
return (
|
|
464
|
+
(session.status === "ready" || session.status === "active") &&
|
|
465
|
+
Boolean(session.cdpUrl)
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isSandboxSessionOperationallyReady(
|
|
470
|
+
session: SandboxSession,
|
|
471
|
+
): boolean {
|
|
472
|
+
if (session.status !== "ready" && session.status !== "active") {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const browserMode = session.capabilities?.browser?.mode ?? "none";
|
|
477
|
+
if (browserMode !== "none") {
|
|
478
|
+
if (!session.pairedBrowser?.cdpUrl) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (session.capabilities?.opencode?.enabled) {
|
|
484
|
+
if (
|
|
485
|
+
!session.opencode ||
|
|
486
|
+
session.opencode.status === "failed" ||
|
|
487
|
+
session.opencode.status === "disabled"
|
|
488
|
+
) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
192
496
|
export class BrowserCloud {
|
|
193
497
|
private apiKey: string;
|
|
194
498
|
private baseUrl: string;
|
|
499
|
+
private appUrl: string;
|
|
195
500
|
private retryConfig: Required<RetryConfig>;
|
|
196
501
|
|
|
197
|
-
constructor(config: { apiKey: string; baseUrl?: string; retry?: RetryConfig }) {
|
|
502
|
+
constructor(config: { apiKey: string; baseUrl?: string; appUrl?: string; retry?: RetryConfig }) {
|
|
198
503
|
if (!config.apiKey) throw new Error('API key is required');
|
|
199
504
|
this.apiKey = config.apiKey;
|
|
200
|
-
this.baseUrl = config.baseUrl ?? 'https://api.banata.dev';
|
|
505
|
+
this.baseUrl = config.baseUrl ?? 'https://api.boxes.banata.dev';
|
|
506
|
+
this.appUrl = config.appUrl ?? 'https://boxes.banata.dev';
|
|
201
507
|
this.retryConfig = {
|
|
202
508
|
maxRetries: config.retry?.maxRetries ?? 3,
|
|
203
509
|
baseDelayMs: config.retry?.baseDelayMs ?? 500,
|
|
@@ -294,12 +600,13 @@ export class BrowserCloud {
|
|
|
294
600
|
/** Create a new browser session */
|
|
295
601
|
async createBrowser(config: BrowserConfig = {}): Promise<BrowserSession> {
|
|
296
602
|
const { timeout, waitTimeoutMs, ...rest } = config;
|
|
603
|
+
const requestBody: Record<string, unknown> = { ...rest };
|
|
604
|
+
if (waitTimeoutMs !== undefined) {
|
|
605
|
+
requestBody.waitTimeoutMs = waitTimeoutMs;
|
|
606
|
+
}
|
|
297
607
|
return this.request<BrowserSession>('/v1/browsers', {
|
|
298
608
|
method: 'POST',
|
|
299
|
-
body: JSON.stringify(
|
|
300
|
-
...rest,
|
|
301
|
-
waitTimeoutMs: waitTimeoutMs ?? timeout,
|
|
302
|
-
}),
|
|
609
|
+
body: JSON.stringify(requestBody),
|
|
303
610
|
});
|
|
304
611
|
}
|
|
305
612
|
|
|
@@ -317,6 +624,60 @@ export class BrowserCloud {
|
|
|
317
624
|
});
|
|
318
625
|
}
|
|
319
626
|
|
|
627
|
+
/** Derive the preview websocket/start endpoints for a live browser session. */
|
|
628
|
+
async getPreviewConnection(sessionId: string): Promise<PreviewConnectionInfo | null> {
|
|
629
|
+
const session = await this.getBrowser(sessionId);
|
|
630
|
+
return derivePreviewConnection(session.cdpUrl);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async getPreviewViewerUrl(sessionId: string): Promise<string | null> {
|
|
634
|
+
const session = await this.getBrowser(sessionId);
|
|
635
|
+
return buildPreviewViewerUrl(session.cdpUrl, this.appUrl);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Start the remote browser preview backend for a live browser session. */
|
|
639
|
+
async startPreview(sessionId: string): Promise<PreviewCommandResponse> {
|
|
640
|
+
const connection = await this.getPreviewConnection(sessionId);
|
|
641
|
+
if (!connection) {
|
|
642
|
+
throw new BanataError("Browser preview is not available for this session", 409);
|
|
643
|
+
}
|
|
644
|
+
return requestPreviewEndpoint<PreviewCommandResponse>(connection.startUrl, {
|
|
645
|
+
method: "POST",
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Navigate the live browser preview session. */
|
|
650
|
+
async navigatePreview(
|
|
651
|
+
sessionId: string,
|
|
652
|
+
url: string,
|
|
653
|
+
): Promise<PreviewCommandResponse> {
|
|
654
|
+
const connection = await this.getPreviewConnection(sessionId);
|
|
655
|
+
if (!connection) {
|
|
656
|
+
throw new BanataError("Browser preview is not available for this session", 409);
|
|
657
|
+
}
|
|
658
|
+
return requestPreviewEndpoint<PreviewCommandResponse>(connection.navigateUrl, {
|
|
659
|
+
method: "POST",
|
|
660
|
+
headers: { "Content-Type": "application/json" },
|
|
661
|
+
body: JSON.stringify({ url }),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Resize the live browser preview session if the preview backend supports it. */
|
|
666
|
+
async resizePreview(
|
|
667
|
+
sessionId: string,
|
|
668
|
+
size: { width?: number; height?: number },
|
|
669
|
+
): Promise<PreviewCommandResponse> {
|
|
670
|
+
const connection = await this.getPreviewConnection(sessionId);
|
|
671
|
+
if (!connection) {
|
|
672
|
+
throw new BanataError("Browser preview is not available for this session", 409);
|
|
673
|
+
}
|
|
674
|
+
return requestPreviewEndpoint<PreviewCommandResponse>(connection.resizeUrl, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "Content-Type": "application/json" },
|
|
677
|
+
body: JSON.stringify(size),
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
320
681
|
/** Wait until session is ready and return CDP URL */
|
|
321
682
|
async waitForReady(
|
|
322
683
|
sessionId: string,
|
|
@@ -327,7 +688,7 @@ export class BrowserCloud {
|
|
|
327
688
|
while (Date.now() - start < timeoutMs) {
|
|
328
689
|
const session = await this.getBrowser(sessionId);
|
|
329
690
|
|
|
330
|
-
if ((session
|
|
691
|
+
if (isBrowserSessionOperationallyReady(session)) {
|
|
331
692
|
return session.cdpUrl;
|
|
332
693
|
}
|
|
333
694
|
if (session.status === 'failed') {
|
|
@@ -361,32 +722,38 @@ export class BrowserCloud {
|
|
|
361
722
|
*/
|
|
362
723
|
async launch(
|
|
363
724
|
config: BrowserConfig = {}
|
|
364
|
-
): Promise<{
|
|
365
|
-
cdpUrl: string;
|
|
366
|
-
sessionId: string;
|
|
367
|
-
close: () => Promise<void>;
|
|
368
|
-
}> {
|
|
725
|
+
): Promise<LaunchedBrowserSession> {
|
|
369
726
|
const session = await this.createBrowser(config);
|
|
370
727
|
let cdpUrl: string;
|
|
371
|
-
|
|
372
|
-
cdpUrl =
|
|
373
|
-
|
|
374
|
-
config.timeout ?? 30_000
|
|
375
|
-
);
|
|
376
|
-
} catch (err) {
|
|
377
|
-
// Clean up the orphaned session on timeout/failure
|
|
728
|
+
if (isBrowserSessionOperationallyReady(session)) {
|
|
729
|
+
cdpUrl = session.cdpUrl;
|
|
730
|
+
} else {
|
|
378
731
|
try {
|
|
379
|
-
await this.
|
|
380
|
-
|
|
381
|
-
|
|
732
|
+
cdpUrl = await this.waitForReady(
|
|
733
|
+
session.id,
|
|
734
|
+
config.timeout ?? 30_000
|
|
735
|
+
);
|
|
736
|
+
} catch (err) {
|
|
737
|
+
try {
|
|
738
|
+
await this.closeBrowser(session.id);
|
|
739
|
+
} catch {
|
|
740
|
+
// Best-effort cleanup — session will be cleaned by stale cron
|
|
741
|
+
}
|
|
742
|
+
throw err;
|
|
382
743
|
}
|
|
383
|
-
throw err;
|
|
384
744
|
}
|
|
385
745
|
|
|
386
746
|
return {
|
|
387
747
|
cdpUrl,
|
|
388
748
|
sessionId: session.id,
|
|
749
|
+
previewViewerUrl: buildPreviewViewerUrl(cdpUrl, this.appUrl),
|
|
389
750
|
close: () => this.closeBrowser(session.id),
|
|
751
|
+
getPreviewConnection: () => this.getPreviewConnection(session.id),
|
|
752
|
+
getPreviewViewerUrl: () => this.getPreviewViewerUrl(session.id),
|
|
753
|
+
startPreview: () => this.startPreview(session.id),
|
|
754
|
+
navigatePreview: (url: string) => this.navigatePreview(session.id, url),
|
|
755
|
+
resizePreview: (size: { width?: number; height?: number }) =>
|
|
756
|
+
this.resizePreview(session.id, size),
|
|
390
757
|
};
|
|
391
758
|
}
|
|
392
759
|
|
|
@@ -456,8 +823,8 @@ export class BrowserCloud {
|
|
|
456
823
|
export interface SandboxConfig {
|
|
457
824
|
/** Sandbox runtime: bun, python, or base shell */
|
|
458
825
|
runtime?: 'bun' | 'python' | 'base';
|
|
459
|
-
/** Sandbox
|
|
460
|
-
size?: '
|
|
826
|
+
/** Sandbox compute shape. */
|
|
827
|
+
size?: 'standard';
|
|
461
828
|
/** Environment variables to inject into the sandbox */
|
|
462
829
|
env?: Record<string, string>;
|
|
463
830
|
/** Whether sandbox is ephemeral (destroyed on kill). Default: true */
|
|
@@ -472,7 +839,7 @@ export interface SandboxConfig {
|
|
|
472
839
|
waitUntilReady?: boolean;
|
|
473
840
|
/** Max time in ms for the API to wait before falling back to a queued response */
|
|
474
841
|
waitTimeoutMs?: number;
|
|
475
|
-
/** Timeout in ms to wait for sandbox to become ready (default:
|
|
842
|
+
/** Timeout in ms to wait for sandbox to become ready (default: 120000) */
|
|
476
843
|
timeout?: number;
|
|
477
844
|
}
|
|
478
845
|
|
|
@@ -488,6 +855,10 @@ export interface SandboxCapabilities {
|
|
|
488
855
|
persistentProfile?: boolean;
|
|
489
856
|
streamPreview?: boolean;
|
|
490
857
|
humanInLoop?: boolean;
|
|
858
|
+
viewport?: {
|
|
859
|
+
width?: number;
|
|
860
|
+
height?: number;
|
|
861
|
+
};
|
|
491
862
|
};
|
|
492
863
|
documents?: {
|
|
493
864
|
libreofficeHeadless?: boolean;
|
|
@@ -561,6 +932,17 @@ export interface SandboxBrowserPreviewState {
|
|
|
561
932
|
updatedAt?: number;
|
|
562
933
|
}
|
|
563
934
|
|
|
935
|
+
export interface SandboxTaskReport {
|
|
936
|
+
result: string;
|
|
937
|
+
report: string;
|
|
938
|
+
url?: string;
|
|
939
|
+
stepsCompleted?: number;
|
|
940
|
+
actions?: string[];
|
|
941
|
+
durationMs?: number;
|
|
942
|
+
usage?: JsonValue;
|
|
943
|
+
completedAt: number;
|
|
944
|
+
}
|
|
945
|
+
|
|
564
946
|
export type SandboxHumanHandoffReason =
|
|
565
947
|
| "mfa"
|
|
566
948
|
| "captcha"
|
|
@@ -598,7 +980,7 @@ export interface SandboxSession {
|
|
|
598
980
|
status: 'queued' | 'assigning' | 'ready' | 'active' | 'ending' | 'ended' | 'failed' | 'pausing' | 'paused';
|
|
599
981
|
waitTimedOut?: boolean;
|
|
600
982
|
runtime: 'bun' | 'python' | 'base';
|
|
601
|
-
size: '
|
|
983
|
+
size: 'standard';
|
|
602
984
|
region?: string | null;
|
|
603
985
|
terminalUrl: string | null;
|
|
604
986
|
previewBaseUrl?: string | null;
|
|
@@ -625,6 +1007,7 @@ export interface SandboxWorkerRuntimeState {
|
|
|
625
1007
|
pairedBrowser: SandboxPairedBrowser | null;
|
|
626
1008
|
artifacts: SandboxArtifacts | null;
|
|
627
1009
|
humanHandoff: SandboxHumanHandoffState | null;
|
|
1010
|
+
lastTaskReport?: SandboxTaskReport | null;
|
|
628
1011
|
}
|
|
629
1012
|
|
|
630
1013
|
export interface SandboxOpencodePromptResponse {
|
|
@@ -652,6 +1035,23 @@ export interface SandboxBrowserControlResponse {
|
|
|
652
1035
|
pairedBrowser: SandboxPairedBrowser | null;
|
|
653
1036
|
}
|
|
654
1037
|
|
|
1038
|
+
export interface SandboxOpencodeStateResponse {
|
|
1039
|
+
ok: boolean;
|
|
1040
|
+
opencode: SandboxOpencodeState | null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export interface SandboxOpencodeMessagesResponse {
|
|
1044
|
+
ok: boolean;
|
|
1045
|
+
sessionId: string | null;
|
|
1046
|
+
messages: JsonValue;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export interface SandboxOpencodeStreamEvent {
|
|
1050
|
+
type: string;
|
|
1051
|
+
data: JsonValue | string | null;
|
|
1052
|
+
raw: string;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
655
1055
|
export interface SandboxHumanHandoffResponse {
|
|
656
1056
|
ok: boolean;
|
|
657
1057
|
handoff: SandboxHumanHandoffState;
|
|
@@ -694,11 +1094,35 @@ export interface LaunchedSandbox {
|
|
|
694
1094
|
noReply?: boolean;
|
|
695
1095
|
},
|
|
696
1096
|
) => Promise<SandboxOpencodePromptResponse>;
|
|
1097
|
+
promptAsync: (
|
|
1098
|
+
prompt: string,
|
|
1099
|
+
options?: {
|
|
1100
|
+
agent?: "build" | "plan";
|
|
1101
|
+
sessionId?: string;
|
|
1102
|
+
},
|
|
1103
|
+
) => Promise<SandboxOpencodePromptResponse>;
|
|
1104
|
+
getOpencodeState: (options?: {
|
|
1105
|
+
ensureSession?: boolean;
|
|
1106
|
+
agent?: "build" | "plan";
|
|
1107
|
+
}) => Promise<SandboxOpencodeStateResponse>;
|
|
1108
|
+
getOpencodeConnection: () => Promise<OpenCodeConnectionInfo | null>;
|
|
1109
|
+
listOpencodeMessages: (options?: {
|
|
1110
|
+
sessionId?: string;
|
|
1111
|
+
}) => Promise<SandboxOpencodeMessagesResponse>;
|
|
1112
|
+
streamOpencodeEvents: (options?: {
|
|
1113
|
+
sessionId?: string;
|
|
1114
|
+
}) => AsyncGenerator<SandboxOpencodeStreamEvent, void, void>;
|
|
697
1115
|
checkpoint: () => Promise<SandboxCheckpointResponse>;
|
|
698
1116
|
getRuntime: () => Promise<SandboxRuntimeState>;
|
|
699
1117
|
getPreview: () => Promise<SandboxBrowserPreviewInfo>;
|
|
1118
|
+
getPreviewConnection: () => Promise<PreviewConnectionInfo | null>;
|
|
1119
|
+
getPreviewViewerUrl: () => Promise<string | null>;
|
|
1120
|
+
startPreview: () => Promise<PreviewCommandResponse<SandboxBrowserPreviewState>>;
|
|
1121
|
+
navigatePreview: (url: string) => Promise<PreviewCommandResponse<SandboxBrowserPreviewState>>;
|
|
1122
|
+
resizePreview: (size: { width?: number; height?: number }) => Promise<PreviewCommandResponse<SandboxBrowserPreviewState>>;
|
|
700
1123
|
getHandoff: () => Promise<SandboxHumanHandoffInfo>;
|
|
701
1124
|
browserPreviewUrl: string | null;
|
|
1125
|
+
browserPreviewViewerUrl: string | null;
|
|
702
1126
|
setControl: (
|
|
703
1127
|
mode: "ai" | "human" | "shared",
|
|
704
1128
|
options?: { controller?: string; leaseMs?: number },
|
|
@@ -745,12 +1169,14 @@ export interface SandboxRuntimeState {
|
|
|
745
1169
|
browserPreview: SandboxBrowserPreviewState | null;
|
|
746
1170
|
humanHandoff: SandboxHumanHandoffState | null;
|
|
747
1171
|
artifacts: SandboxArtifacts | null;
|
|
1172
|
+
lastTaskReport?: SandboxTaskReport | null;
|
|
748
1173
|
runtime: SandboxWorkerRuntimeState | null;
|
|
749
1174
|
}
|
|
750
1175
|
|
|
751
1176
|
export interface SandboxBrowserPreviewInfo {
|
|
752
1177
|
id: string;
|
|
753
1178
|
browserPreviewUrl: string | null;
|
|
1179
|
+
browserPreviewViewerUrl: string | null;
|
|
754
1180
|
previewBaseUrl: string | null;
|
|
755
1181
|
browserPreview: SandboxBrowserPreviewState | null;
|
|
756
1182
|
humanHandoff: SandboxHumanHandoffState | null;
|
|
@@ -778,12 +1204,14 @@ export interface SandboxHumanHandoffInfo {
|
|
|
778
1204
|
export class BanataSandbox {
|
|
779
1205
|
private apiKey: string;
|
|
780
1206
|
private baseUrl: string;
|
|
1207
|
+
private appUrl: string;
|
|
781
1208
|
private retryConfig: Required<RetryConfig>;
|
|
782
1209
|
|
|
783
|
-
constructor(config: { apiKey: string; baseUrl?: string; retry?: RetryConfig }) {
|
|
1210
|
+
constructor(config: { apiKey: string; baseUrl?: string; appUrl?: string; retry?: RetryConfig }) {
|
|
784
1211
|
if (!config.apiKey) throw new Error('API key is required');
|
|
785
1212
|
this.apiKey = config.apiKey;
|
|
786
|
-
this.baseUrl = config.baseUrl ?? 'https://api.banata.dev';
|
|
1213
|
+
this.baseUrl = config.baseUrl ?? 'https://api.boxes.banata.dev';
|
|
1214
|
+
this.appUrl = config.appUrl ?? 'https://boxes.banata.dev';
|
|
787
1215
|
this.retryConfig = {
|
|
788
1216
|
maxRetries: config.retry?.maxRetries ?? 3,
|
|
789
1217
|
baseDelayMs: config.retry?.baseDelayMs ?? 500,
|
|
@@ -879,12 +1307,13 @@ export class BanataSandbox {
|
|
|
879
1307
|
/** Create a new sandbox session */
|
|
880
1308
|
async create(config: SandboxConfig = {}): Promise<SandboxSession> {
|
|
881
1309
|
const { timeout, waitTimeoutMs, ...rest } = config;
|
|
1310
|
+
const requestBody: Record<string, unknown> = { ...rest };
|
|
1311
|
+
if (waitTimeoutMs !== undefined) {
|
|
1312
|
+
requestBody.waitTimeoutMs = waitTimeoutMs;
|
|
1313
|
+
}
|
|
882
1314
|
return this.request<SandboxSession>('/v1/sandboxes', {
|
|
883
1315
|
method: 'POST',
|
|
884
|
-
body: JSON.stringify(
|
|
885
|
-
...rest,
|
|
886
|
-
waitTimeoutMs: waitTimeoutMs ?? timeout,
|
|
887
|
-
}),
|
|
1316
|
+
body: JSON.stringify(requestBody),
|
|
888
1317
|
});
|
|
889
1318
|
}
|
|
890
1319
|
|
|
@@ -911,14 +1340,14 @@ export class BanataSandbox {
|
|
|
911
1340
|
/** Wait until sandbox is ready and return the session */
|
|
912
1341
|
async waitForReady(
|
|
913
1342
|
id: string,
|
|
914
|
-
timeoutMs: number =
|
|
1343
|
+
timeoutMs: number = 120_000
|
|
915
1344
|
): Promise<SandboxSession> {
|
|
916
1345
|
const start = Date.now();
|
|
917
1346
|
|
|
918
1347
|
while (Date.now() - start < timeoutMs) {
|
|
919
1348
|
const session = await this.get(id);
|
|
920
1349
|
|
|
921
|
-
if (session
|
|
1350
|
+
if (isSandboxSessionOperationallyReady(session)) {
|
|
922
1351
|
return session;
|
|
923
1352
|
}
|
|
924
1353
|
if (session.status === 'failed') {
|
|
@@ -1013,6 +1442,180 @@ export class BanataSandbox {
|
|
|
1013
1442
|
);
|
|
1014
1443
|
}
|
|
1015
1444
|
|
|
1445
|
+
async getOpencodeState(
|
|
1446
|
+
id: string,
|
|
1447
|
+
options: {
|
|
1448
|
+
ensureSession?: boolean;
|
|
1449
|
+
agent?: "build" | "plan";
|
|
1450
|
+
} = {},
|
|
1451
|
+
): Promise<SandboxOpencodeStateResponse> {
|
|
1452
|
+
const connection = await this.getOpencodeConnection(id);
|
|
1453
|
+
if (connection) {
|
|
1454
|
+
try {
|
|
1455
|
+
const url = new URL(connection.stateUrl);
|
|
1456
|
+
if (options.ensureSession !== undefined) {
|
|
1457
|
+
url.searchParams.set("ensureSession", String(options.ensureSession));
|
|
1458
|
+
}
|
|
1459
|
+
if (options.agent) {
|
|
1460
|
+
url.searchParams.set("agent", options.agent);
|
|
1461
|
+
}
|
|
1462
|
+
return await requestPreviewEndpoint<SandboxOpencodeStateResponse>(url.toString(), {
|
|
1463
|
+
method: "GET",
|
|
1464
|
+
});
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
1467
|
+
throw error;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const query = new URLSearchParams({ id });
|
|
1473
|
+
if (options.ensureSession !== undefined) {
|
|
1474
|
+
query.set("ensureSession", String(options.ensureSession));
|
|
1475
|
+
}
|
|
1476
|
+
if (options.agent) {
|
|
1477
|
+
query.set("agent", options.agent);
|
|
1478
|
+
}
|
|
1479
|
+
return this.request<SandboxOpencodeStateResponse>(
|
|
1480
|
+
`/v1/sandboxes/opencode/state?${query.toString()}`,
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
async getOpencodeConnection(id: string): Promise<OpenCodeConnectionInfo | null> {
|
|
1485
|
+
const preview = await this.getPreview(id);
|
|
1486
|
+
return deriveOpenCodeConnection(
|
|
1487
|
+
preview.browserPreviewUrl ??
|
|
1488
|
+
preview.browserPreview?.publicUrl ??
|
|
1489
|
+
preview.pairedBrowser?.previewUrl ??
|
|
1490
|
+
null,
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async listOpencodeMessages(
|
|
1495
|
+
id: string,
|
|
1496
|
+
options: {
|
|
1497
|
+
sessionId?: string;
|
|
1498
|
+
} = {},
|
|
1499
|
+
): Promise<SandboxOpencodeMessagesResponse> {
|
|
1500
|
+
const connection = await this.getOpencodeConnection(id);
|
|
1501
|
+
if (connection) {
|
|
1502
|
+
try {
|
|
1503
|
+
const url = new URL(connection.messagesUrl);
|
|
1504
|
+
if (options.sessionId) {
|
|
1505
|
+
url.searchParams.set("opencodeSessionId", options.sessionId);
|
|
1506
|
+
}
|
|
1507
|
+
return await requestPreviewEndpoint<SandboxOpencodeMessagesResponse>(url.toString(), {
|
|
1508
|
+
method: "GET",
|
|
1509
|
+
});
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
1512
|
+
throw error;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const query = new URLSearchParams({ id });
|
|
1518
|
+
if (options.sessionId) {
|
|
1519
|
+
query.set("sessionId", options.sessionId);
|
|
1520
|
+
}
|
|
1521
|
+
return this.request<SandboxOpencodeMessagesResponse>(
|
|
1522
|
+
`/v1/sandboxes/opencode/messages?${query.toString()}`,
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async *streamOpencodeEvents(
|
|
1527
|
+
id: string,
|
|
1528
|
+
options: {
|
|
1529
|
+
sessionId?: string;
|
|
1530
|
+
} = {},
|
|
1531
|
+
): AsyncGenerator<SandboxOpencodeStreamEvent, void, void> {
|
|
1532
|
+
const connection = await this.getOpencodeConnection(id);
|
|
1533
|
+
let response: Response;
|
|
1534
|
+
if (connection) {
|
|
1535
|
+
try {
|
|
1536
|
+
const url = new URL(connection.eventsUrl);
|
|
1537
|
+
if (options.sessionId) {
|
|
1538
|
+
url.searchParams.set("opencodeSessionId", options.sessionId);
|
|
1539
|
+
}
|
|
1540
|
+
response = await fetch(url.toString(), {
|
|
1541
|
+
method: "GET",
|
|
1542
|
+
});
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
1545
|
+
throw error;
|
|
1546
|
+
}
|
|
1547
|
+
const query = new URLSearchParams({ id });
|
|
1548
|
+
if (options.sessionId) {
|
|
1549
|
+
query.set("sessionId", options.sessionId);
|
|
1550
|
+
}
|
|
1551
|
+
response = await fetch(
|
|
1552
|
+
`${this.baseUrl}/v1/sandboxes/opencode/events?${query.toString()}`,
|
|
1553
|
+
{
|
|
1554
|
+
method: "GET",
|
|
1555
|
+
headers: this.headers,
|
|
1556
|
+
},
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
} else {
|
|
1560
|
+
const query = new URLSearchParams({ id });
|
|
1561
|
+
if (options.sessionId) {
|
|
1562
|
+
query.set("sessionId", options.sessionId);
|
|
1563
|
+
}
|
|
1564
|
+
response = await fetch(
|
|
1565
|
+
`${this.baseUrl}/v1/sandboxes/opencode/events?${query.toString()}`,
|
|
1566
|
+
{
|
|
1567
|
+
method: "GET",
|
|
1568
|
+
headers: this.headers,
|
|
1569
|
+
},
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (!response.ok) {
|
|
1574
|
+
const payload = await parseJsonResponse(response);
|
|
1575
|
+
const message =
|
|
1576
|
+
payload && typeof payload === "object" && !Array.isArray(payload) && typeof (payload as Record<string, unknown>).error === "string"
|
|
1577
|
+
? String((payload as Record<string, unknown>).error)
|
|
1578
|
+
: typeof payload === "string"
|
|
1579
|
+
? payload
|
|
1580
|
+
: `Failed to open OpenCode event stream (${response.status})`;
|
|
1581
|
+
throw new BanataError(message, response.status);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
if (!response.body) {
|
|
1585
|
+
throw new BanataError("OpenCode event stream did not provide a response body", 502);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const reader = response.body.getReader();
|
|
1589
|
+
const decoder = new TextDecoder();
|
|
1590
|
+
let buffered = "";
|
|
1591
|
+
|
|
1592
|
+
try {
|
|
1593
|
+
while (true) {
|
|
1594
|
+
const { value, done } = await reader.read();
|
|
1595
|
+
if (done) break;
|
|
1596
|
+
|
|
1597
|
+
buffered += decoder.decode(value, { stream: true });
|
|
1598
|
+
const blocks = buffered.split(/\r?\n\r?\n/);
|
|
1599
|
+
buffered = blocks.pop() ?? "";
|
|
1600
|
+
|
|
1601
|
+
for (const block of blocks) {
|
|
1602
|
+
const parsed = parseSseBlock(block);
|
|
1603
|
+
if (parsed) {
|
|
1604
|
+
yield parsed;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
buffered += decoder.decode();
|
|
1610
|
+
const trailing = parseSseBlock(buffered);
|
|
1611
|
+
if (trailing) {
|
|
1612
|
+
yield trailing;
|
|
1613
|
+
}
|
|
1614
|
+
} finally {
|
|
1615
|
+
reader.releaseLock();
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1016
1619
|
async prompt(
|
|
1017
1620
|
id: string,
|
|
1018
1621
|
prompt: string,
|
|
@@ -1022,6 +1625,25 @@ export class BanataSandbox {
|
|
|
1022
1625
|
noReply?: boolean;
|
|
1023
1626
|
} = {},
|
|
1024
1627
|
): Promise<SandboxOpencodePromptResponse> {
|
|
1628
|
+
const connection = await this.getOpencodeConnection(id);
|
|
1629
|
+
if (connection) {
|
|
1630
|
+
try {
|
|
1631
|
+
return await requestPreviewEndpoint<SandboxOpencodePromptResponse>(connection.promptUrl, {
|
|
1632
|
+
method: "POST",
|
|
1633
|
+
headers: { "Content-Type": "application/json" },
|
|
1634
|
+
body: JSON.stringify({
|
|
1635
|
+
prompt,
|
|
1636
|
+
agent: options.agent,
|
|
1637
|
+
sessionId: options.sessionId,
|
|
1638
|
+
}),
|
|
1639
|
+
});
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
1642
|
+
throw error;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1025
1647
|
return this.request<SandboxOpencodePromptResponse>('/v1/sandboxes/opencode/prompt', {
|
|
1026
1648
|
method: 'POST',
|
|
1027
1649
|
body: JSON.stringify({
|
|
@@ -1034,6 +1656,44 @@ export class BanataSandbox {
|
|
|
1034
1656
|
});
|
|
1035
1657
|
}
|
|
1036
1658
|
|
|
1659
|
+
async promptAsync(
|
|
1660
|
+
id: string,
|
|
1661
|
+
prompt: string,
|
|
1662
|
+
options: {
|
|
1663
|
+
agent?: "build" | "plan";
|
|
1664
|
+
sessionId?: string;
|
|
1665
|
+
} = {},
|
|
1666
|
+
): Promise<SandboxOpencodePromptResponse> {
|
|
1667
|
+
const connection = await this.getOpencodeConnection(id);
|
|
1668
|
+
if (connection) {
|
|
1669
|
+
try {
|
|
1670
|
+
return await requestPreviewEndpoint<SandboxOpencodePromptResponse>(connection.promptUrl, {
|
|
1671
|
+
method: "POST",
|
|
1672
|
+
headers: { "Content-Type": "application/json" },
|
|
1673
|
+
body: JSON.stringify({
|
|
1674
|
+
prompt,
|
|
1675
|
+
agent: options.agent,
|
|
1676
|
+
sessionId: options.sessionId,
|
|
1677
|
+
}),
|
|
1678
|
+
});
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
1681
|
+
throw error;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
return this.request<SandboxOpencodePromptResponse>('/v1/sandboxes/opencode/prompt-async', {
|
|
1687
|
+
method: 'POST',
|
|
1688
|
+
body: JSON.stringify({
|
|
1689
|
+
id,
|
|
1690
|
+
prompt,
|
|
1691
|
+
agent: options.agent,
|
|
1692
|
+
sessionId: options.sessionId,
|
|
1693
|
+
}),
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1037
1697
|
async checkpoint(id: string): Promise<SandboxCheckpointResponse> {
|
|
1038
1698
|
return this.request<SandboxCheckpointResponse>('/v1/sandboxes/checkpoint', {
|
|
1039
1699
|
method: 'POST',
|
|
@@ -1058,9 +1718,69 @@ export class BanataSandbox {
|
|
|
1058
1718
|
}
|
|
1059
1719
|
|
|
1060
1720
|
async getPreview(id: string): Promise<SandboxBrowserPreviewInfo> {
|
|
1061
|
-
|
|
1721
|
+
const preview = await this.request<SandboxBrowserPreviewInfo>(
|
|
1062
1722
|
`/v1/sandboxes/browser-preview?id=${encodeURIComponent(id)}`
|
|
1063
1723
|
);
|
|
1724
|
+
return {
|
|
1725
|
+
...preview,
|
|
1726
|
+
browserPreviewViewerUrl: buildPreviewViewerUrl(
|
|
1727
|
+
preview.browserPreviewUrl,
|
|
1728
|
+
this.appUrl,
|
|
1729
|
+
),
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
async getPreviewConnection(id: string): Promise<PreviewConnectionInfo | null> {
|
|
1734
|
+
const preview = await this.getPreview(id);
|
|
1735
|
+
return derivePreviewConnection(preview.browserPreviewUrl);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
async getPreviewViewerUrl(id: string): Promise<string | null> {
|
|
1739
|
+
const preview = await this.getPreview(id);
|
|
1740
|
+
return (
|
|
1741
|
+
preview.browserPreviewViewerUrl ??
|
|
1742
|
+
buildPreviewViewerUrl(preview.browserPreviewUrl, this.appUrl)
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
async startPreview(id: string): Promise<PreviewCommandResponse<SandboxBrowserPreviewState>> {
|
|
1747
|
+
const connection = await this.getPreviewConnection(id);
|
|
1748
|
+
if (!connection) {
|
|
1749
|
+
throw new BanataError("Sandbox browser preview is not available", 409);
|
|
1750
|
+
}
|
|
1751
|
+
return requestPreviewEndpoint<PreviewCommandResponse<SandboxBrowserPreviewState>>(connection.startUrl, {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
async navigatePreview(
|
|
1757
|
+
id: string,
|
|
1758
|
+
url: string,
|
|
1759
|
+
): Promise<PreviewCommandResponse<SandboxBrowserPreviewState>> {
|
|
1760
|
+
const connection = await this.getPreviewConnection(id);
|
|
1761
|
+
if (!connection) {
|
|
1762
|
+
throw new BanataError("Sandbox browser preview is not available", 409);
|
|
1763
|
+
}
|
|
1764
|
+
return requestPreviewEndpoint<PreviewCommandResponse<SandboxBrowserPreviewState>>(connection.navigateUrl, {
|
|
1765
|
+
method: "POST",
|
|
1766
|
+
headers: { "Content-Type": "application/json" },
|
|
1767
|
+
body: JSON.stringify({ url }),
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
async resizePreview(
|
|
1772
|
+
id: string,
|
|
1773
|
+
size: { width?: number; height?: number },
|
|
1774
|
+
): Promise<PreviewCommandResponse<SandboxBrowserPreviewState>> {
|
|
1775
|
+
const connection = await this.getPreviewConnection(id);
|
|
1776
|
+
if (!connection) {
|
|
1777
|
+
throw new BanataError("Sandbox browser preview is not available", 409);
|
|
1778
|
+
}
|
|
1779
|
+
return requestPreviewEndpoint<PreviewCommandResponse<SandboxBrowserPreviewState>>(connection.resizeUrl, {
|
|
1780
|
+
method: "POST",
|
|
1781
|
+
headers: { "Content-Type": "application/json" },
|
|
1782
|
+
body: JSON.stringify(size),
|
|
1783
|
+
});
|
|
1064
1784
|
}
|
|
1065
1785
|
|
|
1066
1786
|
async getHandoff(id: string): Promise<SandboxHumanHandoffInfo> {
|
|
@@ -1148,7 +1868,7 @@ export class BanataSandbox {
|
|
|
1148
1868
|
* Usage:
|
|
1149
1869
|
* ```ts
|
|
1150
1870
|
* const sandbox = new BanataSandbox({ apiKey: '...' });
|
|
1151
|
-
* const session = await sandbox.launch({ runtime: 'bun', size: '
|
|
1871
|
+
* const session = await sandbox.launch({ runtime: 'bun', size: 'standard' });
|
|
1152
1872
|
*
|
|
1153
1873
|
* const result = await session.exec('echo', ['Hello!']);
|
|
1154
1874
|
* console.log(result.stdout);
|
|
@@ -1160,40 +1880,65 @@ export class BanataSandbox {
|
|
|
1160
1880
|
const created = await this.create(config);
|
|
1161
1881
|
let session: SandboxSession;
|
|
1162
1882
|
|
|
1163
|
-
|
|
1164
|
-
session =
|
|
1165
|
-
|
|
1166
|
-
config.timeout ?? 30_000
|
|
1167
|
-
);
|
|
1168
|
-
} catch (err) {
|
|
1169
|
-
// Clean up the orphaned sandbox on timeout/failure
|
|
1883
|
+
if (isSandboxSessionOperationallyReady(created)) {
|
|
1884
|
+
session = created;
|
|
1885
|
+
} else {
|
|
1170
1886
|
try {
|
|
1171
|
-
await this.
|
|
1172
|
-
|
|
1173
|
-
|
|
1887
|
+
session = await this.waitForReady(
|
|
1888
|
+
created.id,
|
|
1889
|
+
config.timeout ?? 120_000
|
|
1890
|
+
);
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
// Clean up the orphaned sandbox on timeout/failure
|
|
1893
|
+
try {
|
|
1894
|
+
await this.kill(created.id);
|
|
1895
|
+
} catch {
|
|
1896
|
+
// Best-effort cleanup
|
|
1897
|
+
}
|
|
1898
|
+
throw err;
|
|
1174
1899
|
}
|
|
1175
|
-
throw err;
|
|
1176
1900
|
}
|
|
1177
1901
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1902
|
+
const sessionId = session.id;
|
|
1903
|
+
const terminalUrl = session.terminalUrl ?? '';
|
|
1904
|
+
const browserPreviewUrl =
|
|
1905
|
+
session.browserPreview?.publicUrl ??
|
|
1906
|
+
session.pairedBrowser?.previewUrl ??
|
|
1907
|
+
null;
|
|
1908
|
+
const browserPreviewViewerUrl = buildPreviewViewerUrl(
|
|
1909
|
+
browserPreviewUrl,
|
|
1910
|
+
this.appUrl,
|
|
1911
|
+
);
|
|
1184
1912
|
|
|
1185
1913
|
return {
|
|
1186
1914
|
sessionId,
|
|
1187
1915
|
terminalUrl,
|
|
1188
1916
|
browserPreviewUrl,
|
|
1917
|
+
browserPreviewViewerUrl,
|
|
1189
1918
|
exec: (command: string, args?: string[]) =>
|
|
1190
1919
|
this.exec(sessionId, command, args),
|
|
1191
1920
|
runCode: (code: string) =>
|
|
1192
1921
|
this.runCode(sessionId, code),
|
|
1193
1922
|
prompt: (prompt, options) => this.prompt(sessionId, prompt, options),
|
|
1923
|
+
promptAsync: (prompt, options) =>
|
|
1924
|
+
this.promptAsync(sessionId, prompt, options),
|
|
1925
|
+
getOpencodeState: (options) =>
|
|
1926
|
+
this.getOpencodeState(sessionId, options),
|
|
1927
|
+
getOpencodeConnection: () =>
|
|
1928
|
+
this.getOpencodeConnection(sessionId),
|
|
1929
|
+
listOpencodeMessages: (options) =>
|
|
1930
|
+
this.listOpencodeMessages(sessionId, options),
|
|
1931
|
+
streamOpencodeEvents: (options) =>
|
|
1932
|
+
this.streamOpencodeEvents(sessionId, options),
|
|
1194
1933
|
checkpoint: () => this.checkpoint(sessionId),
|
|
1195
1934
|
getRuntime: () => this.getRuntime(sessionId),
|
|
1196
1935
|
getPreview: () => this.getPreview(sessionId),
|
|
1936
|
+
getPreviewConnection: () => this.getPreviewConnection(sessionId),
|
|
1937
|
+
getPreviewViewerUrl: () => this.getPreviewViewerUrl(sessionId),
|
|
1938
|
+
startPreview: () => this.startPreview(sessionId),
|
|
1939
|
+
navigatePreview: (url: string) => this.navigatePreview(sessionId, url),
|
|
1940
|
+
resizePreview: (size: { width?: number; height?: number }) =>
|
|
1941
|
+
this.resizePreview(sessionId, size),
|
|
1197
1942
|
getHandoff: () => this.getHandoff(sessionId),
|
|
1198
1943
|
setControl: (mode, options) => this.setControl(sessionId, mode, options),
|
|
1199
1944
|
takeControl: (options) => this.setControl(sessionId, "human", options),
|