@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.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.status === "ready" || session.status === "active") && session.cdpUrl) {
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
- try {
139
- cdpUrl = await this.waitForReady(session.id, config.timeout ?? 30000);
140
- } catch (err) {
358
+ if (isBrowserSessionOperationallyReady(session)) {
359
+ cdpUrl = session.cdpUrl;
360
+ } else {
141
361
  try {
142
- await this.closeBrowser(session.id);
143
- } catch {}
144
- throw err;
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
- close: () => this.closeBrowser(session.id)
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 = 30000) {
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.status === "ready" || session.status === "active") {
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
- return this.request(`/v1/sandboxes/browser-preview?id=${encodeURIComponent(id)}`);
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
- try {
433
- session = await this.waitForReady(created.id, config.timeout ?? 30000);
434
- } catch (err) {
881
+ if (isSandboxSessionOperationallyReady(created)) {
882
+ session = created;
883
+ } else {
435
884
  try {
436
- await this.kill(created.id);
437
- } catch {}
438
- throw err;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banata-boxes/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TypeScript SDK for Banata browser sessions and code sandboxes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",