@banata-boxes/sdk 0.1.0 → 0.1.1
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 +138 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +493 -27
- package/package.json +1 -1
- package/src/index.ts +796 -51
package/dist/index.js
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
function buildPreviewViewerUrl(connectionUrl, appUrl = "https://boxes.banata.dev") {
|
|
3
|
+
if (!connectionUrl) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(connectionUrl);
|
|
8
|
+
const token = parsed.searchParams.get("token");
|
|
9
|
+
const session = parsed.searchParams.get("session");
|
|
10
|
+
const machine = parsed.searchParams.get("machine");
|
|
11
|
+
if (!token || !session) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const backendProtocol = parsed.protocol === "wss:" ? "https:" : parsed.protocol === "ws:" ? "http:" : parsed.protocol;
|
|
15
|
+
const viewerUrl = new URL("/preview", appUrl);
|
|
16
|
+
viewerUrl.searchParams.set("backend", `${backendProtocol}//${parsed.host}`);
|
|
17
|
+
viewerUrl.searchParams.set("token", token);
|
|
18
|
+
viewerUrl.searchParams.set("session", session);
|
|
19
|
+
if (machine) {
|
|
20
|
+
viewerUrl.searchParams.set("machine", machine);
|
|
21
|
+
}
|
|
22
|
+
return viewerUrl.toString();
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
2
28
|
class BanataError extends Error {
|
|
3
29
|
status;
|
|
4
30
|
code;
|
|
@@ -20,16 +46,170 @@ function isRetryableStatus(status) {
|
|
|
20
46
|
function sleep(ms) {
|
|
21
47
|
return new Promise((r) => setTimeout(r, ms));
|
|
22
48
|
}
|
|
49
|
+
function derivePreviewConnection(rawUrl) {
|
|
50
|
+
if (!rawUrl) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(rawUrl);
|
|
55
|
+
const token = parsed.searchParams.get("token");
|
|
56
|
+
const sessionId = parsed.searchParams.get("session");
|
|
57
|
+
if (!token || !sessionId) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const httpProtocol = parsed.protocol === "https:" || parsed.protocol === "wss:" ? "https:" : "http:";
|
|
61
|
+
const wsProtocol = parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" ? "ws:" : parsed.protocol;
|
|
62
|
+
const search = `token=${encodeURIComponent(token)}&session=${encodeURIComponent(sessionId)}`;
|
|
63
|
+
const isDirectPreviewUrl = parsed.pathname.endsWith("/ws") && parsed.pathname.includes("/preview/");
|
|
64
|
+
if (isDirectPreviewUrl) {
|
|
65
|
+
const basePath = parsed.pathname.slice(0, -3);
|
|
66
|
+
return {
|
|
67
|
+
rawUrl,
|
|
68
|
+
token,
|
|
69
|
+
sessionId,
|
|
70
|
+
startUrl: `${httpProtocol}//${parsed.host}${basePath}/start?${search}`,
|
|
71
|
+
navigateUrl: `${httpProtocol}//${parsed.host}${basePath}/navigate?${search}`,
|
|
72
|
+
resizeUrl: `${httpProtocol}//${parsed.host}${basePath}/resize?${search}`,
|
|
73
|
+
wsUrl: `${wsProtocol}//${parsed.host}${parsed.pathname}?${search}`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
rawUrl,
|
|
78
|
+
token,
|
|
79
|
+
sessionId,
|
|
80
|
+
startUrl: `${httpProtocol}//${parsed.host}/preview/start?${search}`,
|
|
81
|
+
navigateUrl: `${httpProtocol}//${parsed.host}/preview/navigate?${search}`,
|
|
82
|
+
resizeUrl: `${httpProtocol}//${parsed.host}/preview/resize?${search}`,
|
|
83
|
+
wsUrl: `${wsProtocol}//${parsed.host}/preview/ws?${search}`
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function deriveOpenCodeConnection(rawUrl) {
|
|
90
|
+
if (!rawUrl) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const parsed = new URL(rawUrl);
|
|
95
|
+
const token = parsed.searchParams.get("token");
|
|
96
|
+
const sessionId = parsed.searchParams.get("session");
|
|
97
|
+
if (!token || !sessionId) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const httpProtocol = parsed.protocol === "https:" || parsed.protocol === "wss:" ? "https:" : "http:";
|
|
101
|
+
const search = `token=${encodeURIComponent(token)}&session=${encodeURIComponent(sessionId)}`;
|
|
102
|
+
return {
|
|
103
|
+
rawUrl,
|
|
104
|
+
token,
|
|
105
|
+
sessionId,
|
|
106
|
+
stateUrl: `${httpProtocol}//${parsed.host}/opencode/state?${search}`,
|
|
107
|
+
messagesUrl: `${httpProtocol}//${parsed.host}/opencode/messages?${search}`,
|
|
108
|
+
promptUrl: `${httpProtocol}//${parsed.host}/opencode/prompt-async?${search}`,
|
|
109
|
+
eventsUrl: `${httpProtocol}//${parsed.host}/opencode/events?${search}`
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function parseJsonResponse(response) {
|
|
116
|
+
const text = await response.text();
|
|
117
|
+
if (!text) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(text);
|
|
122
|
+
} catch {
|
|
123
|
+
return text;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function requestPreviewEndpoint(url, init) {
|
|
127
|
+
const response = await fetch(url, init);
|
|
128
|
+
const payload = await parseJsonResponse(response);
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const message = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.error === "string" ? String(payload.error) : typeof payload === "string" ? payload : `Preview request failed (${response.status})`;
|
|
131
|
+
throw new BanataError(message, response.status);
|
|
132
|
+
}
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
function shouldFallbackFromDirectConnection(error) {
|
|
136
|
+
if (error instanceof BanataError) {
|
|
137
|
+
return error.status === 0 || error.status >= 500;
|
|
138
|
+
}
|
|
139
|
+
if (error instanceof Error) {
|
|
140
|
+
const maybeCause = error.cause;
|
|
141
|
+
const code = maybeCause?.code;
|
|
142
|
+
return code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || error.name === "TypeError";
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function parseSseBlock(block) {
|
|
147
|
+
const trimmed = block.trim();
|
|
148
|
+
if (!trimmed) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
let eventType = "message";
|
|
152
|
+
const dataLines = [];
|
|
153
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
154
|
+
if (line.startsWith("event:")) {
|
|
155
|
+
eventType = line.slice("event:".length).trim() || "message";
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (line.startsWith("data:")) {
|
|
159
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const rawData = dataLines.join(`
|
|
163
|
+
`);
|
|
164
|
+
if (!rawData) {
|
|
165
|
+
return { type: eventType, data: null, raw: trimmed };
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return {
|
|
169
|
+
type: eventType,
|
|
170
|
+
data: JSON.parse(rawData),
|
|
171
|
+
raw: trimmed
|
|
172
|
+
};
|
|
173
|
+
} catch {
|
|
174
|
+
return {
|
|
175
|
+
type: eventType,
|
|
176
|
+
data: rawData,
|
|
177
|
+
raw: trimmed
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function isBrowserSessionOperationallyReady(session) {
|
|
182
|
+
return (session.status === "ready" || session.status === "active") && Boolean(session.cdpUrl);
|
|
183
|
+
}
|
|
184
|
+
function isSandboxSessionOperationallyReady(session) {
|
|
185
|
+
if (session.status !== "ready" && session.status !== "active") {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const browserMode = session.capabilities?.browser?.mode ?? "none";
|
|
189
|
+
if (browserMode !== "none") {
|
|
190
|
+
if (!session.pairedBrowser?.cdpUrl) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (session.capabilities?.opencode?.enabled) {
|
|
195
|
+
if (!session.opencode || session.opencode.status === "failed" || session.opencode.status === "disabled") {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
23
201
|
|
|
24
202
|
class BrowserCloud {
|
|
25
203
|
apiKey;
|
|
26
204
|
baseUrl;
|
|
205
|
+
appUrl;
|
|
27
206
|
retryConfig;
|
|
28
207
|
constructor(config) {
|
|
29
208
|
if (!config.apiKey)
|
|
30
209
|
throw new Error("API key is required");
|
|
31
210
|
this.apiKey = config.apiKey;
|
|
32
|
-
this.baseUrl = config.baseUrl ?? "https://api.banata.dev";
|
|
211
|
+
this.baseUrl = config.baseUrl ?? "https://api.boxes.banata.dev";
|
|
212
|
+
this.appUrl = config.appUrl ?? "https://boxes.banata.dev";
|
|
33
213
|
this.retryConfig = {
|
|
34
214
|
maxRetries: config.retry?.maxRetries ?? 3,
|
|
35
215
|
baseDelayMs: config.retry?.baseDelayMs ?? 500,
|
|
@@ -99,12 +279,13 @@ class BrowserCloud {
|
|
|
99
279
|
}
|
|
100
280
|
async createBrowser(config = {}) {
|
|
101
281
|
const { timeout, waitTimeoutMs, ...rest } = config;
|
|
282
|
+
const requestBody = { ...rest };
|
|
283
|
+
if (waitTimeoutMs !== undefined) {
|
|
284
|
+
requestBody.waitTimeoutMs = waitTimeoutMs;
|
|
285
|
+
}
|
|
102
286
|
return this.request("/v1/browsers", {
|
|
103
287
|
method: "POST",
|
|
104
|
-
body: JSON.stringify(
|
|
105
|
-
...rest,
|
|
106
|
-
waitTimeoutMs: waitTimeoutMs ?? timeout
|
|
107
|
-
})
|
|
288
|
+
body: JSON.stringify(requestBody)
|
|
108
289
|
});
|
|
109
290
|
}
|
|
110
291
|
async getBrowser(sessionId) {
|
|
@@ -115,11 +296,50 @@ class BrowserCloud {
|
|
|
115
296
|
method: "DELETE"
|
|
116
297
|
});
|
|
117
298
|
}
|
|
299
|
+
async getPreviewConnection(sessionId) {
|
|
300
|
+
const session = await this.getBrowser(sessionId);
|
|
301
|
+
return derivePreviewConnection(session.cdpUrl);
|
|
302
|
+
}
|
|
303
|
+
async getPreviewViewerUrl(sessionId) {
|
|
304
|
+
const session = await this.getBrowser(sessionId);
|
|
305
|
+
return buildPreviewViewerUrl(session.cdpUrl, this.appUrl);
|
|
306
|
+
}
|
|
307
|
+
async startPreview(sessionId) {
|
|
308
|
+
const connection = await this.getPreviewConnection(sessionId);
|
|
309
|
+
if (!connection) {
|
|
310
|
+
throw new BanataError("Browser preview is not available for this session", 409);
|
|
311
|
+
}
|
|
312
|
+
return requestPreviewEndpoint(connection.startUrl, {
|
|
313
|
+
method: "POST"
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async navigatePreview(sessionId, url) {
|
|
317
|
+
const connection = await this.getPreviewConnection(sessionId);
|
|
318
|
+
if (!connection) {
|
|
319
|
+
throw new BanataError("Browser preview is not available for this session", 409);
|
|
320
|
+
}
|
|
321
|
+
return requestPreviewEndpoint(connection.navigateUrl, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: { "Content-Type": "application/json" },
|
|
324
|
+
body: JSON.stringify({ url })
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async resizePreview(sessionId, size) {
|
|
328
|
+
const connection = await this.getPreviewConnection(sessionId);
|
|
329
|
+
if (!connection) {
|
|
330
|
+
throw new BanataError("Browser preview is not available for this session", 409);
|
|
331
|
+
}
|
|
332
|
+
return requestPreviewEndpoint(connection.resizeUrl, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: { "Content-Type": "application/json" },
|
|
335
|
+
body: JSON.stringify(size)
|
|
336
|
+
});
|
|
337
|
+
}
|
|
118
338
|
async waitForReady(sessionId, timeoutMs = 30000) {
|
|
119
339
|
const start = Date.now();
|
|
120
340
|
while (Date.now() - start < timeoutMs) {
|
|
121
341
|
const session = await this.getBrowser(sessionId);
|
|
122
|
-
if ((session
|
|
342
|
+
if (isBrowserSessionOperationallyReady(session)) {
|
|
123
343
|
return session.cdpUrl;
|
|
124
344
|
}
|
|
125
345
|
if (session.status === "failed") {
|
|
@@ -135,18 +355,28 @@ class BrowserCloud {
|
|
|
135
355
|
async launch(config = {}) {
|
|
136
356
|
const session = await this.createBrowser(config);
|
|
137
357
|
let cdpUrl;
|
|
138
|
-
|
|
139
|
-
cdpUrl =
|
|
140
|
-
}
|
|
358
|
+
if (isBrowserSessionOperationallyReady(session)) {
|
|
359
|
+
cdpUrl = session.cdpUrl;
|
|
360
|
+
} else {
|
|
141
361
|
try {
|
|
142
|
-
await this.
|
|
143
|
-
} catch {
|
|
144
|
-
|
|
362
|
+
cdpUrl = await this.waitForReady(session.id, config.timeout ?? 30000);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
try {
|
|
365
|
+
await this.closeBrowser(session.id);
|
|
366
|
+
} catch {}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
145
369
|
}
|
|
146
370
|
return {
|
|
147
371
|
cdpUrl,
|
|
148
372
|
sessionId: session.id,
|
|
149
|
-
|
|
373
|
+
previewViewerUrl: buildPreviewViewerUrl(cdpUrl, this.appUrl),
|
|
374
|
+
close: () => this.closeBrowser(session.id),
|
|
375
|
+
getPreviewConnection: () => this.getPreviewConnection(session.id),
|
|
376
|
+
getPreviewViewerUrl: () => this.getPreviewViewerUrl(session.id),
|
|
377
|
+
startPreview: () => this.startPreview(session.id),
|
|
378
|
+
navigatePreview: (url) => this.navigatePreview(session.id, url),
|
|
379
|
+
resizePreview: (size) => this.resizePreview(session.id, size)
|
|
150
380
|
};
|
|
151
381
|
}
|
|
152
382
|
async getUsage() {
|
|
@@ -191,12 +421,14 @@ class BrowserCloud {
|
|
|
191
421
|
class BanataSandbox {
|
|
192
422
|
apiKey;
|
|
193
423
|
baseUrl;
|
|
424
|
+
appUrl;
|
|
194
425
|
retryConfig;
|
|
195
426
|
constructor(config) {
|
|
196
427
|
if (!config.apiKey)
|
|
197
428
|
throw new Error("API key is required");
|
|
198
429
|
this.apiKey = config.apiKey;
|
|
199
|
-
this.baseUrl = config.baseUrl ?? "https://api.banata.dev";
|
|
430
|
+
this.baseUrl = config.baseUrl ?? "https://api.boxes.banata.dev";
|
|
431
|
+
this.appUrl = config.appUrl ?? "https://boxes.banata.dev";
|
|
200
432
|
this.retryConfig = {
|
|
201
433
|
maxRetries: config.retry?.maxRetries ?? 3,
|
|
202
434
|
baseDelayMs: config.retry?.baseDelayMs ?? 500,
|
|
@@ -269,12 +501,13 @@ class BanataSandbox {
|
|
|
269
501
|
}
|
|
270
502
|
async create(config = {}) {
|
|
271
503
|
const { timeout, waitTimeoutMs, ...rest } = config;
|
|
504
|
+
const requestBody = { ...rest };
|
|
505
|
+
if (waitTimeoutMs !== undefined) {
|
|
506
|
+
requestBody.waitTimeoutMs = waitTimeoutMs;
|
|
507
|
+
}
|
|
272
508
|
return this.request("/v1/sandboxes", {
|
|
273
509
|
method: "POST",
|
|
274
|
-
body: JSON.stringify(
|
|
275
|
-
...rest,
|
|
276
|
-
waitTimeoutMs: waitTimeoutMs ?? timeout
|
|
277
|
-
})
|
|
510
|
+
body: JSON.stringify(requestBody)
|
|
278
511
|
});
|
|
279
512
|
}
|
|
280
513
|
async get(id) {
|
|
@@ -289,11 +522,11 @@ class BanataSandbox {
|
|
|
289
522
|
method: "DELETE"
|
|
290
523
|
});
|
|
291
524
|
}
|
|
292
|
-
async waitForReady(id, timeoutMs =
|
|
525
|
+
async waitForReady(id, timeoutMs = 120000) {
|
|
293
526
|
const start = Date.now();
|
|
294
527
|
while (Date.now() - start < timeoutMs) {
|
|
295
528
|
const session = await this.get(id);
|
|
296
|
-
if (session
|
|
529
|
+
if (isSandboxSessionOperationallyReady(session)) {
|
|
297
530
|
return session;
|
|
298
531
|
}
|
|
299
532
|
if (session.status === "failed") {
|
|
@@ -351,7 +584,151 @@ class BanataSandbox {
|
|
|
351
584
|
async getRuntime(id) {
|
|
352
585
|
return this.request(`/v1/sandboxes/runtime?id=${encodeURIComponent(id)}`);
|
|
353
586
|
}
|
|
587
|
+
async getOpencodeState(id, options = {}) {
|
|
588
|
+
const connection = await this.getOpencodeConnection(id);
|
|
589
|
+
if (connection) {
|
|
590
|
+
try {
|
|
591
|
+
const url = new URL(connection.stateUrl);
|
|
592
|
+
if (options.ensureSession !== undefined) {
|
|
593
|
+
url.searchParams.set("ensureSession", String(options.ensureSession));
|
|
594
|
+
}
|
|
595
|
+
if (options.agent) {
|
|
596
|
+
url.searchParams.set("agent", options.agent);
|
|
597
|
+
}
|
|
598
|
+
return await requestPreviewEndpoint(url.toString(), {
|
|
599
|
+
method: "GET"
|
|
600
|
+
});
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const query = new URLSearchParams({ id });
|
|
608
|
+
if (options.ensureSession !== undefined) {
|
|
609
|
+
query.set("ensureSession", String(options.ensureSession));
|
|
610
|
+
}
|
|
611
|
+
if (options.agent) {
|
|
612
|
+
query.set("agent", options.agent);
|
|
613
|
+
}
|
|
614
|
+
return this.request(`/v1/sandboxes/opencode/state?${query.toString()}`);
|
|
615
|
+
}
|
|
616
|
+
async getOpencodeConnection(id) {
|
|
617
|
+
const preview = await this.getPreview(id);
|
|
618
|
+
return deriveOpenCodeConnection(preview.browserPreviewUrl ?? preview.browserPreview?.publicUrl ?? preview.pairedBrowser?.previewUrl ?? null);
|
|
619
|
+
}
|
|
620
|
+
async listOpencodeMessages(id, options = {}) {
|
|
621
|
+
const connection = await this.getOpencodeConnection(id);
|
|
622
|
+
if (connection) {
|
|
623
|
+
try {
|
|
624
|
+
const url = new URL(connection.messagesUrl);
|
|
625
|
+
if (options.sessionId) {
|
|
626
|
+
url.searchParams.set("opencodeSessionId", options.sessionId);
|
|
627
|
+
}
|
|
628
|
+
return await requestPreviewEndpoint(url.toString(), {
|
|
629
|
+
method: "GET"
|
|
630
|
+
});
|
|
631
|
+
} catch (error) {
|
|
632
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const query = new URLSearchParams({ id });
|
|
638
|
+
if (options.sessionId) {
|
|
639
|
+
query.set("sessionId", options.sessionId);
|
|
640
|
+
}
|
|
641
|
+
return this.request(`/v1/sandboxes/opencode/messages?${query.toString()}`);
|
|
642
|
+
}
|
|
643
|
+
async* streamOpencodeEvents(id, options = {}) {
|
|
644
|
+
const connection = await this.getOpencodeConnection(id);
|
|
645
|
+
let response;
|
|
646
|
+
if (connection) {
|
|
647
|
+
try {
|
|
648
|
+
const url = new URL(connection.eventsUrl);
|
|
649
|
+
if (options.sessionId) {
|
|
650
|
+
url.searchParams.set("opencodeSessionId", options.sessionId);
|
|
651
|
+
}
|
|
652
|
+
response = await fetch(url.toString(), {
|
|
653
|
+
method: "GET"
|
|
654
|
+
});
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
const query = new URLSearchParams({ id });
|
|
660
|
+
if (options.sessionId) {
|
|
661
|
+
query.set("sessionId", options.sessionId);
|
|
662
|
+
}
|
|
663
|
+
response = await fetch(`${this.baseUrl}/v1/sandboxes/opencode/events?${query.toString()}`, {
|
|
664
|
+
method: "GET",
|
|
665
|
+
headers: this.headers
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
const query = new URLSearchParams({ id });
|
|
670
|
+
if (options.sessionId) {
|
|
671
|
+
query.set("sessionId", options.sessionId);
|
|
672
|
+
}
|
|
673
|
+
response = await fetch(`${this.baseUrl}/v1/sandboxes/opencode/events?${query.toString()}`, {
|
|
674
|
+
method: "GET",
|
|
675
|
+
headers: this.headers
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (!response.ok) {
|
|
679
|
+
const payload = await parseJsonResponse(response);
|
|
680
|
+
const message = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.error === "string" ? String(payload.error) : typeof payload === "string" ? payload : `Failed to open OpenCode event stream (${response.status})`;
|
|
681
|
+
throw new BanataError(message, response.status);
|
|
682
|
+
}
|
|
683
|
+
if (!response.body) {
|
|
684
|
+
throw new BanataError("OpenCode event stream did not provide a response body", 502);
|
|
685
|
+
}
|
|
686
|
+
const reader = response.body.getReader();
|
|
687
|
+
const decoder = new TextDecoder;
|
|
688
|
+
let buffered = "";
|
|
689
|
+
try {
|
|
690
|
+
while (true) {
|
|
691
|
+
const { value, done } = await reader.read();
|
|
692
|
+
if (done)
|
|
693
|
+
break;
|
|
694
|
+
buffered += decoder.decode(value, { stream: true });
|
|
695
|
+
const blocks = buffered.split(/\r?\n\r?\n/);
|
|
696
|
+
buffered = blocks.pop() ?? "";
|
|
697
|
+
for (const block of blocks) {
|
|
698
|
+
const parsed = parseSseBlock(block);
|
|
699
|
+
if (parsed) {
|
|
700
|
+
yield parsed;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
buffered += decoder.decode();
|
|
705
|
+
const trailing = parseSseBlock(buffered);
|
|
706
|
+
if (trailing) {
|
|
707
|
+
yield trailing;
|
|
708
|
+
}
|
|
709
|
+
} finally {
|
|
710
|
+
reader.releaseLock();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
354
713
|
async prompt(id, prompt, options = {}) {
|
|
714
|
+
const connection = await this.getOpencodeConnection(id);
|
|
715
|
+
if (connection) {
|
|
716
|
+
try {
|
|
717
|
+
return await requestPreviewEndpoint(connection.promptUrl, {
|
|
718
|
+
method: "POST",
|
|
719
|
+
headers: { "Content-Type": "application/json" },
|
|
720
|
+
body: JSON.stringify({
|
|
721
|
+
prompt,
|
|
722
|
+
agent: options.agent,
|
|
723
|
+
sessionId: options.sessionId
|
|
724
|
+
})
|
|
725
|
+
});
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
355
732
|
return this.request("/v1/sandboxes/opencode/prompt", {
|
|
356
733
|
method: "POST",
|
|
357
734
|
body: JSON.stringify({
|
|
@@ -363,6 +740,35 @@ class BanataSandbox {
|
|
|
363
740
|
})
|
|
364
741
|
});
|
|
365
742
|
}
|
|
743
|
+
async promptAsync(id, prompt, options = {}) {
|
|
744
|
+
const connection = await this.getOpencodeConnection(id);
|
|
745
|
+
if (connection) {
|
|
746
|
+
try {
|
|
747
|
+
return await requestPreviewEndpoint(connection.promptUrl, {
|
|
748
|
+
method: "POST",
|
|
749
|
+
headers: { "Content-Type": "application/json" },
|
|
750
|
+
body: JSON.stringify({
|
|
751
|
+
prompt,
|
|
752
|
+
agent: options.agent,
|
|
753
|
+
sessionId: options.sessionId
|
|
754
|
+
})
|
|
755
|
+
});
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (!shouldFallbackFromDirectConnection(error)) {
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return this.request("/v1/sandboxes/opencode/prompt-async", {
|
|
763
|
+
method: "POST",
|
|
764
|
+
body: JSON.stringify({
|
|
765
|
+
id,
|
|
766
|
+
prompt,
|
|
767
|
+
agent: options.agent,
|
|
768
|
+
sessionId: options.sessionId
|
|
769
|
+
})
|
|
770
|
+
});
|
|
771
|
+
}
|
|
366
772
|
async checkpoint(id) {
|
|
367
773
|
return this.request("/v1/sandboxes/checkpoint", {
|
|
368
774
|
method: "POST",
|
|
@@ -376,7 +782,50 @@ class BanataSandbox {
|
|
|
376
782
|
return this.request(`/v1/sandboxes/artifacts/download?id=${encodeURIComponent(id)}&key=${encodeURIComponent(key)}&expiresIn=${encodeURIComponent(String(expiresInSeconds))}`);
|
|
377
783
|
}
|
|
378
784
|
async getPreview(id) {
|
|
379
|
-
|
|
785
|
+
const preview = await this.request(`/v1/sandboxes/browser-preview?id=${encodeURIComponent(id)}`);
|
|
786
|
+
return {
|
|
787
|
+
...preview,
|
|
788
|
+
browserPreviewViewerUrl: buildPreviewViewerUrl(preview.browserPreviewUrl, this.appUrl)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
async getPreviewConnection(id) {
|
|
792
|
+
const preview = await this.getPreview(id);
|
|
793
|
+
return derivePreviewConnection(preview.browserPreviewUrl);
|
|
794
|
+
}
|
|
795
|
+
async getPreviewViewerUrl(id) {
|
|
796
|
+
const preview = await this.getPreview(id);
|
|
797
|
+
return preview.browserPreviewViewerUrl ?? buildPreviewViewerUrl(preview.browserPreviewUrl, this.appUrl);
|
|
798
|
+
}
|
|
799
|
+
async startPreview(id) {
|
|
800
|
+
const connection = await this.getPreviewConnection(id);
|
|
801
|
+
if (!connection) {
|
|
802
|
+
throw new BanataError("Sandbox browser preview is not available", 409);
|
|
803
|
+
}
|
|
804
|
+
return requestPreviewEndpoint(connection.startUrl, {
|
|
805
|
+
method: "POST"
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
async navigatePreview(id, url) {
|
|
809
|
+
const connection = await this.getPreviewConnection(id);
|
|
810
|
+
if (!connection) {
|
|
811
|
+
throw new BanataError("Sandbox browser preview is not available", 409);
|
|
812
|
+
}
|
|
813
|
+
return requestPreviewEndpoint(connection.navigateUrl, {
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers: { "Content-Type": "application/json" },
|
|
816
|
+
body: JSON.stringify({ url })
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
async resizePreview(id, size) {
|
|
820
|
+
const connection = await this.getPreviewConnection(id);
|
|
821
|
+
if (!connection) {
|
|
822
|
+
throw new BanataError("Sandbox browser preview is not available", 409);
|
|
823
|
+
}
|
|
824
|
+
return requestPreviewEndpoint(connection.resizeUrl, {
|
|
825
|
+
method: "POST",
|
|
826
|
+
headers: { "Content-Type": "application/json" },
|
|
827
|
+
body: JSON.stringify(size)
|
|
828
|
+
});
|
|
380
829
|
}
|
|
381
830
|
async getHandoff(id) {
|
|
382
831
|
return this.request(`/v1/sandboxes/handoff?id=${encodeURIComponent(id)}`);
|
|
@@ -429,27 +878,43 @@ class BanataSandbox {
|
|
|
429
878
|
async launch(config = {}) {
|
|
430
879
|
const created = await this.create(config);
|
|
431
880
|
let session;
|
|
432
|
-
|
|
433
|
-
session =
|
|
434
|
-
}
|
|
881
|
+
if (isSandboxSessionOperationallyReady(created)) {
|
|
882
|
+
session = created;
|
|
883
|
+
} else {
|
|
435
884
|
try {
|
|
436
|
-
await this.
|
|
437
|
-
} catch {
|
|
438
|
-
|
|
885
|
+
session = await this.waitForReady(created.id, config.timeout ?? 120000);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
try {
|
|
888
|
+
await this.kill(created.id);
|
|
889
|
+
} catch {}
|
|
890
|
+
throw err;
|
|
891
|
+
}
|
|
439
892
|
}
|
|
440
893
|
const sessionId = session.id;
|
|
441
894
|
const terminalUrl = session.terminalUrl ?? "";
|
|
442
895
|
const browserPreviewUrl = session.browserPreview?.publicUrl ?? session.pairedBrowser?.previewUrl ?? null;
|
|
896
|
+
const browserPreviewViewerUrl = buildPreviewViewerUrl(browserPreviewUrl, this.appUrl);
|
|
443
897
|
return {
|
|
444
898
|
sessionId,
|
|
445
899
|
terminalUrl,
|
|
446
900
|
browserPreviewUrl,
|
|
901
|
+
browserPreviewViewerUrl,
|
|
447
902
|
exec: (command, args) => this.exec(sessionId, command, args),
|
|
448
903
|
runCode: (code) => this.runCode(sessionId, code),
|
|
449
904
|
prompt: (prompt, options) => this.prompt(sessionId, prompt, options),
|
|
905
|
+
promptAsync: (prompt, options) => this.promptAsync(sessionId, prompt, options),
|
|
906
|
+
getOpencodeState: (options) => this.getOpencodeState(sessionId, options),
|
|
907
|
+
getOpencodeConnection: () => this.getOpencodeConnection(sessionId),
|
|
908
|
+
listOpencodeMessages: (options) => this.listOpencodeMessages(sessionId, options),
|
|
909
|
+
streamOpencodeEvents: (options) => this.streamOpencodeEvents(sessionId, options),
|
|
450
910
|
checkpoint: () => this.checkpoint(sessionId),
|
|
451
911
|
getRuntime: () => this.getRuntime(sessionId),
|
|
452
912
|
getPreview: () => this.getPreview(sessionId),
|
|
913
|
+
getPreviewConnection: () => this.getPreviewConnection(sessionId),
|
|
914
|
+
getPreviewViewerUrl: () => this.getPreviewViewerUrl(sessionId),
|
|
915
|
+
startPreview: () => this.startPreview(sessionId),
|
|
916
|
+
navigatePreview: (url) => this.navigatePreview(sessionId, url),
|
|
917
|
+
resizePreview: (size) => this.resizePreview(sessionId, size),
|
|
453
918
|
getHandoff: () => this.getHandoff(sessionId),
|
|
454
919
|
setControl: (mode, options) => this.setControl(sessionId, mode, options),
|
|
455
920
|
takeControl: (options) => this.setControl(sessionId, "human", options),
|
|
@@ -469,6 +934,7 @@ class BanataSandbox {
|
|
|
469
934
|
var src_default = BrowserCloud;
|
|
470
935
|
export {
|
|
471
936
|
src_default as default,
|
|
937
|
+
buildPreviewViewerUrl,
|
|
472
938
|
BrowserServiceError,
|
|
473
939
|
BrowserCloud,
|
|
474
940
|
BanataSandbox,
|