@cloudflare/sandbox 0.0.0-b706463 → 0.0.0-bb855ca

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.
@@ -0,0 +1,255 @@
1
+ export interface ExecutionResult {
2
+ type: 'result' | 'stdout' | 'stderr' | 'error' | 'execution_complete';
3
+ text?: string;
4
+ html?: string;
5
+ png?: string; // base64
6
+ jpeg?: string; // base64
7
+ svg?: string;
8
+ latex?: string;
9
+ markdown?: string;
10
+ javascript?: string;
11
+ json?: any;
12
+ chart?: ChartData;
13
+ data?: any;
14
+ metadata?: any;
15
+ execution_count?: number;
16
+ ename?: string;
17
+ evalue?: string;
18
+ traceback?: string[];
19
+ timestamp: number;
20
+ }
21
+
22
+ export interface ChartData {
23
+ type: 'line' | 'bar' | 'scatter' | 'pie' | 'histogram' | 'heatmap' | 'unknown';
24
+ title?: string;
25
+ data: any;
26
+ layout?: any;
27
+ config?: any;
28
+ library?: 'matplotlib' | 'plotly' | 'altair' | 'seaborn' | 'unknown';
29
+ }
30
+
31
+ export function processJupyterMessage(msg: any): ExecutionResult | null {
32
+ const msgType = msg.header?.msg_type || msg.msg_type;
33
+
34
+ switch (msgType) {
35
+ case 'execute_result':
36
+ case 'display_data':
37
+ return processDisplayData(msg.content.data, msg.content.metadata);
38
+
39
+ case 'stream':
40
+ return {
41
+ type: msg.content.name === 'stdout' ? 'stdout' : 'stderr',
42
+ text: msg.content.text,
43
+ timestamp: Date.now()
44
+ };
45
+
46
+ case 'error':
47
+ return {
48
+ type: 'error',
49
+ ename: msg.content.ename,
50
+ evalue: msg.content.evalue,
51
+ traceback: msg.content.traceback,
52
+ timestamp: Date.now()
53
+ };
54
+
55
+ default:
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function processDisplayData(data: any, metadata?: any): ExecutionResult {
61
+ const result: ExecutionResult = {
62
+ type: 'result',
63
+ timestamp: Date.now(),
64
+ metadata
65
+ };
66
+
67
+ // Process different MIME types in order of preference
68
+
69
+ // Interactive/Rich formats
70
+ if (data['application/vnd.plotly.v1+json']) {
71
+ result.chart = extractPlotlyChart(data['application/vnd.plotly.v1+json']);
72
+ result.json = data['application/vnd.plotly.v1+json'];
73
+ }
74
+
75
+ if (data['application/vnd.vega.v5+json']) {
76
+ result.chart = extractVegaChart(data['application/vnd.vega.v5+json'], 'vega');
77
+ result.json = data['application/vnd.vega.v5+json'];
78
+ }
79
+
80
+ if (data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json']) {
81
+ const vegaData = data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json'];
82
+ result.chart = extractVegaChart(vegaData, 'vega-lite');
83
+ result.json = vegaData;
84
+ }
85
+
86
+ // HTML content (tables, formatted output)
87
+ if (data['text/html']) {
88
+ result.html = data['text/html'];
89
+
90
+ // Check if it's a pandas DataFrame
91
+ if (isPandasDataFrame(data['text/html'])) {
92
+ result.data = { type: 'dataframe', html: data['text/html'] };
93
+ }
94
+ }
95
+
96
+ // Images
97
+ if (data['image/png']) {
98
+ result.png = data['image/png'];
99
+
100
+ // Try to detect if it's a chart
101
+ if (isLikelyChart(data, metadata)) {
102
+ result.chart = {
103
+ type: 'unknown',
104
+ library: 'matplotlib',
105
+ data: { image: data['image/png'] }
106
+ };
107
+ }
108
+ }
109
+
110
+ if (data['image/jpeg']) {
111
+ result.jpeg = data['image/jpeg'];
112
+ }
113
+
114
+ if (data['image/svg+xml']) {
115
+ result.svg = data['image/svg+xml'];
116
+ }
117
+
118
+ // Mathematical content
119
+ if (data['text/latex']) {
120
+ result.latex = data['text/latex'];
121
+ }
122
+
123
+ // Code
124
+ if (data['application/javascript']) {
125
+ result.javascript = data['application/javascript'];
126
+ }
127
+
128
+ // Structured data
129
+ if (data['application/json']) {
130
+ result.json = data['application/json'];
131
+ }
132
+
133
+ // Markdown
134
+ if (data['text/markdown']) {
135
+ result.markdown = data['text/markdown'];
136
+ }
137
+
138
+ // Plain text (fallback)
139
+ if (data['text/plain']) {
140
+ result.text = data['text/plain'];
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ function extractPlotlyChart(plotlyData: any): ChartData {
147
+ const data = plotlyData.data || plotlyData;
148
+ const layout = plotlyData.layout || {};
149
+
150
+ // Try to detect chart type from traces
151
+ let chartType: ChartData['type'] = 'unknown';
152
+ if (data && data.length > 0) {
153
+ const firstTrace = data[0];
154
+ if (firstTrace.type === 'scatter') {
155
+ chartType = firstTrace.mode?.includes('lines') ? 'line' : 'scatter';
156
+ } else if (firstTrace.type === 'bar') {
157
+ chartType = 'bar';
158
+ } else if (firstTrace.type === 'pie') {
159
+ chartType = 'pie';
160
+ } else if (firstTrace.type === 'histogram') {
161
+ chartType = 'histogram';
162
+ } else if (firstTrace.type === 'heatmap') {
163
+ chartType = 'heatmap';
164
+ }
165
+ }
166
+
167
+ return {
168
+ type: chartType,
169
+ title: layout.title?.text || layout.title,
170
+ data: data,
171
+ layout: layout,
172
+ config: plotlyData.config,
173
+ library: 'plotly'
174
+ };
175
+ }
176
+
177
+ function extractVegaChart(vegaData: any, format: 'vega' | 'vega-lite'): ChartData {
178
+ // Try to detect chart type from mark or encoding
179
+ let chartType: ChartData['type'] = 'unknown';
180
+
181
+ if (format === 'vega-lite' && vegaData.mark) {
182
+ const mark = typeof vegaData.mark === 'string' ? vegaData.mark : vegaData.mark.type;
183
+ switch (mark) {
184
+ case 'line':
185
+ chartType = 'line';
186
+ break;
187
+ case 'bar':
188
+ chartType = 'bar';
189
+ break;
190
+ case 'point':
191
+ case 'circle':
192
+ chartType = 'scatter';
193
+ break;
194
+ case 'arc':
195
+ chartType = 'pie';
196
+ break;
197
+ case 'rect':
198
+ if (vegaData.encoding?.color) {
199
+ chartType = 'heatmap';
200
+ }
201
+ break;
202
+ }
203
+ }
204
+
205
+ return {
206
+ type: chartType,
207
+ title: vegaData.title,
208
+ data: vegaData,
209
+ library: 'altair' // Altair outputs Vega-Lite
210
+ };
211
+ }
212
+
213
+ function isPandasDataFrame(html: string): boolean {
214
+ // Simple heuristic to detect pandas DataFrame HTML
215
+ return html.includes('dataframe') ||
216
+ (html.includes('<table') && html.includes('<thead') && html.includes('<tbody'));
217
+ }
218
+
219
+ function isLikelyChart(data: any, metadata?: any): boolean {
220
+ // Check metadata for hints
221
+ if (metadata?.needs?.includes('matplotlib')) {
222
+ return true;
223
+ }
224
+
225
+ // Check if other chart formats are present
226
+ if (data['application/vnd.plotly.v1+json'] ||
227
+ data['application/vnd.vega.v5+json'] ||
228
+ data['application/vnd.vegalite.v4+json']) {
229
+ return true;
230
+ }
231
+
232
+ // If only image output without text, likely a chart
233
+ if ((data['image/png'] || data['image/svg+xml']) && !data['text/plain']) {
234
+ return true;
235
+ }
236
+
237
+ return false;
238
+ }
239
+
240
+ export function extractFormats(result: ExecutionResult): string[] {
241
+ const formats: string[] = [];
242
+
243
+ if (result.text) formats.push('text');
244
+ if (result.html) formats.push('html');
245
+ if (result.png) formats.push('png');
246
+ if (result.jpeg) formats.push('jpeg');
247
+ if (result.svg) formats.push('svg');
248
+ if (result.latex) formats.push('latex');
249
+ if (result.markdown) formats.push('markdown');
250
+ if (result.javascript) formats.push('javascript');
251
+ if (result.json) formats.push('json');
252
+ if (result.chart) formats.push('chart');
253
+
254
+ return formats;
255
+ }
@@ -5,5 +5,14 @@
5
5
  "main": "index.ts",
6
6
  "scripts": {
7
7
  "start": "bun run index.ts"
8
+ },
9
+ "dependencies": {
10
+ "@jupyterlab/services": "^7.0.0",
11
+ "ws": "^8.16.0",
12
+ "uuid": "^9.0.1"
13
+ },
14
+ "devDependencies": {
15
+ "@types/ws": "^8.5.10",
16
+ "@types/uuid": "^9.0.7"
8
17
  }
9
18
  }
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+
3
+ # Start Jupyter notebook server in background
4
+ echo "[Startup] Starting Jupyter server..."
5
+ jupyter notebook \
6
+ --ip=0.0.0.0 \
7
+ --port=8888 \
8
+ --no-browser \
9
+ --allow-root \
10
+ --NotebookApp.token='' \
11
+ --NotebookApp.password='' \
12
+ --NotebookApp.allow_origin='*' \
13
+ --NotebookApp.disable_check_xsrf=True \
14
+ --NotebookApp.allow_remote_access=True \
15
+ --NotebookApp.allow_credentials=True \
16
+ > /tmp/jupyter.log 2>&1 &
17
+
18
+ JUPYTER_PID=$!
19
+
20
+ # Wait for Jupyter to be ready
21
+ echo "[Startup] Waiting for Jupyter to become ready..."
22
+ MAX_ATTEMPTS=30
23
+ ATTEMPT=0
24
+
25
+ while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
26
+ if curl -s http://localhost:8888/api > /dev/null 2>&1; then
27
+ echo "[Startup] Jupyter server is ready!"
28
+ break
29
+ fi
30
+
31
+ # Check if Jupyter process is still running
32
+ if ! kill -0 $JUPYTER_PID 2>/dev/null; then
33
+ echo "[Startup] ERROR: Jupyter process died. Check /tmp/jupyter.log for details"
34
+ cat /tmp/jupyter.log
35
+ exit 1
36
+ fi
37
+
38
+ ATTEMPT=$((ATTEMPT + 1))
39
+ echo "[Startup] Waiting for Jupyter... (attempt $ATTEMPT/$MAX_ATTEMPTS)"
40
+ sleep 1
41
+ done
42
+
43
+ if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
44
+ echo "[Startup] ERROR: Jupyter failed to start within 30 seconds"
45
+ echo "[Startup] Jupyter logs:"
46
+ cat /tmp/jupyter.log
47
+ exit 1
48
+ fi
49
+
50
+ # Start the main Bun server
51
+ echo "[Startup] Starting Bun server..."
52
+ exec bun index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "0.0.0-b706463",
3
+ "version": "0.0.0-bb855ca",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/sandbox-sdk"
package/src/client.ts CHANGED
@@ -6,7 +6,7 @@ import type {
6
6
  GetProcessResponse,
7
7
  ListProcessesResponse,
8
8
  StartProcessRequest,
9
- StartProcessResponse
9
+ StartProcessResponse,
10
10
  } from "./types";
11
11
 
12
12
  export interface ExecuteResponse {
@@ -18,22 +18,6 @@ export interface ExecuteResponse {
18
18
  timestamp: string;
19
19
  }
20
20
 
21
- interface SessionResponse {
22
- sessionId: string;
23
- message: string;
24
- timestamp: string;
25
- }
26
-
27
- interface SessionListResponse {
28
- sessions: Array<{
29
- sessionId: string;
30
- hasActiveProcess: boolean;
31
- createdAt: string;
32
- }>;
33
- count: number;
34
- timestamp: string;
35
- }
36
-
37
21
  interface CommandsResponse {
38
22
  availableCommands: string[];
39
23
  timestamp: string;
@@ -209,7 +193,7 @@ export class HttpClient {
209
193
  this.baseUrl = this.options.baseUrl!;
210
194
  }
211
195
 
212
- private async doFetch(
196
+ protected async doFetch(
213
197
  path: string,
214
198
  options?: RequestInit
215
199
  ): Promise<Response> {
@@ -252,7 +236,7 @@ export class HttpClient {
252
236
 
253
237
  async execute(
254
238
  command: string,
255
- options: Pick<BaseExecOptions, 'sessionId' | 'cwd' | 'env'>
239
+ options: Pick<BaseExecOptions, "sessionId" | "cwd" | "env">
256
240
  ): Promise<ExecuteResponse> {
257
241
  try {
258
242
  const targetSessionId = options.sessionId || this.sessionId;
@@ -305,7 +289,6 @@ export class HttpClient {
305
289
  }
306
290
  }
307
291
 
308
-
309
292
  async executeCommandStream(
310
293
  command: string,
311
294
  sessionId?: string
@@ -320,7 +303,7 @@ export class HttpClient {
320
303
  }),
321
304
  headers: {
322
305
  "Content-Type": "application/json",
323
- "Accept": "text/event-stream",
306
+ Accept: "text/event-stream",
324
307
  },
325
308
  method: "POST",
326
309
  });
@@ -338,9 +321,7 @@ export class HttpClient {
338
321
  throw new Error("No response body for streaming request");
339
322
  }
340
323
 
341
- console.log(
342
- `[HTTP Client] Started command stream: ${command}`
343
- );
324
+ console.log(`[HTTP Client] Started command stream: ${command}`);
344
325
 
345
326
  return response.body;
346
327
  } catch (error) {
@@ -392,7 +373,6 @@ export class HttpClient {
392
373
  }
393
374
  }
394
375
 
395
-
396
376
  async mkdir(
397
377
  path: string,
398
378
  recursive: boolean = false,
@@ -434,7 +414,6 @@ export class HttpClient {
434
414
  }
435
415
  }
436
416
 
437
-
438
417
  async writeFile(
439
418
  path: string,
440
419
  content: string,
@@ -478,7 +457,6 @@ export class HttpClient {
478
457
  }
479
458
  }
480
459
 
481
-
482
460
  async readFile(
483
461
  path: string,
484
462
  encoding: string = "utf-8",
@@ -520,7 +498,6 @@ export class HttpClient {
520
498
  }
521
499
  }
522
500
 
523
-
524
501
  async deleteFile(
525
502
  path: string,
526
503
  sessionId?: string
@@ -560,7 +537,6 @@ export class HttpClient {
560
537
  }
561
538
  }
562
539
 
563
-
564
540
  async renameFile(
565
541
  oldPath: string,
566
542
  newPath: string,
@@ -602,7 +578,6 @@ export class HttpClient {
602
578
  }
603
579
  }
604
580
 
605
-
606
581
  async moveFile(
607
582
  sourcePath: string,
608
583
  destinationPath: string,
@@ -644,7 +619,6 @@ export class HttpClient {
644
619
  }
645
620
  }
646
621
 
647
-
648
622
  async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
649
623
  try {
650
624
  const response = await this.doFetch(`/api/expose-port`, {
@@ -670,7 +644,9 @@ export class HttpClient {
670
644
 
671
645
  const data: ExposePortResponse = await response.json();
672
646
  console.log(
673
- `[HTTP Client] Port exposed: ${port}${name ? ` (${name})` : ""}, Success: ${data.success}`
647
+ `[HTTP Client] Port exposed: ${port}${
648
+ name ? ` (${name})` : ""
649
+ }, Success: ${data.success}`
674
650
  );
675
651
 
676
652
  return data;
@@ -732,9 +708,7 @@ export class HttpClient {
732
708
  }
733
709
 
734
710
  const data: GetExposedPortsResponse = await response.json();
735
- console.log(
736
- `[HTTP Client] Got ${data.count} exposed ports`
737
- );
711
+ console.log(`[HTTP Client] Got ${data.count} exposed ports`);
738
712
 
739
713
  return data;
740
714
  } catch (error) {
@@ -871,9 +845,7 @@ export class HttpClient {
871
845
  }
872
846
 
873
847
  const data: ListProcessesResponse = await response.json();
874
- console.log(
875
- `[HTTP Client] Listed ${data.processes.length} processes`
876
- );
848
+ console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
877
849
 
878
850
  return data;
879
851
  } catch (error) {
@@ -902,7 +874,9 @@ export class HttpClient {
902
874
 
903
875
  const data: GetProcessResponse = await response.json();
904
876
  console.log(
905
- `[HTTP Client] Got process ${processId}: ${data.process?.status || 'not found'}`
877
+ `[HTTP Client] Got process ${processId}: ${
878
+ data.process?.status || "not found"
879
+ }`
906
880
  );
907
881
 
908
882
  return data;
@@ -912,7 +886,9 @@ export class HttpClient {
912
886
  }
913
887
  }
914
888
 
915
- async killProcess(processId: string): Promise<{ success: boolean; message: string }> {
889
+ async killProcess(
890
+ processId: string
891
+ ): Promise<{ success: boolean; message: string }> {
916
892
  try {
917
893
  const response = await this.doFetch(`/api/process/${processId}`, {
918
894
  headers: {
@@ -930,10 +906,11 @@ export class HttpClient {
930
906
  );
931
907
  }
932
908
 
933
- const data = await response.json() as { success: boolean; message: string };
934
- console.log(
935
- `[HTTP Client] Killed process ${processId}`
936
- );
909
+ const data = (await response.json()) as {
910
+ success: boolean;
911
+ message: string;
912
+ };
913
+ console.log(`[HTTP Client] Killed process ${processId}`);
937
914
 
938
915
  return data;
939
916
  } catch (error) {
@@ -942,7 +919,11 @@ export class HttpClient {
942
919
  }
943
920
  }
944
921
 
945
- async killAllProcesses(): Promise<{ success: boolean; killedCount: number; message: string }> {
922
+ async killAllProcesses(): Promise<{
923
+ success: boolean;
924
+ killedCount: number;
925
+ message: string;
926
+ }> {
946
927
  try {
947
928
  const response = await this.doFetch("/api/process/kill-all", {
948
929
  headers: {
@@ -960,10 +941,12 @@ export class HttpClient {
960
941
  );
961
942
  }
962
943
 
963
- const data = await response.json() as { success: boolean; killedCount: number; message: string };
964
- console.log(
965
- `[HTTP Client] Killed ${data.killedCount} processes`
966
- );
944
+ const data = (await response.json()) as {
945
+ success: boolean;
946
+ killedCount: number;
947
+ message: string;
948
+ };
949
+ console.log(`[HTTP Client] Killed ${data.killedCount} processes`);
967
950
 
968
951
  return data;
969
952
  } catch (error) {
@@ -991,9 +974,7 @@ export class HttpClient {
991
974
  }
992
975
 
993
976
  const data: GetProcessLogsResponse = await response.json();
994
- console.log(
995
- `[HTTP Client] Got logs for process ${processId}`
996
- );
977
+ console.log(`[HTTP Client] Got logs for process ${processId}`);
997
978
 
998
979
  return data;
999
980
  } catch (error) {
@@ -1002,11 +983,13 @@ export class HttpClient {
1002
983
  }
1003
984
  }
1004
985
 
1005
- async streamProcessLogs(processId: string): Promise<ReadableStream<Uint8Array>> {
986
+ async streamProcessLogs(
987
+ processId: string
988
+ ): Promise<ReadableStream<Uint8Array>> {
1006
989
  try {
1007
990
  const response = await this.doFetch(`/api/process/${processId}/stream`, {
1008
991
  headers: {
1009
- "Accept": "text/event-stream",
992
+ Accept: "text/event-stream",
1010
993
  "Cache-Control": "no-cache",
1011
994
  },
1012
995
  method: "GET",
package/src/index.ts CHANGED
@@ -5,16 +5,25 @@ export type {
5
5
  MkdirResponse, MoveFileResponse,
6
6
  ReadFileResponse, RenameFileResponse, WriteFileResponse
7
7
  } from "./client";
8
-
8
+ // Export code interpreter types
9
+ export type {
10
+ ChartData,
11
+ CodeContext,
12
+ CreateContextOptions,
13
+ Execution,
14
+ ExecutionError,
15
+ OutputMessage,
16
+ Result,
17
+ RunCodeOptions
18
+ } from "./interpreter-types";
19
+ // Export the implementations
20
+ export { ResultImpl } from "./interpreter-types";
9
21
  // Re-export request handler utilities
10
22
  export {
11
23
  proxyToSandbox, type RouteInfo, type SandboxEnv
12
24
  } from './request-handler';
13
-
14
25
  export { getSandbox, Sandbox } from "./sandbox";
15
-
16
26
  // Export SSE parser for converting ReadableStream to AsyncIterable
17
27
  export { asyncIterableToSSEStream, parseSSEStream, responseToAsyncIterable } from "./sse-parser";
18
-
19
28
  // Export event types for streaming
20
29
  export type { ExecEvent, LogEvent } from "./types";