@cloudflare/sandbox 0.0.0-f5fcd52 → 0.0.0-fb3c9c2

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 (40) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/Dockerfile +47 -36
  3. package/README.md +805 -24
  4. package/container_src/bun.lock +122 -0
  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 +185 -0
  8. package/container_src/handler/file.ts +406 -0
  9. package/container_src/handler/git.ts +130 -0
  10. package/container_src/handler/ports.ts +314 -0
  11. package/container_src/handler/process.ts +568 -0
  12. package/container_src/handler/session.ts +92 -0
  13. package/container_src/index.ts +411 -3006
  14. package/container_src/isolation.ts +1038 -0
  15. package/container_src/jupyter-server.ts +579 -0
  16. package/container_src/jupyter-service.ts +461 -0
  17. package/container_src/jupyter_config.py +48 -0
  18. package/container_src/mime-processor.ts +255 -0
  19. package/container_src/package.json +9 -0
  20. package/container_src/shell-escape.ts +42 -0
  21. package/container_src/startup.sh +84 -0
  22. package/container_src/types.ts +131 -0
  23. package/package.json +5 -9
  24. package/src/client.ts +333 -1413
  25. package/src/errors.ts +218 -0
  26. package/src/index.ts +60 -10
  27. package/src/interpreter-types.ts +383 -0
  28. package/src/interpreter.ts +150 -0
  29. package/src/jupyter-client.ts +349 -0
  30. package/src/request-handler.ts +69 -20
  31. package/src/sandbox.ts +594 -105
  32. package/src/security.ts +113 -0
  33. package/src/sse-parser.ts +147 -0
  34. package/src/types.ts +502 -0
  35. package/tsconfig.json +1 -1
  36. package/tests/client.example.ts +0 -308
  37. package/tests/connection-test.ts +0 -81
  38. package/tests/simple-test.ts +0 -81
  39. package/tests/test1.ts +0 -281
  40. package/tests/test2.ts +0 -929
package/src/client.ts CHANGED
@@ -1,37 +1,23 @@
1
+ import type { ExecuteRequest } from "../container_src/types";
1
2
  import type { Sandbox } from "./index";
3
+ import type {
4
+ BaseExecOptions,
5
+ DeleteFileResponse,
6
+ ExecuteResponse,
7
+ GetProcessLogsResponse,
8
+ GetProcessResponse,
9
+ GitCheckoutResponse,
10
+ ListFilesResponse,
11
+ ListProcessesResponse,
12
+ MkdirResponse,
13
+ MoveFileResponse,
14
+ ReadFileResponse,
15
+ RenameFileResponse,
16
+ StartProcessRequest,
17
+ StartProcessResponse,
18
+ WriteFileResponse,
19
+ } from "./types";
2
20
 
3
- interface ExecuteRequest {
4
- command: string;
5
- args?: string[];
6
- sessionId?: string;
7
- background?: boolean;
8
- }
9
-
10
- export interface ExecuteResponse {
11
- success: boolean;
12
- stdout: string;
13
- stderr: string;
14
- exitCode: number;
15
- command: string;
16
- args: string[];
17
- timestamp: string;
18
- }
19
-
20
- interface SessionResponse {
21
- sessionId: string;
22
- message: string;
23
- timestamp: string;
24
- }
25
-
26
- interface SessionListResponse {
27
- sessions: Array<{
28
- sessionId: string;
29
- hasActiveProcess: boolean;
30
- createdAt: string;
31
- }>;
32
- count: number;
33
- timestamp: string;
34
- }
35
21
 
36
22
  interface CommandsResponse {
37
23
  availableCommands: string[];
@@ -42,104 +28,62 @@ interface GitCheckoutRequest {
42
28
  repoUrl: string;
43
29
  branch?: string;
44
30
  targetDir?: string;
45
- sessionId?: string;
31
+ sessionId: string;
46
32
  }
47
33
 
48
- export interface GitCheckoutResponse {
49
- success: boolean;
50
- stdout: string;
51
- stderr: string;
52
- exitCode: number;
53
- repoUrl: string;
54
- branch: string;
55
- targetDir: string;
56
- timestamp: string;
57
- }
58
34
 
59
35
  interface MkdirRequest {
60
36
  path: string;
61
37
  recursive?: boolean;
62
- sessionId?: string;
38
+ sessionId: string;
63
39
  }
64
40
 
65
- export interface MkdirResponse {
66
- success: boolean;
67
- stdout: string;
68
- stderr: string;
69
- exitCode: number;
70
- path: string;
71
- recursive: boolean;
72
- timestamp: string;
73
- }
74
41
 
75
42
  interface WriteFileRequest {
76
43
  path: string;
77
44
  content: string;
78
45
  encoding?: string;
79
- sessionId?: string;
46
+ sessionId: string;
80
47
  }
81
48
 
82
- export interface WriteFileResponse {
83
- success: boolean;
84
- exitCode: number;
85
- path: string;
86
- timestamp: string;
87
- }
88
49
 
89
50
  interface ReadFileRequest {
90
51
  path: string;
91
52
  encoding?: string;
92
- sessionId?: string;
53
+ sessionId: string;
93
54
  }
94
55
 
95
- export interface ReadFileResponse {
96
- success: boolean;
97
- exitCode: number;
98
- path: string;
99
- content: string;
100
- timestamp: string;
101
- }
102
56
 
103
57
  interface DeleteFileRequest {
104
58
  path: string;
105
- sessionId?: string;
59
+ sessionId: string;
106
60
  }
107
61
 
108
- export interface DeleteFileResponse {
109
- success: boolean;
110
- exitCode: number;
111
- path: string;
112
- timestamp: string;
113
- }
114
62
 
115
63
  interface RenameFileRequest {
116
64
  oldPath: string;
117
65
  newPath: string;
118
- sessionId?: string;
66
+ sessionId: string;
119
67
  }
120
68
 
121
- export interface RenameFileResponse {
122
- success: boolean;
123
- exitCode: number;
124
- oldPath: string;
125
- newPath: string;
126
- timestamp: string;
127
- }
128
69
 
129
70
  interface MoveFileRequest {
130
71
  sourcePath: string;
131
72
  destinationPath: string;
132
- sessionId?: string;
73
+ sessionId: string;
133
74
  }
134
75
 
135
- export interface MoveFileResponse {
136
- success: boolean;
137
- exitCode: number;
138
- sourcePath: string;
139
- destinationPath: string;
140
- timestamp: string;
76
+
77
+ interface ListFilesRequest {
78
+ path: string;
79
+ options?: {
80
+ recursive?: boolean;
81
+ includeHidden?: boolean;
82
+ };
83
+ sessionId: string;
141
84
  }
142
85
 
86
+
143
87
  interface PreviewInfo {
144
88
  url: string;
145
89
  port: number;
@@ -176,32 +120,11 @@ interface PingResponse {
176
120
  timestamp: string;
177
121
  }
178
122
 
179
- interface StreamEvent {
180
- type: "command_start" | "output" | "command_complete" | "error";
181
- command?: string;
182
- args?: string[];
183
- stream?: "stdout" | "stderr";
184
- data?: string;
185
- message?: string;
186
- path?: string;
187
- oldPath?: string;
188
- newPath?: string;
189
- sourcePath?: string;
190
- destinationPath?: string;
191
- content?: string;
192
- success?: boolean;
193
- exitCode?: number;
194
- stdout?: string;
195
- stderr?: string;
196
- error?: string;
197
- timestamp?: string;
198
- }
199
-
200
123
  interface HttpClientOptions {
201
124
  stub?: Sandbox;
202
125
  baseUrl?: string;
203
126
  port?: number;
204
- onCommandStart?: (command: string, args: string[]) => void;
127
+ onCommandStart?: (command: string) => void;
205
128
  onOutput?: (
206
129
  stream: "stdout" | "stderr",
207
130
  data: string,
@@ -212,17 +135,14 @@ interface HttpClientOptions {
212
135
  exitCode: number,
213
136
  stdout: string,
214
137
  stderr: string,
215
- command: string,
216
- args: string[]
138
+ command: string
217
139
  ) => void;
218
- onError?: (error: string, command?: string, args?: string[]) => void;
219
- onStreamEvent?: (event: StreamEvent) => void;
140
+ onError?: (error: string, command?: string) => void;
220
141
  }
221
142
 
222
143
  export class HttpClient {
223
144
  private baseUrl: string;
224
145
  private options: HttpClientOptions;
225
- private sessionId: string | null = null;
226
146
 
227
147
  constructor(options: HttpClientOptions = {}) {
228
148
  this.options = {
@@ -231,7 +151,7 @@ export class HttpClient {
231
151
  this.baseUrl = this.options.baseUrl!;
232
152
  }
233
153
 
234
- private async doFetch(
154
+ protected async doFetch(
235
155
  path: string,
236
156
  options?: RequestInit
237
157
  ): Promise<Response> {
@@ -271,123 +191,53 @@ export class HttpClient {
271
191
  throw error;
272
192
  }
273
193
  }
274
- // Public methods to set event handlers
275
- setOnOutput(
276
- handler: (
277
- stream: "stdout" | "stderr",
278
- data: string,
279
- command: string
280
- ) => void
281
- ): void {
282
- this.options.onOutput = handler;
283
- }
284
-
285
- setOnCommandComplete(
286
- handler: (
287
- success: boolean,
288
- exitCode: number,
289
- stdout: string,
290
- stderr: string,
291
- command: string,
292
- args: string[]
293
- ) => void
294
- ): void {
295
- this.options.onCommandComplete = handler;
296
- }
297
-
298
- setOnStreamEvent(handler: (event: StreamEvent) => void): void {
299
- this.options.onStreamEvent = handler;
300
- }
301
194
 
302
- // Public getter methods
303
- getOnOutput():
304
- | ((stream: "stdout" | "stderr", data: string, command: string) => void)
305
- | undefined {
306
- return this.options.onOutput;
307
- }
308
-
309
- getOnCommandComplete():
310
- | ((
311
- success: boolean,
312
- exitCode: number,
313
- stdout: string,
314
- stderr: string,
315
- command: string,
316
- args: string[]
317
- ) => void)
318
- | undefined {
319
- return this.options.onCommandComplete;
320
- }
321
-
322
- getOnStreamEvent(): ((event: StreamEvent) => void) | undefined {
323
- return this.options.onStreamEvent;
324
- }
325
-
326
- async createSession(): Promise<string> {
195
+ async createSession(options: {
196
+ id: string;
197
+ env?: Record<string, string>;
198
+ cwd?: string;
199
+ isolation?: boolean;
200
+ }): Promise<{ success: boolean; id: string; message: string }> {
327
201
  try {
328
202
  const response = await this.doFetch(`/api/session/create`, {
329
- headers: {
330
- "Content-Type": "application/json",
331
- },
332
203
  method: "POST",
333
- });
334
-
335
- if (!response.ok) {
336
- throw new Error(`HTTP error! status: ${response.status}`);
337
- }
338
-
339
- const data: SessionResponse = await response.json();
340
- this.sessionId = data.sessionId;
341
- console.log(`[HTTP Client] Created session: ${this.sessionId}`);
342
- return this.sessionId;
343
- } catch (error) {
344
- console.error("[HTTP Client] Error creating session:", error);
345
- throw error;
346
- }
347
- }
348
-
349
- async listSessions(): Promise<SessionListResponse> {
350
- try {
351
- const response = await this.doFetch(`/api/session/list`, {
352
204
  headers: {
353
205
  "Content-Type": "application/json",
354
206
  },
355
- method: "GET",
207
+ body: JSON.stringify(options),
356
208
  });
357
209
 
358
210
  if (!response.ok) {
359
- throw new Error(`HTTP error! status: ${response.status}`);
211
+ const errorData = (await response.json().catch(() => ({}))) as {
212
+ error?: string;
213
+ };
214
+ throw new Error(
215
+ errorData.error || `Failed to create session: ${response.status}`
216
+ );
360
217
  }
361
218
 
362
- const data: SessionListResponse = await response.json();
363
- console.log(`[HTTP Client] Listed ${data.count} sessions`);
219
+ const data = await response.json() as { success: boolean; id: string; message: string };
220
+ console.log(`[HTTP Client] Session created: ${options.id}`);
364
221
  return data;
365
222
  } catch (error) {
366
- console.error("[HTTP Client] Error listing sessions:", error);
223
+ console.error("[HTTP Client] Error creating session:", error);
367
224
  throw error;
368
225
  }
369
226
  }
370
227
 
371
- async execute(
228
+ async exec(
229
+ sessionId: string,
372
230
  command: string,
373
- args: string[] = [],
374
- sessionId?: string,
375
- background: boolean = false,
231
+ options?: Pick<BaseExecOptions, "cwd" | "env">
376
232
  ): Promise<ExecuteResponse> {
377
233
  try {
378
- const targetSessionId = sessionId || this.sessionId;
379
-
234
+ // Always use session-specific endpoint
380
235
  const response = await this.doFetch(`/api/execute`, {
381
- body: JSON.stringify({
382
- args,
383
- command,
384
- background,
385
- sessionId: targetSessionId,
386
- } as ExecuteRequest),
236
+ method: "POST",
387
237
  headers: {
388
238
  "Content-Type": "application/json",
389
239
  },
390
- method: "POST",
240
+ body: JSON.stringify({ id: sessionId, command }),
391
241
  });
392
242
 
393
243
  if (!response.ok) {
@@ -395,57 +245,57 @@ export class HttpClient {
395
245
  error?: string;
396
246
  };
397
247
  throw new Error(
398
- errorData.error || `HTTP error! status: ${response.status}`
248
+ errorData.error || `Failed to execute in session: ${response.status}`
399
249
  );
400
250
  }
401
251
 
402
- const data: ExecuteResponse = await response.json();
252
+ const data = await response.json() as { stdout: string; stderr: string; exitCode: number; success: boolean };
403
253
  console.log(
404
- `[HTTP Client] Command executed: ${command}, Success: ${data.success}`
254
+ `[HTTP Client] Command executed in session ${sessionId}: ${command}`
405
255
  );
256
+
257
+ // Convert to ExecuteResponse format for consistency
258
+ const executeResponse: ExecuteResponse = {
259
+ ...data,
260
+ command,
261
+ timestamp: new Date().toISOString()
262
+ };
406
263
 
407
264
  // Call the callback if provided
408
265
  this.options.onCommandComplete?.(
409
- data.success,
410
- data.exitCode,
411
- data.stdout,
412
- data.stderr,
413
- data.command,
414
- data.args
266
+ executeResponse.success,
267
+ executeResponse.exitCode,
268
+ executeResponse.stdout,
269
+ executeResponse.stderr,
270
+ executeResponse.command
415
271
  );
416
272
 
417
- return data;
273
+ return executeResponse;
418
274
  } catch (error) {
419
- console.error("[HTTP Client] Error executing command:", error);
275
+ console.error("[HTTP Client] Error executing in session:", error);
420
276
  this.options.onError?.(
421
277
  error instanceof Error ? error.message : "Unknown error",
422
- command,
423
- args
278
+ command
424
279
  );
425
280
  throw error;
426
281
  }
427
282
  }
428
283
 
429
- async executeStream(
430
- command: string,
431
- args: string[] = [],
432
- sessionId?: string,
433
- background: boolean = false
434
- ): Promise<void> {
284
+ async execStream(
285
+ sessionId: string,
286
+ command: string
287
+ ): Promise<ReadableStream<Uint8Array>> {
435
288
  try {
436
- const targetSessionId = sessionId || this.sessionId;
437
-
289
+ // Always use session-specific streaming endpoint
438
290
  const response = await this.doFetch(`/api/execute/stream`, {
439
- body: JSON.stringify({
440
- args,
441
- command,
442
- background,
443
- sessionId: targetSessionId,
444
- }),
291
+ method: "POST",
445
292
  headers: {
446
293
  "Content-Type": "application/json",
447
294
  },
448
- method: "POST",
295
+ body: JSON.stringify({
296
+ id: sessionId,
297
+ command
298
+ }),
449
299
  });
450
300
 
451
301
  if (!response.ok) {
@@ -453,121 +303,37 @@ export class HttpClient {
453
303
  error?: string;
454
304
  };
455
305
  throw new Error(
456
- errorData.error || `HTTP error! status: ${response.status}`
306
+ errorData.error || `Failed to stream execute in session: ${response.status}`
457
307
  );
458
308
  }
459
309
 
460
310
  if (!response.body) {
461
- throw new Error("No response body for streaming request");
311
+ throw new Error("No response body for streaming execution");
462
312
  }
463
313
 
464
- const reader = response.body.getReader();
465
- const decoder = new TextDecoder();
466
-
467
- try {
468
- while (true) {
469
- const { done, value } = await reader.read();
470
-
471
- if (done) {
472
- break;
473
- }
474
-
475
- const chunk = decoder.decode(value, { stream: true });
476
- const lines = chunk.split("\n");
477
-
478
- for (const line of lines) {
479
- if (line.startsWith("data: ")) {
480
- try {
481
- const eventData = line.slice(6); // Remove 'data: ' prefix
482
- const event: StreamEvent = JSON.parse(eventData);
483
-
484
- console.log(`[HTTP Client] Stream event: ${event.type}`);
485
- this.options.onStreamEvent?.(event);
486
-
487
- switch (event.type) {
488
- case "command_start":
489
- console.log(
490
- `[HTTP Client] Command started: ${event.command
491
- } ${event.args?.join(" ")}`
492
- );
493
- this.options.onCommandStart?.(
494
- event.command!,
495
- event.args || []
496
- );
497
- break;
498
-
499
- case "output":
500
- console.log(`[${event.stream}] ${event.data}`);
501
- this.options.onOutput?.(
502
- event.stream!,
503
- event.data!,
504
- event.command!
505
- );
506
- break;
507
-
508
- case "command_complete":
509
- console.log(
510
- `[HTTP Client] Command completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
511
- );
512
- this.options.onCommandComplete?.(
513
- event.success!,
514
- event.exitCode!,
515
- event.stdout!,
516
- event.stderr!,
517
- event.command!,
518
- event.args || []
519
- );
520
- break;
521
-
522
- case "error":
523
- console.error(
524
- `[HTTP Client] Command error: ${event.error}`
525
- );
526
- this.options.onError?.(
527
- event.error!,
528
- event.command,
529
- event.args
530
- );
531
- break;
532
- }
533
- } catch (parseError) {
534
- console.warn(
535
- "[HTTP Client] Failed to parse stream event:",
536
- parseError
537
- );
538
- }
539
- }
540
- }
541
- }
542
- } finally {
543
- reader.releaseLock();
544
- }
545
- } catch (error) {
546
- console.error("[HTTP Client] Error in streaming execution:", error);
547
- this.options.onError?.(
548
- error instanceof Error ? error.message : "Unknown error",
549
- command,
550
- args
314
+ console.log(
315
+ `[HTTP Client] Started streaming command in session ${sessionId}: ${command}`
551
316
  );
317
+ return response.body;
318
+ } catch (error) {
319
+ console.error("[HTTP Client] Error streaming execute in session:", error);
552
320
  throw error;
553
321
  }
554
322
  }
555
323
 
556
324
  async gitCheckout(
557
325
  repoUrl: string,
326
+ sessionId: string,
558
327
  branch: string = "main",
559
- targetDir?: string,
560
- sessionId?: string
328
+ targetDir?: string
561
329
  ): Promise<GitCheckoutResponse> {
562
330
  try {
563
- const targetSessionId = sessionId || this.sessionId;
564
-
565
331
  const response = await this.doFetch(`/api/git/checkout`, {
566
332
  body: JSON.stringify({
567
333
  branch,
568
334
  repoUrl,
569
- sessionId: targetSessionId,
570
335
  targetDir,
336
+ sessionId,
571
337
  } as GitCheckoutRequest),
572
338
  headers: {
573
339
  "Content-Type": "application/json",
@@ -596,148 +362,17 @@ export class HttpClient {
596
362
  }
597
363
  }
598
364
 
599
- async gitCheckoutStream(
600
- repoUrl: string,
601
- branch: string = "main",
602
- targetDir?: string,
603
- sessionId?: string
604
- ): Promise<void> {
605
- try {
606
- const targetSessionId = sessionId || this.sessionId;
607
-
608
- const response = await this.doFetch(`/api/git/checkout/stream`, {
609
- body: JSON.stringify({
610
- branch,
611
- repoUrl,
612
- sessionId: targetSessionId,
613
- targetDir,
614
- }),
615
- headers: {
616
- "Content-Type": "application/json",
617
- },
618
- method: "POST",
619
- });
620
-
621
- if (!response.ok) {
622
- const errorData = (await response.json().catch(() => ({}))) as {
623
- error?: string;
624
- };
625
- throw new Error(
626
- errorData.error || `HTTP error! status: ${response.status}`
627
- );
628
- }
629
-
630
- if (!response.body) {
631
- throw new Error("No response body for streaming request");
632
- }
633
-
634
- const reader = response.body.getReader();
635
- const decoder = new TextDecoder();
636
-
637
- try {
638
- while (true) {
639
- const { done, value } = await reader.read();
640
-
641
- if (done) {
642
- break;
643
- }
644
-
645
- const chunk = decoder.decode(value, { stream: true });
646
- const lines = chunk.split("\n");
647
-
648
- for (const line of lines) {
649
- if (line.startsWith("data: ")) {
650
- try {
651
- const eventData = line.slice(6); // Remove 'data: ' prefix
652
- const event: StreamEvent = JSON.parse(eventData);
653
-
654
- console.log(
655
- `[HTTP Client] Git checkout stream event: ${event.type}`
656
- );
657
- this.options.onStreamEvent?.(event);
658
-
659
- switch (event.type) {
660
- case "command_start":
661
- console.log(
662
- `[HTTP Client] Git checkout started: ${event.command
663
- } ${event.args?.join(" ")}`
664
- );
665
- this.options.onCommandStart?.(
666
- event.command!,
667
- event.args || []
668
- );
669
- break;
670
-
671
- case "output":
672
- console.log(`[${event.stream}] ${event.data}`);
673
- this.options.onOutput?.(
674
- event.stream!,
675
- event.data!,
676
- event.command!
677
- );
678
- break;
679
-
680
- case "command_complete":
681
- console.log(
682
- `[HTTP Client] Git checkout completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
683
- );
684
- this.options.onCommandComplete?.(
685
- event.success!,
686
- event.exitCode!,
687
- event.stdout!,
688
- event.stderr!,
689
- event.command!,
690
- event.args || []
691
- );
692
- break;
693
-
694
- case "error":
695
- console.error(
696
- `[HTTP Client] Git checkout error: ${event.error}`
697
- );
698
- this.options.onError?.(
699
- event.error!,
700
- event.command,
701
- event.args
702
- );
703
- break;
704
- }
705
- } catch (parseError) {
706
- console.warn(
707
- "[HTTP Client] Failed to parse git checkout stream event:",
708
- parseError
709
- );
710
- }
711
- }
712
- }
713
- }
714
- } finally {
715
- reader.releaseLock();
716
- }
717
- } catch (error) {
718
- console.error("[HTTP Client] Error in streaming git checkout:", error);
719
- this.options.onError?.(
720
- error instanceof Error ? error.message : "Unknown error",
721
- "git clone",
722
- [branch, repoUrl, targetDir || ""]
723
- );
724
- throw error;
725
- }
726
- }
727
-
728
365
  async mkdir(
729
366
  path: string,
730
367
  recursive: boolean = false,
731
- sessionId?: string
368
+ sessionId: string
732
369
  ): Promise<MkdirResponse> {
733
370
  try {
734
- const targetSessionId = sessionId || this.sessionId;
735
-
736
371
  const response = await this.doFetch(`/api/mkdir`, {
737
372
  body: JSON.stringify({
738
373
  path,
739
374
  recursive,
740
- sessionId: targetSessionId,
375
+ sessionId,
741
376
  } as MkdirRequest),
742
377
  headers: {
743
378
  "Content-Type": "application/json",
@@ -756,7 +391,7 @@ export class HttpClient {
756
391
 
757
392
  const data: MkdirResponse = await response.json();
758
393
  console.log(
759
- `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}`
394
+ `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}${sessionId ? ` in session: ${sessionId}` : ''}`
760
395
  );
761
396
 
762
397
  return data;
@@ -766,20 +401,20 @@ export class HttpClient {
766
401
  }
767
402
  }
768
403
 
769
- async mkdirStream(
404
+ async writeFile(
770
405
  path: string,
771
- recursive: boolean = false,
772
- sessionId?: string
773
- ): Promise<void> {
406
+ content: string,
407
+ encoding: string = "utf-8",
408
+ sessionId: string
409
+ ): Promise<WriteFileResponse> {
774
410
  try {
775
- const targetSessionId = sessionId || this.sessionId;
776
-
777
- const response = await this.doFetch(`/api/mkdir/stream`, {
411
+ const response = await this.doFetch(`/api/write`, {
778
412
  body: JSON.stringify({
413
+ content,
414
+ encoding,
779
415
  path,
780
- recursive,
781
- sessionId: targetSessionId,
782
- } as MkdirRequest),
416
+ sessionId,
417
+ } as WriteFileRequest),
783
418
  headers: {
784
419
  "Content-Type": "application/json",
785
420
  },
@@ -795,116 +430,30 @@ export class HttpClient {
795
430
  );
796
431
  }
797
432
 
798
- if (!response.body) {
799
- throw new Error("No response body for streaming request");
800
- }
433
+ const data: WriteFileResponse = await response.json();
434
+ console.log(
435
+ `[HTTP Client] File written: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
436
+ );
801
437
 
802
- const reader = response.body.getReader();
803
- const decoder = new TextDecoder();
804
-
805
- try {
806
- while (true) {
807
- const { done, value } = await reader.read();
808
-
809
- if (done) {
810
- break;
811
- }
812
-
813
- const chunk = decoder.decode(value, { stream: true });
814
- const lines = chunk.split("\n");
815
-
816
- for (const line of lines) {
817
- if (line.startsWith("data: ")) {
818
- try {
819
- const eventData = line.slice(6); // Remove 'data: ' prefix
820
- const event: StreamEvent = JSON.parse(eventData);
821
-
822
- console.log(`[HTTP Client] Mkdir stream event: ${event.type}`);
823
- this.options.onStreamEvent?.(event);
824
-
825
- switch (event.type) {
826
- case "command_start":
827
- console.log(
828
- `[HTTP Client] Mkdir started: ${event.command
829
- } ${event.args?.join(" ")}`
830
- );
831
- this.options.onCommandStart?.(
832
- event.command!,
833
- event.args || []
834
- );
835
- break;
836
-
837
- case "output":
838
- console.log(`[${event.stream}] ${event.data}`);
839
- this.options.onOutput?.(
840
- event.stream!,
841
- event.data!,
842
- event.command!
843
- );
844
- break;
845
-
846
- case "command_complete":
847
- console.log(
848
- `[HTTP Client] Mkdir completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
849
- );
850
- this.options.onCommandComplete?.(
851
- event.success!,
852
- event.exitCode!,
853
- event.stdout!,
854
- event.stderr!,
855
- event.command!,
856
- event.args || []
857
- );
858
- break;
859
-
860
- case "error":
861
- console.error(`[HTTP Client] Mkdir error: ${event.error}`);
862
- this.options.onError?.(
863
- event.error!,
864
- event.command,
865
- event.args
866
- );
867
- break;
868
- }
869
- } catch (parseError) {
870
- console.warn(
871
- "[HTTP Client] Failed to parse mkdir stream event:",
872
- parseError
873
- );
874
- }
875
- }
876
- }
877
- }
878
- } finally {
879
- reader.releaseLock();
880
- }
438
+ return data;
881
439
  } catch (error) {
882
- console.error("[HTTP Client] Error in streaming mkdir:", error);
883
- this.options.onError?.(
884
- error instanceof Error ? error.message : "Unknown error",
885
- "mkdir",
886
- recursive ? ["-p", path] : [path]
887
- );
440
+ console.error("[HTTP Client] Error writing file:", error);
888
441
  throw error;
889
442
  }
890
443
  }
891
444
 
892
- async writeFile(
445
+ async readFile(
893
446
  path: string,
894
- content: string,
895
447
  encoding: string = "utf-8",
896
- sessionId?: string
897
- ): Promise<WriteFileResponse> {
448
+ sessionId: string
449
+ ): Promise<ReadFileResponse> {
898
450
  try {
899
- const targetSessionId = sessionId || this.sessionId;
900
-
901
- const response = await this.doFetch(`/api/write`, {
451
+ const response = await this.doFetch(`/api/read`, {
902
452
  body: JSON.stringify({
903
- content,
904
453
  encoding,
905
454
  path,
906
- sessionId: targetSessionId,
907
- } as WriteFileRequest),
455
+ sessionId,
456
+ } as ReadFileRequest),
908
457
  headers: {
909
458
  "Content-Type": "application/json",
910
459
  },
@@ -920,34 +469,28 @@ export class HttpClient {
920
469
  );
921
470
  }
922
471
 
923
- const data: WriteFileResponse = await response.json();
472
+ const data: ReadFileResponse = await response.json();
924
473
  console.log(
925
- `[HTTP Client] File written: ${path}, Success: ${data.success}`
474
+ `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}${sessionId ? ` in session: ${sessionId}` : ''}`
926
475
  );
927
476
 
928
477
  return data;
929
478
  } catch (error) {
930
- console.error("[HTTP Client] Error writing file:", error);
479
+ console.error("[HTTP Client] Error reading file:", error);
931
480
  throw error;
932
481
  }
933
482
  }
934
483
 
935
- async writeFileStream(
484
+ async deleteFile(
936
485
  path: string,
937
- content: string,
938
- encoding: string = "utf-8",
939
- sessionId?: string
940
- ): Promise<void> {
486
+ sessionId: string
487
+ ): Promise<DeleteFileResponse> {
941
488
  try {
942
- const targetSessionId = sessionId || this.sessionId;
943
-
944
- const response = await this.doFetch(`/api/write/stream`, {
489
+ const response = await this.doFetch(`/api/delete`, {
945
490
  body: JSON.stringify({
946
- content,
947
- encoding,
948
491
  path,
949
- sessionId: targetSessionId,
950
- } as WriteFileRequest),
492
+ sessionId,
493
+ } as DeleteFileRequest),
951
494
  headers: {
952
495
  "Content-Type": "application/json",
953
496
  },
@@ -963,114 +506,30 @@ export class HttpClient {
963
506
  );
964
507
  }
965
508
 
966
- if (!response.body) {
967
- throw new Error("No response body for streaming request");
968
- }
509
+ const data: DeleteFileResponse = await response.json();
510
+ console.log(
511
+ `[HTTP Client] File deleted: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
512
+ );
969
513
 
970
- const reader = response.body.getReader();
971
- const decoder = new TextDecoder();
972
-
973
- try {
974
- while (true) {
975
- const { done, value } = await reader.read();
976
-
977
- if (done) {
978
- break;
979
- }
980
-
981
- const chunk = decoder.decode(value, { stream: true });
982
- const lines = chunk.split("\n");
983
-
984
- for (const line of lines) {
985
- if (line.startsWith("data: ")) {
986
- try {
987
- const eventData = line.slice(6); // Remove 'data: ' prefix
988
- const event: StreamEvent = JSON.parse(eventData);
989
-
990
- console.log(
991
- `[HTTP Client] Write file stream event: ${event.type}`
992
- );
993
- this.options.onStreamEvent?.(event);
994
-
995
- switch (event.type) {
996
- case "command_start":
997
- console.log(
998
- `[HTTP Client] Write file started: ${event.path}`
999
- );
1000
- this.options.onCommandStart?.("write", [
1001
- path,
1002
- content,
1003
- encoding,
1004
- ]);
1005
- break;
1006
-
1007
- case "output":
1008
- console.log(`[output] ${event.message}`);
1009
- this.options.onOutput?.("stdout", event.message!, "write");
1010
- break;
1011
-
1012
- case "command_complete":
1013
- console.log(
1014
- `[HTTP Client] Write file completed: ${event.path}, Success: ${event.success}`
1015
- );
1016
- this.options.onCommandComplete?.(
1017
- event.success!,
1018
- 0,
1019
- "",
1020
- "",
1021
- "write",
1022
- [path, content, encoding]
1023
- );
1024
- break;
1025
-
1026
- case "error":
1027
- console.error(
1028
- `[HTTP Client] Write file error: ${event.error}`
1029
- );
1030
- this.options.onError?.(event.error!, "write", [
1031
- path,
1032
- content,
1033
- encoding,
1034
- ]);
1035
- break;
1036
- }
1037
- } catch (parseError) {
1038
- console.warn(
1039
- "[HTTP Client] Failed to parse write file stream event:",
1040
- parseError
1041
- );
1042
- }
1043
- }
1044
- }
1045
- }
1046
- } finally {
1047
- reader.releaseLock();
1048
- }
514
+ return data;
1049
515
  } catch (error) {
1050
- console.error("[HTTP Client] Error in streaming write file:", error);
1051
- this.options.onError?.(
1052
- error instanceof Error ? error.message : "Unknown error",
1053
- "write",
1054
- [path, content, encoding]
1055
- );
516
+ console.error("[HTTP Client] Error deleting file:", error);
1056
517
  throw error;
1057
518
  }
1058
519
  }
1059
520
 
1060
- async readFile(
1061
- path: string,
1062
- encoding: string = "utf-8",
1063
- sessionId?: string
1064
- ): Promise<ReadFileResponse> {
521
+ async renameFile(
522
+ oldPath: string,
523
+ newPath: string,
524
+ sessionId: string
525
+ ): Promise<RenameFileResponse> {
1065
526
  try {
1066
- const targetSessionId = sessionId || this.sessionId;
1067
-
1068
- const response = await this.doFetch(`/api/read`, {
527
+ const response = await this.doFetch(`/api/rename`, {
1069
528
  body: JSON.stringify({
1070
- encoding,
1071
- path,
1072
- sessionId: targetSessionId,
1073
- } as ReadFileRequest),
529
+ newPath,
530
+ oldPath,
531
+ sessionId,
532
+ } as RenameFileRequest),
1074
533
  headers: {
1075
534
  "Content-Type": "application/json",
1076
535
  },
@@ -1086,32 +545,30 @@ export class HttpClient {
1086
545
  );
1087
546
  }
1088
547
 
1089
- const data: ReadFileResponse = await response.json();
548
+ const data: RenameFileResponse = await response.json();
1090
549
  console.log(
1091
- `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}`
550
+ `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1092
551
  );
1093
552
 
1094
553
  return data;
1095
554
  } catch (error) {
1096
- console.error("[HTTP Client] Error reading file:", error);
555
+ console.error("[HTTP Client] Error renaming file:", error);
1097
556
  throw error;
1098
557
  }
1099
558
  }
1100
559
 
1101
- async readFileStream(
1102
- path: string,
1103
- encoding: string = "utf-8",
1104
- sessionId?: string
1105
- ): Promise<void> {
560
+ async moveFile(
561
+ sourcePath: string,
562
+ destinationPath: string,
563
+ sessionId: string
564
+ ): Promise<MoveFileResponse> {
1106
565
  try {
1107
- const targetSessionId = sessionId || this.sessionId;
1108
-
1109
- const response = await this.doFetch(`/api/read/stream`, {
566
+ const response = await this.doFetch(`/api/move`, {
1110
567
  body: JSON.stringify({
1111
- encoding,
1112
- path,
1113
- sessionId: targetSessionId,
1114
- } as ReadFileRequest),
568
+ destinationPath,
569
+ sourcePath,
570
+ sessionId,
571
+ } as MoveFileRequest),
1115
572
  headers: {
1116
573
  "Content-Type": "application/json",
1117
574
  },
@@ -1127,104 +584,33 @@ export class HttpClient {
1127
584
  );
1128
585
  }
1129
586
 
1130
- if (!response.body) {
1131
- throw new Error("No response body for streaming request");
1132
- }
587
+ const data: MoveFileResponse = await response.json();
588
+ console.log(
589
+ `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
590
+ );
1133
591
 
1134
- const reader = response.body.getReader();
1135
- const decoder = new TextDecoder();
1136
-
1137
- try {
1138
- while (true) {
1139
- const { done, value } = await reader.read();
1140
-
1141
- if (done) {
1142
- break;
1143
- }
1144
-
1145
- const chunk = decoder.decode(value, { stream: true });
1146
- const lines = chunk.split("\n");
1147
-
1148
- for (const line of lines) {
1149
- if (line.startsWith("data: ")) {
1150
- try {
1151
- const eventData = line.slice(6); // Remove 'data: ' prefix
1152
- const event: StreamEvent = JSON.parse(eventData);
1153
-
1154
- console.log(
1155
- `[HTTP Client] Read file stream event: ${event.type}`
1156
- );
1157
- this.options.onStreamEvent?.(event);
1158
-
1159
- switch (event.type) {
1160
- case "command_start":
1161
- console.log(
1162
- `[HTTP Client] Read file started: ${event.path}`
1163
- );
1164
- this.options.onCommandStart?.("read", [path, encoding]);
1165
- break;
1166
-
1167
- case "command_complete":
1168
- console.log(
1169
- `[HTTP Client] Read file completed: ${event.path
1170
- }, Success: ${event.success}, Content length: ${event.content?.length || 0
1171
- }`
1172
- );
1173
- this.options.onCommandComplete?.(
1174
- event.success!,
1175
- 0,
1176
- event.content || "",
1177
- "",
1178
- "read",
1179
- [path, encoding]
1180
- );
1181
- break;
1182
-
1183
- case "error":
1184
- console.error(
1185
- `[HTTP Client] Read file error: ${event.error}`
1186
- );
1187
- this.options.onError?.(event.error!, "read", [
1188
- path,
1189
- encoding,
1190
- ]);
1191
- break;
1192
- }
1193
- } catch (parseError) {
1194
- console.warn(
1195
- "[HTTP Client] Failed to parse read file stream event:",
1196
- parseError
1197
- );
1198
- }
1199
- }
1200
- }
1201
- }
1202
- } finally {
1203
- reader.releaseLock();
1204
- }
592
+ return data;
1205
593
  } catch (error) {
1206
- console.error("[HTTP Client] Error in streaming read file:", error);
1207
- this.options.onError?.(
1208
- error instanceof Error ? error.message : "Unknown error",
1209
- "read",
1210
- [path, encoding]
1211
- );
594
+ console.error("[HTTP Client] Error moving file:", error);
1212
595
  throw error;
1213
596
  }
1214
597
  }
1215
598
 
1216
- async deleteFile(
599
+ async listFiles(
1217
600
  path: string,
1218
- sessionId?: string
1219
- ): Promise<DeleteFileResponse> {
601
+ sessionId: string,
602
+ options?: {
603
+ recursive?: boolean;
604
+ includeHidden?: boolean;
605
+ }
606
+ ): Promise<ListFilesResponse> {
1220
607
  try {
1221
- const targetSessionId = sessionId || this.sessionId;
1222
-
1223
- const response = await this.doFetch(`/api/delete`, {
608
+ const response = await this.doFetch(`/api/list-files`, {
1224
609
  body: JSON.stringify({
1225
610
  path,
1226
- sessionId: targetSessionId,
1227
- } as DeleteFileRequest),
611
+ options,
612
+ sessionId,
613
+ } as ListFilesRequest),
1228
614
  headers: {
1229
615
  "Content-Type": "application/json",
1230
616
  },
@@ -1240,27 +626,25 @@ export class HttpClient {
1240
626
  );
1241
627
  }
1242
628
 
1243
- const data: DeleteFileResponse = await response.json();
629
+ const data: ListFilesResponse = await response.json();
1244
630
  console.log(
1245
- `[HTTP Client] File deleted: ${path}, Success: ${data.success}`
631
+ `[HTTP Client] Listed ${data.files.length} files in: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1246
632
  );
1247
633
 
1248
634
  return data;
1249
635
  } catch (error) {
1250
- console.error("[HTTP Client] Error deleting file:", error);
636
+ console.error("[HTTP Client] Error listing files:", error);
1251
637
  throw error;
1252
638
  }
1253
639
  }
1254
640
 
1255
- async deleteFileStream(path: string, sessionId?: string): Promise<void> {
641
+ async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
1256
642
  try {
1257
- const targetSessionId = sessionId || this.sessionId;
1258
-
1259
- const response = await this.doFetch(`/api/delete/stream`, {
643
+ const response = await this.doFetch(`/api/expose-port`, {
1260
644
  body: JSON.stringify({
1261
- path,
1262
- sessionId: targetSessionId,
1263
- } as DeleteFileRequest),
645
+ port,
646
+ name,
647
+ }),
1264
648
  headers: {
1265
649
  "Content-Type": "application/json",
1266
650
  },
@@ -1271,110 +655,36 @@ export class HttpClient {
1271
655
  const errorData = (await response.json().catch(() => ({}))) as {
1272
656
  error?: string;
1273
657
  };
658
+ console.log(errorData);
1274
659
  throw new Error(
1275
660
  errorData.error || `HTTP error! status: ${response.status}`
1276
661
  );
1277
662
  }
1278
663
 
1279
- if (!response.body) {
1280
- throw new Error("No response body for streaming request");
1281
- }
664
+ const data: ExposePortResponse = await response.json();
665
+ console.log(
666
+ `[HTTP Client] Port exposed: ${port}${
667
+ name ? ` (${name})` : ""
668
+ }, Success: ${data.success}`
669
+ );
1282
670
 
1283
- const reader = response.body.getReader();
1284
- const decoder = new TextDecoder();
1285
-
1286
- try {
1287
- while (true) {
1288
- const { done, value } = await reader.read();
1289
-
1290
- if (done) {
1291
- break;
1292
- }
1293
-
1294
- const chunk = decoder.decode(value, { stream: true });
1295
- const lines = chunk.split("\n");
1296
-
1297
- for (const line of lines) {
1298
- if (line.startsWith("data: ")) {
1299
- try {
1300
- const eventData = line.slice(6); // Remove 'data: ' prefix
1301
- const event: StreamEvent = JSON.parse(eventData);
1302
-
1303
- console.log(
1304
- `[HTTP Client] Delete file stream event: ${event.type}`
1305
- );
1306
- this.options.onStreamEvent?.(event);
1307
-
1308
- switch (event.type) {
1309
- case "command_start":
1310
- console.log(
1311
- `[HTTP Client] Delete file started: ${event.path}`
1312
- );
1313
- this.options.onCommandStart?.("delete", [path]);
1314
- break;
1315
-
1316
- case "command_complete":
1317
- console.log(
1318
- `[HTTP Client] Delete file completed: ${event.path}, Success: ${event.success}`
1319
- );
1320
- this.options.onCommandComplete?.(
1321
- event.success!,
1322
- 0,
1323
- "",
1324
- "",
1325
- "delete",
1326
- [path]
1327
- );
1328
- break;
1329
-
1330
- case "error":
1331
- console.error(
1332
- `[HTTP Client] Delete file error: ${event.error}`
1333
- );
1334
- this.options.onError?.(event.error!, "delete", [path]);
1335
- break;
1336
- }
1337
- } catch (parseError) {
1338
- console.warn(
1339
- "[HTTP Client] Failed to parse delete file stream event:",
1340
- parseError
1341
- );
1342
- }
1343
- }
1344
- }
1345
- }
1346
- } finally {
1347
- reader.releaseLock();
1348
- }
671
+ return data;
1349
672
  } catch (error) {
1350
- console.error("[HTTP Client] Error in streaming delete file:", error);
1351
- this.options.onError?.(
1352
- error instanceof Error ? error.message : "Unknown error",
1353
- "delete",
1354
- [path]
1355
- );
673
+ console.error("[HTTP Client] Error exposing port:", error);
1356
674
  throw error;
1357
675
  }
1358
676
  }
1359
677
 
1360
- async renameFile(
1361
- oldPath: string,
1362
- newPath: string,
1363
- sessionId?: string
1364
- ): Promise<RenameFileResponse> {
678
+ async unexposePort(port: number): Promise<UnexposePortResponse> {
1365
679
  try {
1366
- const targetSessionId = sessionId || this.sessionId;
1367
-
1368
- const response = await this.doFetch(`/api/rename`, {
680
+ const response = await this.doFetch(`/api/unexpose-port`, {
1369
681
  body: JSON.stringify({
1370
- newPath,
1371
- oldPath,
1372
- sessionId: targetSessionId,
1373
- } as RenameFileRequest),
682
+ port,
683
+ }),
1374
684
  headers: {
1375
685
  "Content-Type": "application/json",
1376
686
  },
1377
- method: "POST",
687
+ method: "DELETE",
1378
688
  });
1379
689
 
1380
690
  if (!response.ok) {
@@ -1386,36 +696,25 @@ export class HttpClient {
1386
696
  );
1387
697
  }
1388
698
 
1389
- const data: RenameFileResponse = await response.json();
699
+ const data: UnexposePortResponse = await response.json();
1390
700
  console.log(
1391
- `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}`
701
+ `[HTTP Client] Port unexposed: ${port}, Success: ${data.success}`
1392
702
  );
1393
703
 
1394
704
  return data;
1395
705
  } catch (error) {
1396
- console.error("[HTTP Client] Error renaming file:", error);
706
+ console.error("[HTTP Client] Error unexposing port:", error);
1397
707
  throw error;
1398
708
  }
1399
709
  }
1400
710
 
1401
- async renameFileStream(
1402
- oldPath: string,
1403
- newPath: string,
1404
- sessionId?: string
1405
- ): Promise<void> {
711
+ async getExposedPorts(): Promise<GetExposedPortsResponse> {
1406
712
  try {
1407
- const targetSessionId = sessionId || this.sessionId;
1408
-
1409
- const response = await this.doFetch(`/api/rename/stream`, {
1410
- body: JSON.stringify({
1411
- newPath,
1412
- oldPath,
1413
- sessionId: targetSessionId,
1414
- } as RenameFileRequest),
713
+ const response = await this.doFetch(`/api/exposed-ports`, {
1415
714
  headers: {
1416
715
  "Content-Type": "application/json",
1417
716
  },
1418
- method: "POST",
717
+ method: "GET",
1419
718
  });
1420
719
 
1421
720
  if (!response.ok) {
@@ -1427,104 +726,59 @@ export class HttpClient {
1427
726
  );
1428
727
  }
1429
728
 
1430
- if (!response.body) {
1431
- throw new Error("No response body for streaming request");
1432
- }
729
+ const data: GetExposedPortsResponse = await response.json();
730
+ console.log(`[HTTP Client] Got ${data.count} exposed ports`);
1433
731
 
1434
- const reader = response.body.getReader();
1435
- const decoder = new TextDecoder();
1436
-
1437
- try {
1438
- while (true) {
1439
- const { done, value } = await reader.read();
1440
-
1441
- if (done) {
1442
- break;
1443
- }
1444
-
1445
- const chunk = decoder.decode(value, { stream: true });
1446
- const lines = chunk.split("\n");
1447
-
1448
- for (const line of lines) {
1449
- if (line.startsWith("data: ")) {
1450
- try {
1451
- const eventData = line.slice(6); // Remove 'data: ' prefix
1452
- const event: StreamEvent = JSON.parse(eventData);
1453
-
1454
- console.log(
1455
- `[HTTP Client] Rename file stream event: ${event.type}`
1456
- );
1457
- this.options.onStreamEvent?.(event);
1458
-
1459
- switch (event.type) {
1460
- case "command_start":
1461
- console.log(
1462
- `[HTTP Client] Rename file started: ${event.oldPath} -> ${event.newPath}`
1463
- );
1464
- this.options.onCommandStart?.("rename", [oldPath, newPath]);
1465
- break;
1466
-
1467
- case "command_complete":
1468
- console.log(
1469
- `[HTTP Client] Rename file completed: ${event.oldPath} -> ${event.newPath}, Success: ${event.success}`
1470
- );
1471
- this.options.onCommandComplete?.(
1472
- event.success!,
1473
- 0,
1474
- "",
1475
- "",
1476
- "rename",
1477
- [oldPath, newPath]
1478
- );
1479
- break;
1480
-
1481
- case "error":
1482
- console.error(
1483
- `[HTTP Client] Rename file error: ${event.error}`
1484
- );
1485
- this.options.onError?.(event.error!, "rename", [
1486
- oldPath,
1487
- newPath,
1488
- ]);
1489
- break;
1490
- }
1491
- } catch (parseError) {
1492
- console.warn(
1493
- "[HTTP Client] Failed to parse rename file stream event:",
1494
- parseError
1495
- );
1496
- }
1497
- }
1498
- }
1499
- }
1500
- } finally {
1501
- reader.releaseLock();
1502
- }
732
+ return data;
1503
733
  } catch (error) {
1504
- console.error("[HTTP Client] Error in streaming rename file:", error);
1505
- this.options.onError?.(
1506
- error instanceof Error ? error.message : "Unknown error",
1507
- "rename",
1508
- [oldPath, newPath]
1509
- );
734
+ console.error("[HTTP Client] Error getting exposed ports:", error);
1510
735
  throw error;
1511
736
  }
1512
737
  }
1513
738
 
1514
- async moveFile(
1515
- sourcePath: string,
1516
- destinationPath: string,
1517
- sessionId?: string
1518
- ): Promise<MoveFileResponse> {
739
+ async ping(): Promise<string> {
1519
740
  try {
1520
- const targetSessionId = sessionId || this.sessionId;
741
+ const response = await this.doFetch(`/api/ping`, {
742
+ headers: {
743
+ "Content-Type": "application/json",
744
+ },
745
+ method: "GET",
746
+ });
1521
747
 
1522
- const response = await this.doFetch(`/api/move`, {
748
+ if (!response.ok) {
749
+ throw new Error(`HTTP error! status: ${response.status}`);
750
+ }
751
+
752
+ const data: PingResponse = await response.json();
753
+ console.log(`[HTTP Client] Ping response: ${data.message}`);
754
+ return data.timestamp;
755
+ } catch (error) {
756
+ console.error("[HTTP Client] Error pinging server:", error);
757
+ throw error;
758
+ }
759
+ }
760
+
761
+
762
+ // Process management methods
763
+ async startProcess(
764
+ command: string,
765
+ sessionId: string,
766
+ options?: {
767
+ processId?: string;
768
+ timeout?: number;
769
+ env?: Record<string, string>;
770
+ cwd?: string;
771
+ encoding?: string;
772
+ autoCleanup?: boolean;
773
+ }
774
+ ): Promise<StartProcessResponse> {
775
+ try {
776
+ const response = await this.doFetch("/api/process/start", {
1523
777
  body: JSON.stringify({
1524
- destinationPath,
1525
- sessionId: targetSessionId,
1526
- sourcePath,
1527
- } as MoveFileRequest),
778
+ command,
779
+ sessionId,
780
+ options,
781
+ } as StartProcessRequest),
1528
782
  headers: {
1529
783
  "Content-Type": "application/json",
1530
784
  },
@@ -1540,36 +794,28 @@ export class HttpClient {
1540
794
  );
1541
795
  }
1542
796
 
1543
- const data: MoveFileResponse = await response.json();
797
+ const data: StartProcessResponse = await response.json();
1544
798
  console.log(
1545
- `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}`
799
+ `[HTTP Client] Process started: ${command}, ID: ${data.process.id}`
1546
800
  );
1547
801
 
1548
802
  return data;
1549
803
  } catch (error) {
1550
- console.error("[HTTP Client] Error moving file:", error);
804
+ console.error("[HTTP Client] Error starting process:", error);
1551
805
  throw error;
1552
806
  }
1553
807
  }
1554
808
 
1555
- async moveFileStream(
1556
- sourcePath: string,
1557
- destinationPath: string,
1558
- sessionId?: string
1559
- ): Promise<void> {
809
+ async listProcesses(sessionId?: string): Promise<ListProcessesResponse> {
1560
810
  try {
1561
- const targetSessionId = sessionId || this.sessionId;
1562
-
1563
- const response = await this.doFetch(`/api/move/stream`, {
1564
- body: JSON.stringify({
1565
- destinationPath,
1566
- sessionId: targetSessionId,
1567
- sourcePath,
1568
- } as MoveFileRequest),
811
+ const url = sessionId
812
+ ? `/api/process/list?session=${encodeURIComponent(sessionId)}`
813
+ : "/api/process/list";
814
+ const response = await this.doFetch(url, {
1569
815
  headers: {
1570
816
  "Content-Type": "application/json",
1571
817
  },
1572
- method: "POST",
818
+ method: "GET",
1573
819
  });
1574
820
 
1575
821
  if (!response.ok) {
@@ -1581,134 +827,53 @@ export class HttpClient {
1581
827
  );
1582
828
  }
1583
829
 
1584
- if (!response.body) {
1585
- throw new Error("No response body for streaming request");
1586
- }
830
+ const data: ListProcessesResponse = await response.json();
831
+ console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
1587
832
 
1588
- const reader = response.body.getReader();
1589
- const decoder = new TextDecoder();
1590
-
1591
- try {
1592
- while (true) {
1593
- const { done, value } = await reader.read();
1594
-
1595
- if (done) {
1596
- break;
1597
- }
1598
-
1599
- const chunk = decoder.decode(value, { stream: true });
1600
- const lines = chunk.split("\n");
1601
-
1602
- for (const line of lines) {
1603
- if (line.startsWith("data: ")) {
1604
- try {
1605
- const eventData = line.slice(6); // Remove 'data: ' prefix
1606
- const event: StreamEvent = JSON.parse(eventData);
1607
-
1608
- console.log(
1609
- `[HTTP Client] Move file stream event: ${event.type}`
1610
- );
1611
- this.options.onStreamEvent?.(event);
1612
-
1613
- switch (event.type) {
1614
- case "command_start":
1615
- console.log(
1616
- `[HTTP Client] Move file started: ${event.sourcePath} -> ${event.destinationPath}`
1617
- );
1618
- this.options.onCommandStart?.("move", [
1619
- sourcePath,
1620
- destinationPath,
1621
- ]);
1622
- break;
1623
-
1624
- case "command_complete":
1625
- console.log(
1626
- `[HTTP Client] Move file completed: ${event.sourcePath} -> ${event.destinationPath}, Success: ${event.success}`
1627
- );
1628
- this.options.onCommandComplete?.(
1629
- event.success!,
1630
- 0,
1631
- "",
1632
- "",
1633
- "move",
1634
- [sourcePath, destinationPath]
1635
- );
1636
- break;
1637
-
1638
- case "error":
1639
- console.error(
1640
- `[HTTP Client] Move file error: ${event.error}`
1641
- );
1642
- this.options.onError?.(event.error!, "move", [
1643
- sourcePath,
1644
- destinationPath,
1645
- ]);
1646
- break;
1647
- }
1648
- } catch (parseError) {
1649
- console.warn(
1650
- "[HTTP Client] Failed to parse move file stream event:",
1651
- parseError
1652
- );
1653
- }
1654
- }
1655
- }
1656
- }
1657
- } finally {
1658
- reader.releaseLock();
1659
- }
833
+ return data;
1660
834
  } catch (error) {
1661
- console.error("[HTTP Client] Error in streaming move file:", error);
1662
- this.options.onError?.(
1663
- error instanceof Error ? error.message : "Unknown error",
1664
- "move",
1665
- [sourcePath, destinationPath]
1666
- );
835
+ console.error("[HTTP Client] Error listing processes:", error);
1667
836
  throw error;
1668
837
  }
1669
838
  }
1670
839
 
1671
- async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
840
+ async getProcess(processId: string): Promise<GetProcessResponse> {
1672
841
  try {
1673
- const response = await this.doFetch(`/api/expose-port`, {
1674
- body: JSON.stringify({
1675
- port,
1676
- name,
1677
- }),
842
+ const response = await this.doFetch(`/api/process/${processId}`, {
1678
843
  headers: {
1679
844
  "Content-Type": "application/json",
1680
845
  },
1681
- method: "POST",
846
+ method: "GET",
1682
847
  });
1683
848
 
1684
849
  if (!response.ok) {
1685
850
  const errorData = (await response.json().catch(() => ({}))) as {
1686
851
  error?: string;
1687
852
  };
1688
- console.log(errorData);
1689
853
  throw new Error(
1690
854
  errorData.error || `HTTP error! status: ${response.status}`
1691
855
  );
1692
856
  }
1693
857
 
1694
- const data: ExposePortResponse = await response.json();
858
+ const data: GetProcessResponse = await response.json();
1695
859
  console.log(
1696
- `[HTTP Client] Port exposed: ${port}${name ? ` (${name})` : ""}, Success: ${data.success}`
860
+ `[HTTP Client] Got process ${processId}: ${
861
+ data.process?.status || "not found"
862
+ }`
1697
863
  );
1698
864
 
1699
865
  return data;
1700
866
  } catch (error) {
1701
- console.error("[HTTP Client] Error exposing port:", error);
867
+ console.error("[HTTP Client] Error getting process:", error);
1702
868
  throw error;
1703
869
  }
1704
870
  }
1705
871
 
1706
- async unexposePort(port: number): Promise<UnexposePortResponse> {
872
+ async killProcess(
873
+ processId: string
874
+ ): Promise<{ success: boolean; message: string }> {
1707
875
  try {
1708
- const response = await this.doFetch(`/api/unexpose-port`, {
1709
- body: JSON.stringify({
1710
- port,
1711
- }),
876
+ const response = await this.doFetch(`/api/process/${processId}`, {
1712
877
  headers: {
1713
878
  "Content-Type": "application/json",
1714
879
  },
@@ -1724,25 +889,33 @@ export class HttpClient {
1724
889
  );
1725
890
  }
1726
891
 
1727
- const data: UnexposePortResponse = await response.json();
1728
- console.log(
1729
- `[HTTP Client] Port unexposed: ${port}, Success: ${data.success}`
1730
- );
892
+ const data = (await response.json()) as {
893
+ success: boolean;
894
+ message: string;
895
+ };
896
+ console.log(`[HTTP Client] Killed process ${processId}`);
1731
897
 
1732
898
  return data;
1733
899
  } catch (error) {
1734
- console.error("[HTTP Client] Error unexposing port:", error);
900
+ console.error("[HTTP Client] Error killing process:", error);
1735
901
  throw error;
1736
902
  }
1737
903
  }
1738
904
 
1739
- async getExposedPorts(): Promise<GetExposedPortsResponse> {
905
+ async killAllProcesses(sessionId?: string): Promise<{
906
+ success: boolean;
907
+ killedCount: number;
908
+ message: string;
909
+ }> {
1740
910
  try {
1741
- const response = await this.doFetch(`/api/exposed-ports`, {
911
+ const url = sessionId
912
+ ? `/api/process/kill-all?session=${encodeURIComponent(sessionId)}`
913
+ : "/api/process/kill-all";
914
+ const response = await this.doFetch(url, {
1742
915
  headers: {
1743
916
  "Content-Type": "application/json",
1744
917
  },
1745
- method: "GET",
918
+ method: "DELETE",
1746
919
  });
1747
920
 
1748
921
  if (!response.ok) {
@@ -1754,21 +927,23 @@ export class HttpClient {
1754
927
  );
1755
928
  }
1756
929
 
1757
- const data: GetExposedPortsResponse = await response.json();
1758
- console.log(
1759
- `[HTTP Client] Got ${data.count} exposed ports`
1760
- );
930
+ const data = (await response.json()) as {
931
+ success: boolean;
932
+ killedCount: number;
933
+ message: string;
934
+ };
935
+ console.log(`[HTTP Client] Killed ${data.killedCount} processes`);
1761
936
 
1762
937
  return data;
1763
938
  } catch (error) {
1764
- console.error("[HTTP Client] Error getting exposed ports:", error);
939
+ console.error("[HTTP Client] Error killing all processes:", error);
1765
940
  throw error;
1766
941
  }
1767
942
  }
1768
943
 
1769
- async ping(): Promise<string> {
944
+ async getProcessLogs(processId: string): Promise<GetProcessLogsResponse> {
1770
945
  try {
1771
- const response = await this.doFetch(`/api/ping`, {
946
+ const response = await this.doFetch(`/api/process/${processId}/logs`, {
1772
947
  headers: {
1773
948
  "Content-Type": "application/json",
1774
949
  },
@@ -1776,314 +951,59 @@ export class HttpClient {
1776
951
  });
1777
952
 
1778
953
  if (!response.ok) {
1779
- throw new Error(`HTTP error! status: ${response.status}`);
954
+ const errorData = (await response.json().catch(() => ({}))) as {
955
+ error?: string;
956
+ };
957
+ throw new Error(
958
+ errorData.error || `HTTP error! status: ${response.status}`
959
+ );
1780
960
  }
1781
961
 
1782
- const data: PingResponse = await response.json();
1783
- console.log(`[HTTP Client] Ping response: ${data.message}`);
1784
- return data.timestamp;
962
+ const data: GetProcessLogsResponse = await response.json();
963
+ console.log(`[HTTP Client] Got logs for process ${processId}`);
964
+
965
+ return data;
1785
966
  } catch (error) {
1786
- console.error("[HTTP Client] Error pinging server:", error);
967
+ console.error("[HTTP Client] Error getting process logs:", error);
1787
968
  throw error;
1788
969
  }
1789
970
  }
1790
971
 
1791
- async getCommands(): Promise<string[]> {
972
+ async streamProcessLogs(
973
+ processId: string,
974
+ options?: { signal?: AbortSignal }
975
+ ): Promise<ReadableStream<Uint8Array>> {
1792
976
  try {
1793
- const response = await fetch(`${this.baseUrl}/api/commands`, {
977
+ const response = await this.doFetch(`/api/process/${processId}/stream`, {
1794
978
  headers: {
1795
- "Content-Type": "application/json",
979
+ Accept: "text/event-stream",
980
+ "Cache-Control": "no-cache",
1796
981
  },
1797
982
  method: "GET",
983
+ signal: options?.signal,
1798
984
  });
1799
985
 
1800
986
  if (!response.ok) {
1801
- throw new Error(`HTTP error! status: ${response.status}`);
987
+ const errorData = (await response.json().catch(() => ({}))) as {
988
+ error?: string;
989
+ };
990
+ throw new Error(
991
+ errorData.error || `HTTP error! status: ${response.status}`
992
+ );
993
+ }
994
+
995
+ if (!response.body) {
996
+ throw new Error("No response body for streaming request");
1802
997
  }
1803
998
 
1804
- const data: CommandsResponse = await response.json();
1805
999
  console.log(
1806
- `[HTTP Client] Available commands: ${data.availableCommands.length}`
1000
+ `[HTTP Client] Started streaming logs for process ${processId}`
1807
1001
  );
1808
- return data.availableCommands;
1002
+
1003
+ return response.body;
1809
1004
  } catch (error) {
1810
- console.error("[HTTP Client] Error getting commands:", error);
1005
+ console.error("[HTTP Client] Error streaming process logs:", error);
1811
1006
  throw error;
1812
1007
  }
1813
1008
  }
1814
-
1815
- getSessionId(): string | null {
1816
- return this.sessionId;
1817
- }
1818
-
1819
- setSessionId(sessionId: string): void {
1820
- this.sessionId = sessionId;
1821
- }
1822
-
1823
- clearSession(): void {
1824
- this.sessionId = null;
1825
- }
1826
- }
1827
-
1828
- // Example usage and utility functions
1829
- export function createClient(options?: HttpClientOptions): HttpClient {
1830
- return new HttpClient(options);
1831
- }
1832
-
1833
- // Convenience function for quick command execution
1834
- export async function quickExecute(
1835
- command: string,
1836
- args: string[] = [],
1837
- options?: HttpClientOptions
1838
- ): Promise<ExecuteResponse> {
1839
- const client = createClient(options);
1840
- await client.createSession();
1841
-
1842
- try {
1843
- return await client.execute(command, args);
1844
- } finally {
1845
- client.clearSession();
1846
- }
1847
- }
1848
-
1849
- // Convenience function for quick streaming command execution
1850
- export async function quickExecuteStream(
1851
- command: string,
1852
- args: string[] = [],
1853
- options?: HttpClientOptions
1854
- ): Promise<void> {
1855
- const client = createClient(options);
1856
- await client.createSession();
1857
-
1858
- try {
1859
- await client.executeStream(command, args);
1860
- } finally {
1861
- client.clearSession();
1862
- }
1863
- }
1864
-
1865
- // Convenience function for quick git checkout
1866
- export async function quickGitCheckout(
1867
- repoUrl: string,
1868
- branch: string = "main",
1869
- targetDir?: string,
1870
- options?: HttpClientOptions
1871
- ): Promise<GitCheckoutResponse> {
1872
- const client = createClient(options);
1873
- await client.createSession();
1874
-
1875
- try {
1876
- return await client.gitCheckout(repoUrl, branch, targetDir);
1877
- } finally {
1878
- client.clearSession();
1879
- }
1880
- }
1881
-
1882
- // Convenience function for quick directory creation
1883
- export async function quickMkdir(
1884
- path: string,
1885
- recursive: boolean = false,
1886
- options?: HttpClientOptions
1887
- ): Promise<MkdirResponse> {
1888
- const client = createClient(options);
1889
- await client.createSession();
1890
-
1891
- try {
1892
- return await client.mkdir(path, recursive);
1893
- } finally {
1894
- client.clearSession();
1895
- }
1896
- }
1897
-
1898
- // Convenience function for quick streaming git checkout
1899
- export async function quickGitCheckoutStream(
1900
- repoUrl: string,
1901
- branch: string = "main",
1902
- targetDir?: string,
1903
- options?: HttpClientOptions
1904
- ): Promise<void> {
1905
- const client = createClient(options);
1906
- await client.createSession();
1907
-
1908
- try {
1909
- await client.gitCheckoutStream(repoUrl, branch, targetDir);
1910
- } finally {
1911
- client.clearSession();
1912
- }
1913
- }
1914
-
1915
- // Convenience function for quick streaming directory creation
1916
- export async function quickMkdirStream(
1917
- path: string,
1918
- recursive: boolean = false,
1919
- options?: HttpClientOptions
1920
- ): Promise<void> {
1921
- const client = createClient(options);
1922
- await client.createSession();
1923
-
1924
- try {
1925
- await client.mkdirStream(path, recursive);
1926
- } finally {
1927
- client.clearSession();
1928
- }
1929
- }
1930
-
1931
- // Convenience function for quick file writing
1932
- export async function quickWriteFile(
1933
- path: string,
1934
- content: string,
1935
- encoding: string = "utf-8",
1936
- options?: HttpClientOptions
1937
- ): Promise<WriteFileResponse> {
1938
- const client = createClient(options);
1939
- await client.createSession();
1940
-
1941
- try {
1942
- return await client.writeFile(path, content, encoding);
1943
- } finally {
1944
- client.clearSession();
1945
- }
1946
- }
1947
-
1948
- // Convenience function for quick streaming file writing
1949
- export async function quickWriteFileStream(
1950
- path: string,
1951
- content: string,
1952
- encoding: string = "utf-8",
1953
- options?: HttpClientOptions
1954
- ): Promise<void> {
1955
- const client = createClient(options);
1956
- await client.createSession();
1957
-
1958
- try {
1959
- await client.writeFileStream(path, content, encoding);
1960
- } finally {
1961
- client.clearSession();
1962
- }
1963
- }
1964
-
1965
- // Convenience function for quick file reading
1966
- export async function quickReadFile(
1967
- path: string,
1968
- encoding: string = "utf-8",
1969
- options?: HttpClientOptions
1970
- ): Promise<ReadFileResponse> {
1971
- const client = createClient(options);
1972
- await client.createSession();
1973
-
1974
- try {
1975
- return await client.readFile(path, encoding);
1976
- } finally {
1977
- client.clearSession();
1978
- }
1979
- }
1980
-
1981
- // Convenience function for quick streaming file reading
1982
- export async function quickReadFileStream(
1983
- path: string,
1984
- encoding: string = "utf-8",
1985
- options?: HttpClientOptions
1986
- ): Promise<void> {
1987
- const client = createClient(options);
1988
- await client.createSession();
1989
-
1990
- try {
1991
- await client.readFileStream(path, encoding);
1992
- } finally {
1993
- client.clearSession();
1994
- }
1995
- }
1996
-
1997
- // Convenience function for quick file deletion
1998
- export async function quickDeleteFile(
1999
- path: string,
2000
- options?: HttpClientOptions
2001
- ): Promise<DeleteFileResponse> {
2002
- const client = createClient(options);
2003
- await client.createSession();
2004
-
2005
- try {
2006
- return await client.deleteFile(path);
2007
- } finally {
2008
- client.clearSession();
2009
- }
2010
- }
2011
-
2012
- // Convenience function for quick streaming file deletion
2013
- export async function quickDeleteFileStream(
2014
- path: string,
2015
- options?: HttpClientOptions
2016
- ): Promise<void> {
2017
- const client = createClient(options);
2018
- await client.createSession();
2019
-
2020
- try {
2021
- await client.deleteFileStream(path);
2022
- } finally {
2023
- client.clearSession();
2024
- }
2025
- }
2026
-
2027
- // Convenience function for quick file renaming
2028
- export async function quickRenameFile(
2029
- oldPath: string,
2030
- newPath: string,
2031
- options?: HttpClientOptions
2032
- ): Promise<RenameFileResponse> {
2033
- const client = createClient(options);
2034
- await client.createSession();
2035
-
2036
- try {
2037
- return await client.renameFile(oldPath, newPath);
2038
- } finally {
2039
- client.clearSession();
2040
- }
2041
- }
2042
-
2043
- // Convenience function for quick streaming file renaming
2044
- export async function quickRenameFileStream(
2045
- oldPath: string,
2046
- newPath: string,
2047
- options?: HttpClientOptions
2048
- ): Promise<void> {
2049
- const client = createClient(options);
2050
- await client.createSession();
2051
-
2052
- try {
2053
- await client.renameFileStream(oldPath, newPath);
2054
- } finally {
2055
- client.clearSession();
2056
- }
2057
- }
2058
-
2059
- // Convenience function for quick file moving
2060
- export async function quickMoveFile(
2061
- sourcePath: string,
2062
- destinationPath: string,
2063
- options?: HttpClientOptions
2064
- ): Promise<MoveFileResponse> {
2065
- const client = createClient(options);
2066
- await client.createSession();
2067
-
2068
- try {
2069
- return await client.moveFile(sourcePath, destinationPath);
2070
- } finally {
2071
- client.clearSession();
2072
- }
2073
- }
2074
-
2075
- // Convenience function for quick streaming file moving
2076
- export async function quickMoveFileStream(
2077
- sourcePath: string,
2078
- destinationPath: string,
2079
- options?: HttpClientOptions
2080
- ): Promise<void> {
2081
- const client = createClient(options);
2082
- await client.createSession();
2083
-
2084
- try {
2085
- await client.moveFileStream(sourcePath, destinationPath);
2086
- } finally {
2087
- client.clearSession();
2088
- }
2089
1009
  }