@cloudflare/sandbox 0.0.0-d81d2a5 → 0.0.0-d951819

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/Dockerfile +34 -27
  3. package/README.md +127 -12
  4. package/container_src/bun.lock +31 -77
  5. package/container_src/circuit-breaker.ts +121 -0
  6. package/container_src/control-process.ts +784 -0
  7. package/container_src/handler/exec.ts +99 -254
  8. package/container_src/handler/file.ts +253 -640
  9. package/container_src/handler/git.ts +28 -80
  10. package/container_src/handler/process.ts +443 -515
  11. package/container_src/handler/session.ts +92 -0
  12. package/container_src/index.ts +289 -219
  13. package/container_src/interpreter-service.ts +276 -0
  14. package/container_src/isolation.ts +1213 -0
  15. package/container_src/mime-processor.ts +1 -1
  16. package/container_src/package.json +4 -4
  17. package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
  18. package/container_src/runtime/executors/python/ipython_executor.py +338 -0
  19. package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
  20. package/container_src/runtime/process-pool.ts +464 -0
  21. package/container_src/shell-escape.ts +42 -0
  22. package/container_src/startup.sh +6 -47
  23. package/container_src/types.ts +35 -12
  24. package/package.json +2 -2
  25. package/src/client.ts +214 -187
  26. package/src/errors.ts +219 -0
  27. package/src/file-stream.ts +162 -0
  28. package/src/index.ts +66 -14
  29. package/src/interpreter-client.ts +352 -0
  30. package/src/interpreter-types.ts +102 -95
  31. package/src/interpreter.ts +8 -8
  32. package/src/sandbox.ts +315 -337
  33. package/src/types.ts +194 -24
  34. package/container_src/jupyter-server.ts +0 -336
  35. package/src/jupyter-client.ts +0 -266
@@ -1,11 +1,15 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { serve } from "bun";
3
- import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
2
+ import {
3
+ handleExecuteRequest,
4
+ handleStreamingExecuteRequest,
5
+ } from "./handler/exec";
4
6
  import {
5
7
  handleDeleteFileRequest,
8
+ handleListFilesRequest,
6
9
  handleMkdirRequest,
7
10
  handleMoveFileRequest,
8
11
  handleReadFileRequest,
12
+ handleReadFileStreamRequest,
9
13
  handleRenameFileRequest,
10
14
  handleWriteFileRequest,
11
15
  } from "./handler/file";
@@ -25,58 +29,65 @@ import {
25
29
  handleStartProcessRequest,
26
30
  handleStreamProcessLogsRequest,
27
31
  } from "./handler/process";
28
- import { type CreateContextRequest, JupyterServer } from "./jupyter-server";
29
- import type { ProcessRecord, SessionData } from "./types";
30
-
31
- // In-memory session storage (in production, you'd want to use a proper database)
32
- const sessions = new Map<string, SessionData>();
32
+ import { handleCreateSession, handleListSessions } from "./handler/session";
33
+ import type { CreateContextRequest } from "./interpreter-service";
34
+ import {
35
+ InterpreterNotReadyError,
36
+ InterpreterService,
37
+ } from "./interpreter-service";
38
+ import { hasNamespaceSupport, SessionManager } from "./isolation";
33
39
 
34
40
  // In-memory storage for exposed ports
35
41
  const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
36
42
 
37
- // In-memory process storage - cleared on container restart
38
- const processes = new Map<string, ProcessRecord>();
39
-
40
- // Generate a unique session ID using cryptographically secure randomness
41
- function generateSessionId(): string {
42
- return `session_${Date.now()}_${randomBytes(6).toString('hex')}`;
43
- }
44
-
45
- // Clean up old sessions (older than 1 hour)
46
- function cleanupOldSessions() {
47
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
48
- for (const [sessionId, session] of sessions.entries()) {
49
- if (session.createdAt < oneHourAgo && !session.activeProcess) {
50
- sessions.delete(sessionId);
51
- console.log(`[Server] Cleaned up old session: ${sessionId}`);
52
- }
53
- }
54
- }
55
-
56
- // Run cleanup every 10 minutes
57
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
58
-
59
- // Initialize Jupyter server
60
- const jupyterServer = new JupyterServer();
61
- let jupyterInitialized = false;
62
-
63
- // Initialize Jupyter immediately since startup.sh ensures it's ready
64
- (async () => {
65
- try {
66
- await jupyterServer.initialize();
67
- jupyterInitialized = true;
68
- console.log("[Container] Jupyter integration initialized successfully");
69
- } catch (error) {
70
- console.error("[Container] Failed to initialize Jupyter:", error);
71
- // Log more details to help debug
72
- if (error instanceof Error) {
73
- console.error("[Container] Error details:", error.message);
74
- console.error("[Container] Stack trace:", error.stack);
75
- }
76
- }
77
- })();
43
+ // Check isolation capabilities on startup
44
+ const isolationAvailable = hasNamespaceSupport();
45
+ console.log(
46
+ `[Container] Process isolation: ${
47
+ isolationAvailable
48
+ ? "ENABLED (production mode)"
49
+ : "DISABLED (development mode)"
50
+ }`
51
+ );
52
+
53
+ // Session manager for secure execution with isolation
54
+ const sessionManager = new SessionManager();
55
+
56
+ // Graceful shutdown handler
57
+ const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
58
+
59
+ process.on("SIGTERM", async () => {
60
+ console.log("[Container] SIGTERM received, cleaning up sessions...");
61
+ await sessionManager.destroyAll();
62
+ setTimeout(() => {
63
+ process.exit(0);
64
+ }, SHUTDOWN_GRACE_PERIOD_MS);
65
+ });
66
+
67
+ process.on("SIGINT", async () => {
68
+ console.log("[Container] SIGINT received, cleaning up sessions...");
69
+ await sessionManager.destroyAll();
70
+ setTimeout(() => {
71
+ process.exit(0);
72
+ }, SHUTDOWN_GRACE_PERIOD_MS);
73
+ });
74
+
75
+ // Cleanup on uncaught exceptions (log but still exit)
76
+ process.on("uncaughtException", async (error) => {
77
+ console.error("[Container] Uncaught exception:", error);
78
+ await sessionManager.destroyAll();
79
+ process.exit(1);
80
+ });
81
+
82
+ // Initialize interpreter service
83
+ const interpreterService = new InterpreterService();
84
+
85
+ // No initialization needed - service is ready immediately!
86
+ console.log("[Container] Interpreter service ready - no cold start!");
87
+ console.log("[Container] All API endpoints available immediately");
78
88
 
79
89
  const server = serve({
90
+ idleTimeout: 255,
80
91
  async fetch(req: Request) {
81
92
  const url = new URL(req.url);
82
93
  const pathname = url.pathname;
@@ -110,109 +121,42 @@ const server = serve({
110
121
 
111
122
  case "/api/session/create":
112
123
  if (req.method === "POST") {
113
- const sessionId = generateSessionId();
114
- const sessionData: SessionData = {
115
- activeProcess: null,
116
- createdAt: new Date(),
117
- sessionId,
118
- };
119
- sessions.set(sessionId, sessionData);
120
-
121
- console.log(`[Server] Created new session: ${sessionId}`);
122
-
123
- return new Response(
124
- JSON.stringify({
125
- message: "Session created successfully",
126
- sessionId,
127
- timestamp: new Date().toISOString(),
128
- }),
129
- {
130
- headers: {
131
- "Content-Type": "application/json",
132
- ...corsHeaders,
133
- },
134
- }
135
- );
124
+ return handleCreateSession(req, corsHeaders, sessionManager);
136
125
  }
137
126
  break;
138
127
 
139
128
  case "/api/session/list":
140
129
  if (req.method === "GET") {
141
- const sessionList = Array.from(sessions.values()).map(
142
- (session) => ({
143
- createdAt: session.createdAt.toISOString(),
144
- hasActiveProcess: !!session.activeProcess,
145
- sessionId: session.sessionId,
146
- })
147
- );
148
-
149
- return new Response(
150
- JSON.stringify({
151
- count: sessionList.length,
152
- sessions: sessionList,
153
- timestamp: new Date().toISOString(),
154
- }),
155
- {
156
- headers: {
157
- "Content-Type": "application/json",
158
- ...corsHeaders,
159
- },
160
- }
161
- );
130
+ return handleListSessions(corsHeaders, sessionManager);
162
131
  }
163
132
  break;
164
133
 
165
134
  case "/api/execute":
166
135
  if (req.method === "POST") {
167
- return handleExecuteRequest(sessions, req, corsHeaders);
136
+ return handleExecuteRequest(req, corsHeaders, sessionManager);
168
137
  }
169
138
  break;
170
139
 
171
140
  case "/api/execute/stream":
172
141
  if (req.method === "POST") {
173
- return handleStreamingExecuteRequest(sessions, req, corsHeaders);
142
+ return handleStreamingExecuteRequest(
143
+ req,
144
+ sessionManager,
145
+ corsHeaders
146
+ );
174
147
  }
175
148
  break;
176
149
 
177
150
  case "/api/ping":
178
151
  if (req.method === "GET") {
152
+ const health = await interpreterService.getHealthStatus();
179
153
  return new Response(
180
154
  JSON.stringify({
181
155
  message: "pong",
182
156
  timestamp: new Date().toISOString(),
183
- jupyter: jupyterInitialized ? "ready" : "not ready",
184
- }),
185
- {
186
- headers: {
187
- "Content-Type": "application/json",
188
- ...corsHeaders,
189
- },
190
- }
191
- );
192
- }
193
- break;
194
-
195
- case "/api/commands":
196
- if (req.method === "GET") {
197
- return new Response(
198
- JSON.stringify({
199
- availableCommands: [
200
- "ls",
201
- "pwd",
202
- "echo",
203
- "cat",
204
- "grep",
205
- "find",
206
- "whoami",
207
- "date",
208
- "uptime",
209
- "ps",
210
- "top",
211
- "df",
212
- "du",
213
- "free",
214
- ],
215
- timestamp: new Date().toISOString(),
157
+ system: "interpreter (70x faster)",
158
+ status: health.ready ? "ready" : "initializing",
159
+ progress: health.progress,
216
160
  }),
217
161
  {
218
162
  headers: {
@@ -226,43 +170,55 @@ const server = serve({
226
170
 
227
171
  case "/api/git/checkout":
228
172
  if (req.method === "POST") {
229
- return handleGitCheckoutRequest(sessions, req, corsHeaders);
173
+ return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
230
174
  }
231
175
  break;
232
176
 
233
177
  case "/api/mkdir":
234
178
  if (req.method === "POST") {
235
- return handleMkdirRequest(sessions, req, corsHeaders);
179
+ return handleMkdirRequest(req, corsHeaders, sessionManager);
236
180
  }
237
181
  break;
238
182
 
239
183
  case "/api/write":
240
184
  if (req.method === "POST") {
241
- return handleWriteFileRequest(req, corsHeaders);
185
+ return handleWriteFileRequest(req, corsHeaders, sessionManager);
242
186
  }
243
187
  break;
244
188
 
245
189
  case "/api/read":
246
190
  if (req.method === "POST") {
247
- return handleReadFileRequest(req, corsHeaders);
191
+ return handleReadFileRequest(req, corsHeaders, sessionManager);
192
+ }
193
+ break;
194
+
195
+ case "/api/read/stream":
196
+ if (req.method === "POST") {
197
+ return handleReadFileStreamRequest(req, corsHeaders, sessionManager);
248
198
  }
249
199
  break;
250
200
 
251
201
  case "/api/delete":
252
202
  if (req.method === "POST") {
253
- return handleDeleteFileRequest(req, corsHeaders);
203
+ return handleDeleteFileRequest(req, corsHeaders, sessionManager);
254
204
  }
255
205
  break;
256
206
 
257
207
  case "/api/rename":
258
208
  if (req.method === "POST") {
259
- return handleRenameFileRequest(req, corsHeaders);
209
+ return handleRenameFileRequest(req, corsHeaders, sessionManager);
260
210
  }
261
211
  break;
262
212
 
263
213
  case "/api/move":
264
214
  if (req.method === "POST") {
265
- return handleMoveFileRequest(req, corsHeaders);
215
+ return handleMoveFileRequest(req, corsHeaders, sessionManager);
216
+ }
217
+ break;
218
+
219
+ case "/api/list-files":
220
+ if (req.method === "POST") {
221
+ return handleListFilesRequest(req, corsHeaders, sessionManager);
266
222
  }
267
223
  break;
268
224
 
@@ -286,49 +242,38 @@ const server = serve({
286
242
 
287
243
  case "/api/process/start":
288
244
  if (req.method === "POST") {
289
- return handleStartProcessRequest(processes, req, corsHeaders);
245
+ return handleStartProcessRequest(req, corsHeaders, sessionManager);
290
246
  }
291
247
  break;
292
248
 
293
249
  case "/api/process/list":
294
250
  if (req.method === "GET") {
295
- return handleListProcessesRequest(processes, req, corsHeaders);
251
+ return handleListProcessesRequest(req, corsHeaders, sessionManager);
296
252
  }
297
253
  break;
298
254
 
299
255
  case "/api/process/kill-all":
300
256
  if (req.method === "DELETE") {
301
- return handleKillAllProcessesRequest(processes, req, corsHeaders);
257
+ return handleKillAllProcessesRequest(
258
+ req,
259
+ corsHeaders,
260
+ sessionManager
261
+ );
302
262
  }
303
263
  break;
304
264
 
305
- // Code interpreter endpoints
306
265
  case "/api/contexts":
307
266
  if (req.method === "POST") {
308
- if (!jupyterInitialized) {
309
- return new Response(
310
- JSON.stringify({
311
- error: "Jupyter server is not ready. Please try again in a moment."
312
- }),
313
- {
314
- status: 503,
315
- headers: {
316
- "Content-Type": "application/json",
317
- ...corsHeaders,
318
- },
319
- }
320
- );
321
- }
322
267
  try {
323
- const body = await req.json() as CreateContextRequest;
324
- const context = await jupyterServer.createContext(body);
268
+ const body = (await req.json()) as CreateContextRequest;
269
+ const context = await interpreterService.createContext(body);
325
270
  return new Response(
326
271
  JSON.stringify({
327
272
  id: context.id,
328
273
  language: context.language,
329
274
  cwd: context.cwd,
330
275
  createdAt: context.createdAt,
331
- lastUsed: context.lastUsed
276
+ lastUsed: context.lastUsed,
332
277
  }),
333
278
  {
334
279
  headers: {
@@ -338,10 +283,62 @@ const server = serve({
338
283
  }
339
284
  );
340
285
  } catch (error) {
286
+ if (error instanceof InterpreterNotReadyError) {
287
+ console.log(
288
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
289
+ );
290
+ return new Response(
291
+ JSON.stringify({
292
+ error: error.message,
293
+ status: "initializing",
294
+ progress: error.progress,
295
+ }),
296
+ {
297
+ status: 503,
298
+ headers: {
299
+ "Content-Type": "application/json",
300
+ "Retry-After": String(error.retryAfter),
301
+ ...corsHeaders,
302
+ },
303
+ }
304
+ );
305
+ }
306
+
307
+ // Check if it's a circuit breaker error
308
+ if (
309
+ error instanceof Error &&
310
+ error.message.includes("Circuit breaker is open")
311
+ ) {
312
+ console.log(
313
+ "[Container] Circuit breaker is open:",
314
+ error.message
315
+ );
316
+ return new Response(
317
+ JSON.stringify({
318
+ error:
319
+ "Service temporarily unavailable due to high error rate. Please try again later.",
320
+ status: "circuit_open",
321
+ details: error.message,
322
+ }),
323
+ {
324
+ status: 503,
325
+ headers: {
326
+ "Content-Type": "application/json",
327
+ "Retry-After": "60",
328
+ ...corsHeaders,
329
+ },
330
+ }
331
+ );
332
+ }
333
+
334
+ // Only log actual errors with stack traces
341
335
  console.error("[Container] Error creating context:", error);
342
336
  return new Response(
343
337
  JSON.stringify({
344
- error: error instanceof Error ? error.message : "Failed to create context"
338
+ error:
339
+ error instanceof Error
340
+ ? error.message
341
+ : "Failed to create context",
345
342
  }),
346
343
  {
347
344
  status: 500,
@@ -353,54 +350,75 @@ const server = serve({
353
350
  );
354
351
  }
355
352
  } else if (req.method === "GET") {
356
- if (!jupyterInitialized) {
357
- return new Response(
358
- JSON.stringify({ contexts: [] }),
359
- {
360
- headers: {
361
- "Content-Type": "application/json",
362
- ...corsHeaders,
363
- },
364
- }
365
- );
366
- }
367
- const contexts = await jupyterServer.listContexts();
368
- return new Response(
369
- JSON.stringify({ contexts }),
370
- {
371
- headers: {
372
- "Content-Type": "application/json",
373
- ...corsHeaders,
374
- },
375
- }
376
- );
353
+ const contexts = await interpreterService.listContexts();
354
+ return new Response(JSON.stringify({ contexts }), {
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ ...corsHeaders,
358
+ },
359
+ });
377
360
  }
378
361
  break;
379
362
 
380
363
  case "/api/execute/code":
381
364
  if (req.method === "POST") {
382
- if (!jupyterInitialized) {
383
- return new Response(
384
- JSON.stringify({
385
- error: "Jupyter server is not ready. Please try again in a moment."
386
- }),
387
- {
388
- status: 503,
389
- headers: {
390
- "Content-Type": "application/json",
391
- ...corsHeaders,
392
- },
393
- }
394
- );
395
- }
396
365
  try {
397
- const body = await req.json() as { context_id: string; code: string; language?: string };
398
- return await jupyterServer.executeCode(body.context_id, body.code, body.language);
366
+ const body = (await req.json()) as {
367
+ context_id: string;
368
+ code: string;
369
+ language?: string;
370
+ };
371
+ return await interpreterService.executeCode(
372
+ body.context_id,
373
+ body.code,
374
+ body.language
375
+ );
399
376
  } catch (error) {
400
- console.error("[Container] Error executing code:", error);
377
+ // Check if it's a circuit breaker error
378
+ if (
379
+ error instanceof Error &&
380
+ error.message.includes("Circuit breaker is open")
381
+ ) {
382
+ console.log(
383
+ "[Container] Circuit breaker is open for code execution:",
384
+ error.message
385
+ );
386
+ return new Response(
387
+ JSON.stringify({
388
+ error:
389
+ "Service temporarily unavailable due to high error rate. Please try again later.",
390
+ status: "circuit_open",
391
+ details: error.message,
392
+ }),
393
+ {
394
+ status: 503,
395
+ headers: {
396
+ "Content-Type": "application/json",
397
+ "Retry-After": "30",
398
+ ...corsHeaders,
399
+ },
400
+ }
401
+ );
402
+ }
403
+
404
+ // Don't log stack traces for expected initialization state
405
+ if (
406
+ error instanceof Error &&
407
+ error.message.includes("initializing")
408
+ ) {
409
+ console.log(
410
+ "[Container] Code execution deferred - service still initializing"
411
+ );
412
+ } else {
413
+ console.error("[Container] Error executing code:", error);
414
+ }
415
+ // Error response is already handled by service.executeCode for not ready state
401
416
  return new Response(
402
417
  JSON.stringify({
403
- error: error instanceof Error ? error.message : "Failed to execute code"
418
+ error:
419
+ error instanceof Error
420
+ ? error.message
421
+ : "Failed to execute code",
404
422
  }),
405
423
  {
406
424
  status: 500,
@@ -416,27 +434,54 @@ const server = serve({
416
434
 
417
435
  default:
418
436
  // Handle dynamic routes for contexts
419
- if (pathname.startsWith("/api/contexts/") && pathname.split('/').length === 4) {
420
- const contextId = pathname.split('/')[3];
437
+ if (
438
+ pathname.startsWith("/api/contexts/") &&
439
+ pathname.split("/").length === 4
440
+ ) {
441
+ const contextId = pathname.split("/")[3];
421
442
  if (req.method === "DELETE") {
422
443
  try {
423
- await jupyterServer.deleteContext(contextId);
424
- return new Response(
425
- JSON.stringify({ success: true }),
426
- {
427
- headers: {
428
- "Content-Type": "application/json",
429
- ...corsHeaders,
430
- },
431
- }
432
- );
444
+ await interpreterService.deleteContext(contextId);
445
+ return new Response(JSON.stringify({ success: true }), {
446
+ headers: {
447
+ "Content-Type": "application/json",
448
+ ...corsHeaders,
449
+ },
450
+ });
433
451
  } catch (error) {
452
+ if (error instanceof InterpreterNotReadyError) {
453
+ console.log(
454
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
455
+ );
456
+ return new Response(
457
+ JSON.stringify({
458
+ error: error.message,
459
+ status: "initializing",
460
+ progress: error.progress,
461
+ }),
462
+ {
463
+ status: 503,
464
+ headers: {
465
+ "Content-Type": "application/json",
466
+ "Retry-After": "5",
467
+ ...corsHeaders,
468
+ },
469
+ }
470
+ );
471
+ }
434
472
  return new Response(
435
473
  JSON.stringify({
436
- error: error instanceof Error ? error.message : "Failed to delete context"
474
+ error:
475
+ error instanceof Error
476
+ ? error.message
477
+ : "Failed to delete context",
437
478
  }),
438
479
  {
439
- status: error instanceof Error && error.message.includes("not found") ? 404 : 500,
480
+ status:
481
+ error instanceof Error &&
482
+ error.message.includes("not found")
483
+ ? 404
484
+ : 500,
440
485
  headers: {
441
486
  "Content-Type": "application/json",
442
487
  ...corsHeaders,
@@ -446,22 +491,42 @@ const server = serve({
446
491
  }
447
492
  }
448
493
  }
449
-
494
+
450
495
  // Handle dynamic routes for individual processes
451
496
  if (pathname.startsWith("/api/process/")) {
452
- const segments = pathname.split('/');
497
+ const segments = pathname.split("/");
453
498
  if (segments.length >= 4) {
454
499
  const processId = segments[3];
455
500
  const action = segments[4]; // Optional: logs, stream, etc.
456
501
 
457
502
  if (!action && req.method === "GET") {
458
- return handleGetProcessRequest(processes, req, corsHeaders, processId);
503
+ return handleGetProcessRequest(
504
+ req,
505
+ corsHeaders,
506
+ processId,
507
+ sessionManager
508
+ );
459
509
  } else if (!action && req.method === "DELETE") {
460
- return handleKillProcessRequest(processes, req, corsHeaders, processId);
510
+ return handleKillProcessRequest(
511
+ req,
512
+ corsHeaders,
513
+ processId,
514
+ sessionManager
515
+ );
461
516
  } else if (action === "logs" && req.method === "GET") {
462
- return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
517
+ return handleGetProcessLogsRequest(
518
+ req,
519
+ corsHeaders,
520
+ processId,
521
+ sessionManager
522
+ );
463
523
  } else if (action === "stream" && req.method === "GET") {
464
- return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
524
+ return handleStreamProcessLogsRequest(
525
+ req,
526
+ corsHeaders,
527
+ processId,
528
+ sessionManager
529
+ );
465
530
  }
466
531
  }
467
532
  }
@@ -477,7 +542,10 @@ const server = serve({
477
542
  });
478
543
  }
479
544
  } catch (error) {
480
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
545
+ console.error(
546
+ `[Container] Error handling ${req.method} ${pathname}:`,
547
+ error
548
+ );
481
549
  return new Response(
482
550
  JSON.stringify({
483
551
  error: "Internal server error",
@@ -496,7 +564,7 @@ const server = serve({
496
564
  hostname: "0.0.0.0",
497
565
  port: 3000,
498
566
  // We don't need this, but typescript complains
499
- websocket: { async message() { } },
567
+ websocket: { async message() {} },
500
568
  });
501
569
 
502
570
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
@@ -509,6 +577,7 @@ console.log(` POST /api/git/checkout - Checkout a git repository`);
509
577
  console.log(` POST /api/mkdir - Create a directory`);
510
578
  console.log(` POST /api/write - Write a file`);
511
579
  console.log(` POST /api/read - Read a file`);
580
+ console.log(` POST /api/read/stream - Stream a file (SSE)`);
512
581
  console.log(` POST /api/delete - Delete a file`);
513
582
  console.log(` POST /api/rename - Rename a file`);
514
583
  console.log(` POST /api/move - Move a file`);
@@ -526,6 +595,7 @@ console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
526
595
  console.log(` POST /api/contexts - Create a code execution context`);
527
596
  console.log(` GET /api/contexts - List all contexts`);
528
597
  console.log(` DELETE /api/contexts/{id} - Delete a context`);
529
- console.log(` POST /api/execute/code - Execute code in a context (streaming)`);
598
+ console.log(
599
+ ` POST /api/execute/code - Execute code in a context (streaming)`
600
+ );
530
601
  console.log(` GET /api/ping - Health check`);
531
- console.log(` GET /api/commands - List available commands`);