@cloudflare/sandbox 0.0.0-db09b4d → 0.0.0-e1fa354

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 +187 -0
  2. package/Dockerfile +99 -11
  3. package/README.md +806 -22
  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 +448 -2467
  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 +6 -8
  24. package/src/client.ts +477 -1192
  25. package/src/errors.ts +218 -0
  26. package/src/index.ts +63 -78
  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 +144 -0
  31. package/src/sandbox.ts +747 -0
  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 -710
package/src/client.ts CHANGED
@@ -1,36 +1,23 @@
1
- import type { DurableObject } from "cloudflare:workers";
1
+ import type { ExecuteRequest } from "../container_src/types";
2
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";
3
20
 
4
- interface ExecuteRequest {
5
- command: string;
6
- args?: string[];
7
- }
8
-
9
- export interface ExecuteResponse {
10
- success: boolean;
11
- stdout: string;
12
- stderr: string;
13
- exitCode: number;
14
- command: string;
15
- args: string[];
16
- timestamp: string;
17
- }
18
-
19
- interface SessionResponse {
20
- sessionId: string;
21
- message: string;
22
- timestamp: string;
23
- }
24
-
25
- interface SessionListResponse {
26
- sessions: Array<{
27
- sessionId: string;
28
- hasActiveProcess: boolean;
29
- createdAt: string;
30
- }>;
31
- count: number;
32
- timestamp: string;
33
- }
34
21
 
35
22
  interface CommandsResponse {
36
23
  availableCommands: string[];
@@ -41,120 +28,103 @@ interface GitCheckoutRequest {
41
28
  repoUrl: string;
42
29
  branch?: string;
43
30
  targetDir?: string;
44
- sessionId?: string;
31
+ sessionId: string;
45
32
  }
46
33
 
47
- export interface GitCheckoutResponse {
48
- success: boolean;
49
- stdout: string;
50
- stderr: string;
51
- exitCode: number;
52
- repoUrl: string;
53
- branch: string;
54
- targetDir: string;
55
- timestamp: string;
56
- }
57
34
 
58
35
  interface MkdirRequest {
59
36
  path: string;
60
37
  recursive?: boolean;
61
- sessionId?: string;
38
+ sessionId: string;
62
39
  }
63
40
 
64
- export interface MkdirResponse {
65
- success: boolean;
66
- stdout: string;
67
- stderr: string;
68
- exitCode: number;
69
- path: string;
70
- recursive: boolean;
71
- timestamp: string;
72
- }
73
41
 
74
42
  interface WriteFileRequest {
75
43
  path: string;
76
44
  content: string;
77
45
  encoding?: string;
78
- sessionId?: string;
46
+ sessionId: string;
79
47
  }
80
48
 
81
- export interface WriteFileResponse {
82
- success: boolean;
83
- exitCode: number;
49
+
50
+ interface ReadFileRequest {
84
51
  path: string;
85
- timestamp: string;
52
+ encoding?: string;
53
+ sessionId: string;
86
54
  }
87
55
 
56
+
88
57
  interface DeleteFileRequest {
89
58
  path: string;
90
- sessionId?: string;
59
+ sessionId: string;
91
60
  }
92
61
 
93
- export interface DeleteFileResponse {
94
- success: boolean;
95
- exitCode: number;
96
- path: string;
97
- timestamp: string;
98
- }
99
62
 
100
63
  interface RenameFileRequest {
101
64
  oldPath: string;
102
65
  newPath: string;
103
- sessionId?: string;
66
+ sessionId: string;
104
67
  }
105
68
 
106
- export interface RenameFileResponse {
107
- success: boolean;
108
- exitCode: number;
109
- oldPath: string;
110
- newPath: string;
111
- timestamp: string;
112
- }
113
69
 
114
70
  interface MoveFileRequest {
115
71
  sourcePath: string;
116
72
  destinationPath: string;
117
- sessionId?: string;
73
+ sessionId: string;
74
+ }
75
+
76
+
77
+ interface ListFilesRequest {
78
+ path: string;
79
+ options?: {
80
+ recursive?: boolean;
81
+ includeHidden?: boolean;
82
+ };
83
+ sessionId: string;
84
+ }
85
+
86
+
87
+ interface PreviewInfo {
88
+ url: string;
89
+ port: number;
90
+ name?: string;
118
91
  }
119
92
 
120
- export interface MoveFileResponse {
93
+ interface ExposedPort extends PreviewInfo {
94
+ exposedAt: string;
95
+ timestamp: string;
96
+ }
97
+
98
+ interface ExposePortResponse {
121
99
  success: boolean;
122
- exitCode: number;
123
- sourcePath: string;
124
- destinationPath: string;
100
+ port: number;
101
+ name?: string;
102
+ exposedAt: string;
125
103
  timestamp: string;
126
104
  }
127
105
 
128
- interface PingResponse {
129
- message: string;
106
+ interface UnexposePortResponse {
107
+ success: boolean;
108
+ port: number;
130
109
  timestamp: string;
131
110
  }
132
111
 
133
- interface StreamEvent {
134
- type: "command_start" | "output" | "command_complete" | "error";
135
- command?: string;
136
- args?: string[];
137
- stream?: "stdout" | "stderr";
138
- data?: string;
139
- message?: string;
140
- path?: string;
141
- oldPath?: string;
142
- newPath?: string;
143
- sourcePath?: string;
144
- destinationPath?: string;
145
- success?: boolean;
146
- exitCode?: number;
147
- stdout?: string;
148
- stderr?: string;
149
- error?: string;
150
- timestamp?: string;
112
+ interface GetExposedPortsResponse {
113
+ ports: ExposedPort[];
114
+ count: number;
115
+ timestamp: string;
116
+ }
117
+
118
+ interface PingResponse {
119
+ message: string;
120
+ timestamp: string;
151
121
  }
152
122
 
153
123
  interface HttpClientOptions {
154
124
  stub?: Sandbox;
155
125
  baseUrl?: string;
156
126
  port?: number;
157
- onCommandStart?: (command: string, args: string[]) => void;
127
+ onCommandStart?: (command: string) => void;
158
128
  onOutput?: (
159
129
  stream: "stdout" | "stderr",
160
130
  data: string,
@@ -165,17 +135,14 @@ interface HttpClientOptions {
165
135
  exitCode: number,
166
136
  stdout: string,
167
137
  stderr: string,
168
- command: string,
169
- args: string[]
138
+ command: string
170
139
  ) => void;
171
- onError?: (error: string, command?: string, args?: string[]) => void;
172
- onStreamEvent?: (event: StreamEvent) => void;
140
+ onError?: (error: string, command?: string) => void;
173
141
  }
174
142
 
175
143
  export class HttpClient {
176
144
  private baseUrl: string;
177
145
  private options: HttpClientOptions;
178
- private sessionId: string | null = null;
179
146
 
180
147
  constructor(options: HttpClientOptions = {}) {
181
148
  this.options = {
@@ -184,130 +151,93 @@ export class HttpClient {
184
151
  this.baseUrl = this.options.baseUrl!;
185
152
  }
186
153
 
187
- private async doFetch(
154
+ protected async doFetch(
188
155
  path: string,
189
156
  options?: RequestInit
190
157
  ): Promise<Response> {
191
- if (this.options.stub) {
192
- return this.options.stub.containerFetch(path, options, this.options.port);
193
- }
194
- return fetch(this.baseUrl + path, options);
195
- }
196
- // Public methods to set event handlers
197
- setOnOutput(
198
- handler: (
199
- stream: "stdout" | "stderr",
200
- data: string,
201
- command: string
202
- ) => void
203
- ): void {
204
- this.options.onOutput = handler;
205
- }
206
-
207
- setOnCommandComplete(
208
- handler: (
209
- success: boolean,
210
- exitCode: number,
211
- stdout: string,
212
- stderr: string,
213
- command: string,
214
- args: string[]
215
- ) => void
216
- ): void {
217
- this.options.onCommandComplete = handler;
218
- }
219
-
220
- setOnStreamEvent(handler: (event: StreamEvent) => void): void {
221
- this.options.onStreamEvent = handler;
222
- }
158
+ const url = this.options.stub
159
+ ? `http://localhost:${this.options.port}${path}`
160
+ : `${this.baseUrl}${path}`;
161
+ const method = options?.method || "GET";
223
162
 
224
- // Public getter methods
225
- getOnOutput():
226
- | ((stream: "stdout" | "stderr", data: string, command: string) => void)
227
- | undefined {
228
- return this.options.onOutput;
229
- }
163
+ console.log(`[HTTP Client] Making ${method} request to ${url}`);
230
164
 
231
- getOnCommandComplete():
232
- | ((
233
- success: boolean,
234
- exitCode: number,
235
- stdout: string,
236
- stderr: string,
237
- command: string,
238
- args: string[]
239
- ) => void)
240
- | undefined {
241
- return this.options.onCommandComplete;
242
- }
165
+ try {
166
+ let response: Response;
243
167
 
244
- getOnStreamEvent(): ((event: StreamEvent) => void) | undefined {
245
- return this.options.onStreamEvent;
246
- }
168
+ if (this.options.stub) {
169
+ response = await this.options.stub.containerFetch(
170
+ url,
171
+ options,
172
+ this.options.port
173
+ );
174
+ } else {
175
+ response = await fetch(url, options);
176
+ }
247
177
 
248
- async createSession(): Promise<string> {
249
- try {
250
- const response = await this.doFetch(`/api/session/create`, {
251
- headers: {
252
- "Content-Type": "application/json",
253
- },
254
- method: "POST",
255
- });
178
+ console.log(
179
+ `[HTTP Client] Response: ${response.status} ${response.statusText}`
180
+ );
256
181
 
257
182
  if (!response.ok) {
258
- throw new Error(`HTTP error! status: ${response.status}`);
183
+ console.error(
184
+ `[HTTP Client] Request failed: ${method} ${url} - ${response.status} ${response.statusText}`
185
+ );
259
186
  }
260
187
 
261
- const data: SessionResponse = await response.json();
262
- this.sessionId = data.sessionId;
263
- console.log(`[HTTP Client] Created session: ${this.sessionId}`);
264
- return this.sessionId;
188
+ return response;
265
189
  } catch (error) {
266
- console.error("[HTTP Client] Error creating session:", error);
190
+ console.error(`[HTTP Client] Request error: ${method} ${url}`, error);
267
191
  throw error;
268
192
  }
269
193
  }
270
194
 
271
- async listSessions(): Promise<SessionListResponse> {
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 }> {
272
201
  try {
273
- const response = await this.doFetch(`/api/session/list`, {
202
+ const response = await this.doFetch(`/api/session/create`, {
203
+ method: "POST",
274
204
  headers: {
275
205
  "Content-Type": "application/json",
276
206
  },
277
- method: "GET",
207
+ body: JSON.stringify(options),
278
208
  });
279
209
 
280
210
  if (!response.ok) {
281
- 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
+ );
282
217
  }
283
218
 
284
- const data: SessionListResponse = await response.json();
285
- 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}`);
286
221
  return data;
287
222
  } catch (error) {
288
- console.error("[HTTP Client] Error listing sessions:", error);
223
+ console.error("[HTTP Client] Error creating session:", error);
289
224
  throw error;
290
225
  }
291
226
  }
292
227
 
293
- async execute(
228
+ async exec(
229
+ sessionId: string,
294
230
  command: string,
295
- args: string[] = [],
296
- sessionId?: string
231
+ options?: Pick<BaseExecOptions, "cwd" | "env">
297
232
  ): Promise<ExecuteResponse> {
298
233
  try {
299
- const targetSessionId = sessionId || this.sessionId;
300
-
234
+ // Always use session-specific endpoint
301
235
  const response = await this.doFetch(`/api/execute`, {
302
- body: JSON.stringify({
303
- args,
304
- command,
305
- sessionId: targetSessionId,
306
- }),
236
+ method: "POST",
307
237
  headers: {
308
238
  "Content-Type": "application/json",
309
239
  },
310
- method: "POST",
240
+ body: JSON.stringify({ id: sessionId, command }),
311
241
  });
312
242
 
313
243
  if (!response.ok) {
@@ -315,55 +245,57 @@ export class HttpClient {
315
245
  error?: string;
316
246
  };
317
247
  throw new Error(
318
- errorData.error || `HTTP error! status: ${response.status}`
248
+ errorData.error || `Failed to execute in session: ${response.status}`
319
249
  );
320
250
  }
321
251
 
322
- const data: ExecuteResponse = await response.json();
252
+ const data = await response.json() as { stdout: string; stderr: string; exitCode: number; success: boolean };
323
253
  console.log(
324
- `[HTTP Client] Command executed: ${command}, Success: ${data.success}`
254
+ `[HTTP Client] Command executed in session ${sessionId}: ${command}`
325
255
  );
256
+
257
+ // Convert to ExecuteResponse format for consistency
258
+ const executeResponse: ExecuteResponse = {
259
+ ...data,
260
+ command,
261
+ timestamp: new Date().toISOString()
262
+ };
326
263
 
327
264
  // Call the callback if provided
328
265
  this.options.onCommandComplete?.(
329
- data.success,
330
- data.exitCode,
331
- data.stdout,
332
- data.stderr,
333
- data.command,
334
- data.args
266
+ executeResponse.success,
267
+ executeResponse.exitCode,
268
+ executeResponse.stdout,
269
+ executeResponse.stderr,
270
+ executeResponse.command
335
271
  );
336
272
 
337
- return data;
273
+ return executeResponse;
338
274
  } catch (error) {
339
- console.error("[HTTP Client] Error executing command:", error);
275
+ console.error("[HTTP Client] Error executing in session:", error);
340
276
  this.options.onError?.(
341
277
  error instanceof Error ? error.message : "Unknown error",
342
- command,
343
- args
278
+ command
344
279
  );
345
280
  throw error;
346
281
  }
347
282
  }
348
283
 
349
- async executeStream(
350
- command: string,
351
- args: string[] = [],
352
- sessionId?: string
353
- ): Promise<void> {
284
+ async execStream(
285
+ sessionId: string,
286
+ command: string
287
+ ): Promise<ReadableStream<Uint8Array>> {
354
288
  try {
355
- const targetSessionId = sessionId || this.sessionId;
356
-
289
+ // Always use session-specific streaming endpoint
357
290
  const response = await this.doFetch(`/api/execute/stream`, {
358
- body: JSON.stringify({
359
- args,
360
- command,
361
- sessionId: targetSessionId,
362
- }),
291
+ method: "POST",
363
292
  headers: {
364
293
  "Content-Type": "application/json",
365
294
  },
366
- method: "POST",
295
+ body: JSON.stringify({
296
+ id: sessionId,
297
+ command
298
+ }),
367
299
  });
368
300
 
369
301
  if (!response.ok) {
@@ -371,123 +303,38 @@ export class HttpClient {
371
303
  error?: string;
372
304
  };
373
305
  throw new Error(
374
- errorData.error || `HTTP error! status: ${response.status}`
306
+ errorData.error || `Failed to stream execute in session: ${response.status}`
375
307
  );
376
308
  }
377
309
 
378
310
  if (!response.body) {
379
- throw new Error("No response body for streaming request");
311
+ throw new Error("No response body for streaming execution");
380
312
  }
381
313
 
382
- const reader = response.body.getReader();
383
- const decoder = new TextDecoder();
384
-
385
- try {
386
- while (true) {
387
- const { done, value } = await reader.read();
388
-
389
- if (done) {
390
- break;
391
- }
392
-
393
- const chunk = decoder.decode(value, { stream: true });
394
- const lines = chunk.split("\n");
395
-
396
- for (const line of lines) {
397
- if (line.startsWith("data: ")) {
398
- try {
399
- const eventData = line.slice(6); // Remove 'data: ' prefix
400
- const event: StreamEvent = JSON.parse(eventData);
401
-
402
- console.log(`[HTTP Client] Stream event: ${event.type}`);
403
- this.options.onStreamEvent?.(event);
404
-
405
- switch (event.type) {
406
- case "command_start":
407
- console.log(
408
- `[HTTP Client] Command started: ${
409
- event.command
410
- } ${event.args?.join(" ")}`
411
- );
412
- this.options.onCommandStart?.(
413
- event.command!,
414
- event.args || []
415
- );
416
- break;
417
-
418
- case "output":
419
- console.log(`[${event.stream}] ${event.data}`);
420
- this.options.onOutput?.(
421
- event.stream!,
422
- event.data!,
423
- event.command!
424
- );
425
- break;
426
-
427
- case "command_complete":
428
- console.log(
429
- `[HTTP Client] Command completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
430
- );
431
- this.options.onCommandComplete?.(
432
- event.success!,
433
- event.exitCode!,
434
- event.stdout!,
435
- event.stderr!,
436
- event.command!,
437
- event.args || []
438
- );
439
- break;
440
-
441
- case "error":
442
- console.error(
443
- `[HTTP Client] Command error: ${event.error}`
444
- );
445
- this.options.onError?.(
446
- event.error!,
447
- event.command,
448
- event.args
449
- );
450
- break;
451
- }
452
- } catch (parseError) {
453
- console.warn(
454
- "[HTTP Client] Failed to parse stream event:",
455
- parseError
456
- );
457
- }
458
- }
459
- }
460
- }
461
- } finally {
462
- reader.releaseLock();
463
- }
464
- } catch (error) {
465
- console.error("[HTTP Client] Error in streaming execution:", error);
466
- this.options.onError?.(
467
- error instanceof Error ? error.message : "Unknown error",
468
- command,
469
- args
314
+ console.log(
315
+ `[HTTP Client] Started streaming command in session ${sessionId}: ${command}`
470
316
  );
317
+ return response.body;
318
+ } catch (error) {
319
+ console.error("[HTTP Client] Error streaming execute in session:", error);
471
320
  throw error;
472
321
  }
473
322
  }
474
323
 
475
324
  async gitCheckout(
476
325
  repoUrl: string,
326
+ sessionId: string,
477
327
  branch: string = "main",
478
- targetDir?: string,
479
- sessionId?: string
328
+ targetDir?: string
480
329
  ): Promise<GitCheckoutResponse> {
481
330
  try {
482
- const targetSessionId = sessionId || this.sessionId;
483
-
484
331
  const response = await this.doFetch(`/api/git/checkout`, {
485
332
  body: JSON.stringify({
486
333
  branch,
487
334
  repoUrl,
488
- sessionId: targetSessionId,
489
335
  targetDir,
490
- }),
336
+ sessionId,
337
+ } as GitCheckoutRequest),
491
338
  headers: {
492
339
  "Content-Type": "application/json",
493
340
  },
@@ -515,22 +362,18 @@ export class HttpClient {
515
362
  }
516
363
  }
517
364
 
518
- async gitCheckoutStream(
519
- repoUrl: string,
520
- branch: string = "main",
521
- targetDir?: string,
522
- sessionId?: string
523
- ): Promise<void> {
365
+ async mkdir(
366
+ path: string,
367
+ recursive: boolean = false,
368
+ sessionId: string
369
+ ): Promise<MkdirResponse> {
524
370
  try {
525
- const targetSessionId = sessionId || this.sessionId;
526
-
527
- const response = await this.doFetch(`/api/git/checkout/stream`, {
371
+ const response = await this.doFetch(`/api/mkdir`, {
528
372
  body: JSON.stringify({
529
- branch,
530
- repoUrl,
531
- sessionId: targetSessionId,
532
- targetDir,
533
- }),
373
+ path,
374
+ recursive,
375
+ sessionId,
376
+ } as MkdirRequest),
534
377
  headers: {
535
378
  "Content-Type": "application/json",
536
379
  },
@@ -546,119 +389,32 @@ export class HttpClient {
546
389
  );
547
390
  }
548
391
 
549
- if (!response.body) {
550
- throw new Error("No response body for streaming request");
551
- }
392
+ const data: MkdirResponse = await response.json();
393
+ console.log(
394
+ `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}${sessionId ? ` in session: ${sessionId}` : ''}`
395
+ );
552
396
 
553
- const reader = response.body.getReader();
554
- const decoder = new TextDecoder();
555
-
556
- try {
557
- while (true) {
558
- const { done, value } = await reader.read();
559
-
560
- if (done) {
561
- break;
562
- }
563
-
564
- const chunk = decoder.decode(value, { stream: true });
565
- const lines = chunk.split("\n");
566
-
567
- for (const line of lines) {
568
- if (line.startsWith("data: ")) {
569
- try {
570
- const eventData = line.slice(6); // Remove 'data: ' prefix
571
- const event: StreamEvent = JSON.parse(eventData);
572
-
573
- console.log(
574
- `[HTTP Client] Git checkout stream event: ${event.type}`
575
- );
576
- this.options.onStreamEvent?.(event);
577
-
578
- switch (event.type) {
579
- case "command_start":
580
- console.log(
581
- `[HTTP Client] Git checkout started: ${
582
- event.command
583
- } ${event.args?.join(" ")}`
584
- );
585
- this.options.onCommandStart?.(
586
- event.command!,
587
- event.args || []
588
- );
589
- break;
590
-
591
- case "output":
592
- console.log(`[${event.stream}] ${event.data}`);
593
- this.options.onOutput?.(
594
- event.stream!,
595
- event.data!,
596
- event.command!
597
- );
598
- break;
599
-
600
- case "command_complete":
601
- console.log(
602
- `[HTTP Client] Git checkout completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
603
- );
604
- this.options.onCommandComplete?.(
605
- event.success!,
606
- event.exitCode!,
607
- event.stdout!,
608
- event.stderr!,
609
- event.command!,
610
- event.args || []
611
- );
612
- break;
613
-
614
- case "error":
615
- console.error(
616
- `[HTTP Client] Git checkout error: ${event.error}`
617
- );
618
- this.options.onError?.(
619
- event.error!,
620
- event.command,
621
- event.args
622
- );
623
- break;
624
- }
625
- } catch (parseError) {
626
- console.warn(
627
- "[HTTP Client] Failed to parse git checkout stream event:",
628
- parseError
629
- );
630
- }
631
- }
632
- }
633
- }
634
- } finally {
635
- reader.releaseLock();
636
- }
397
+ return data;
637
398
  } catch (error) {
638
- console.error("[HTTP Client] Error in streaming git checkout:", error);
639
- this.options.onError?.(
640
- error instanceof Error ? error.message : "Unknown error",
641
- "git clone",
642
- [branch, repoUrl, targetDir || ""]
643
- );
399
+ console.error("[HTTP Client] Error creating directory:", error);
644
400
  throw error;
645
401
  }
646
402
  }
647
403
 
648
- async mkdir(
404
+ async writeFile(
649
405
  path: string,
650
- recursive: boolean = false,
651
- sessionId?: string
652
- ): Promise<MkdirResponse> {
406
+ content: string,
407
+ encoding: string = "utf-8",
408
+ sessionId: string
409
+ ): Promise<WriteFileResponse> {
653
410
  try {
654
- const targetSessionId = sessionId || this.sessionId;
655
-
656
- const response = await this.doFetch(`/api/mkdir`, {
411
+ const response = await this.doFetch(`/api/write`, {
657
412
  body: JSON.stringify({
413
+ content,
414
+ encoding,
658
415
  path,
659
- recursive,
660
- sessionId: targetSessionId,
661
- }),
416
+ sessionId,
417
+ } as WriteFileRequest),
662
418
  headers: {
663
419
  "Content-Type": "application/json",
664
420
  },
@@ -674,32 +430,30 @@ export class HttpClient {
674
430
  );
675
431
  }
676
432
 
677
- const data: MkdirResponse = await response.json();
433
+ const data: WriteFileResponse = await response.json();
678
434
  console.log(
679
- `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}`
435
+ `[HTTP Client] File written: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
680
436
  );
681
437
 
682
438
  return data;
683
439
  } catch (error) {
684
- console.error("[HTTP Client] Error creating directory:", error);
440
+ console.error("[HTTP Client] Error writing file:", error);
685
441
  throw error;
686
442
  }
687
443
  }
688
444
 
689
- async mkdirStream(
445
+ async readFile(
690
446
  path: string,
691
- recursive: boolean = false,
692
- sessionId?: string
693
- ): Promise<void> {
447
+ encoding: string = "utf-8",
448
+ sessionId: string
449
+ ): Promise<ReadFileResponse> {
694
450
  try {
695
- const targetSessionId = sessionId || this.sessionId;
696
-
697
- const response = await this.doFetch(`/api/mkdir/stream`, {
451
+ const response = await this.doFetch(`/api/read`, {
698
452
  body: JSON.stringify({
453
+ encoding,
699
454
  path,
700
- recursive,
701
- sessionId: targetSessionId,
702
- }),
455
+ sessionId,
456
+ } as ReadFileRequest),
703
457
  headers: {
704
458
  "Content-Type": "application/json",
705
459
  },
@@ -715,117 +469,28 @@ export class HttpClient {
715
469
  );
716
470
  }
717
471
 
718
- if (!response.body) {
719
- throw new Error("No response body for streaming request");
720
- }
472
+ const data: ReadFileResponse = await response.json();
473
+ console.log(
474
+ `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}${sessionId ? ` in session: ${sessionId}` : ''}`
475
+ );
721
476
 
722
- const reader = response.body.getReader();
723
- const decoder = new TextDecoder();
724
-
725
- try {
726
- while (true) {
727
- const { done, value } = await reader.read();
728
-
729
- if (done) {
730
- break;
731
- }
732
-
733
- const chunk = decoder.decode(value, { stream: true });
734
- const lines = chunk.split("\n");
735
-
736
- for (const line of lines) {
737
- if (line.startsWith("data: ")) {
738
- try {
739
- const eventData = line.slice(6); // Remove 'data: ' prefix
740
- const event: StreamEvent = JSON.parse(eventData);
741
-
742
- console.log(`[HTTP Client] Mkdir stream event: ${event.type}`);
743
- this.options.onStreamEvent?.(event);
744
-
745
- switch (event.type) {
746
- case "command_start":
747
- console.log(
748
- `[HTTP Client] Mkdir started: ${
749
- event.command
750
- } ${event.args?.join(" ")}`
751
- );
752
- this.options.onCommandStart?.(
753
- event.command!,
754
- event.args || []
755
- );
756
- break;
757
-
758
- case "output":
759
- console.log(`[${event.stream}] ${event.data}`);
760
- this.options.onOutput?.(
761
- event.stream!,
762
- event.data!,
763
- event.command!
764
- );
765
- break;
766
-
767
- case "command_complete":
768
- console.log(
769
- `[HTTP Client] Mkdir completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
770
- );
771
- this.options.onCommandComplete?.(
772
- event.success!,
773
- event.exitCode!,
774
- event.stdout!,
775
- event.stderr!,
776
- event.command!,
777
- event.args || []
778
- );
779
- break;
780
-
781
- case "error":
782
- console.error(`[HTTP Client] Mkdir error: ${event.error}`);
783
- this.options.onError?.(
784
- event.error!,
785
- event.command,
786
- event.args
787
- );
788
- break;
789
- }
790
- } catch (parseError) {
791
- console.warn(
792
- "[HTTP Client] Failed to parse mkdir stream event:",
793
- parseError
794
- );
795
- }
796
- }
797
- }
798
- }
799
- } finally {
800
- reader.releaseLock();
801
- }
477
+ return data;
802
478
  } catch (error) {
803
- console.error("[HTTP Client] Error in streaming mkdir:", error);
804
- this.options.onError?.(
805
- error instanceof Error ? error.message : "Unknown error",
806
- "mkdir",
807
- recursive ? ["-p", path] : [path]
808
- );
479
+ console.error("[HTTP Client] Error reading file:", error);
809
480
  throw error;
810
481
  }
811
482
  }
812
483
 
813
- async writeFile(
484
+ async deleteFile(
814
485
  path: string,
815
- content: string,
816
- encoding: string = "utf-8",
817
- sessionId?: string
818
- ): Promise<WriteFileResponse> {
486
+ sessionId: string
487
+ ): Promise<DeleteFileResponse> {
819
488
  try {
820
- const targetSessionId = sessionId || this.sessionId;
821
-
822
- const response = await this.doFetch(`/api/write`, {
489
+ const response = await this.doFetch(`/api/delete`, {
823
490
  body: JSON.stringify({
824
- content,
825
- encoding,
826
491
  path,
827
- sessionId: targetSessionId,
828
- }),
492
+ sessionId,
493
+ } as DeleteFileRequest),
829
494
  headers: {
830
495
  "Content-Type": "application/json",
831
496
  },
@@ -841,34 +506,30 @@ export class HttpClient {
841
506
  );
842
507
  }
843
508
 
844
- const data: WriteFileResponse = await response.json();
509
+ const data: DeleteFileResponse = await response.json();
845
510
  console.log(
846
- `[HTTP Client] File written: ${path}, Success: ${data.success}`
511
+ `[HTTP Client] File deleted: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
847
512
  );
848
513
 
849
514
  return data;
850
515
  } catch (error) {
851
- console.error("[HTTP Client] Error writing file:", error);
516
+ console.error("[HTTP Client] Error deleting file:", error);
852
517
  throw error;
853
518
  }
854
519
  }
855
520
 
856
- async writeFileStream(
857
- path: string,
858
- content: string,
859
- encoding: string = "utf-8",
860
- sessionId?: string
861
- ): Promise<void> {
521
+ async renameFile(
522
+ oldPath: string,
523
+ newPath: string,
524
+ sessionId: string
525
+ ): Promise<RenameFileResponse> {
862
526
  try {
863
- const targetSessionId = sessionId || this.sessionId;
864
-
865
- const response = await this.doFetch(`/api/write/stream`, {
527
+ const response = await this.doFetch(`/api/rename`, {
866
528
  body: JSON.stringify({
867
- content,
868
- encoding,
869
- path,
870
- sessionId: targetSessionId,
871
- }),
529
+ newPath,
530
+ oldPath,
531
+ sessionId,
532
+ } as RenameFileRequest),
872
533
  headers: {
873
534
  "Content-Type": "application/json",
874
535
  },
@@ -884,112 +545,30 @@ export class HttpClient {
884
545
  );
885
546
  }
886
547
 
887
- if (!response.body) {
888
- throw new Error("No response body for streaming request");
889
- }
548
+ const data: RenameFileResponse = await response.json();
549
+ console.log(
550
+ `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
551
+ );
890
552
 
891
- const reader = response.body.getReader();
892
- const decoder = new TextDecoder();
893
-
894
- try {
895
- while (true) {
896
- const { done, value } = await reader.read();
897
-
898
- if (done) {
899
- break;
900
- }
901
-
902
- const chunk = decoder.decode(value, { stream: true });
903
- const lines = chunk.split("\n");
904
-
905
- for (const line of lines) {
906
- if (line.startsWith("data: ")) {
907
- try {
908
- const eventData = line.slice(6); // Remove 'data: ' prefix
909
- const event: StreamEvent = JSON.parse(eventData);
910
-
911
- console.log(
912
- `[HTTP Client] Write file stream event: ${event.type}`
913
- );
914
- this.options.onStreamEvent?.(event);
915
-
916
- switch (event.type) {
917
- case "command_start":
918
- console.log(
919
- `[HTTP Client] Write file started: ${event.path}`
920
- );
921
- this.options.onCommandStart?.("write", [
922
- path,
923
- content,
924
- encoding,
925
- ]);
926
- break;
927
-
928
- case "output":
929
- console.log(`[output] ${event.message}`);
930
- this.options.onOutput?.("stdout", event.message!, "write");
931
- break;
932
-
933
- case "command_complete":
934
- console.log(
935
- `[HTTP Client] Write file completed: ${event.path}, Success: ${event.success}`
936
- );
937
- this.options.onCommandComplete?.(
938
- event.success!,
939
- 0,
940
- "",
941
- "",
942
- "write",
943
- [path, content, encoding]
944
- );
945
- break;
946
-
947
- case "error":
948
- console.error(
949
- `[HTTP Client] Write file error: ${event.error}`
950
- );
951
- this.options.onError?.(event.error!, "write", [
952
- path,
953
- content,
954
- encoding,
955
- ]);
956
- break;
957
- }
958
- } catch (parseError) {
959
- console.warn(
960
- "[HTTP Client] Failed to parse write file stream event:",
961
- parseError
962
- );
963
- }
964
- }
965
- }
966
- }
967
- } finally {
968
- reader.releaseLock();
969
- }
553
+ return data;
970
554
  } catch (error) {
971
- console.error("[HTTP Client] Error in streaming write file:", error);
972
- this.options.onError?.(
973
- error instanceof Error ? error.message : "Unknown error",
974
- "write",
975
- [path, content, encoding]
976
- );
555
+ console.error("[HTTP Client] Error renaming file:", error);
977
556
  throw error;
978
557
  }
979
558
  }
980
559
 
981
- async deleteFile(
982
- path: string,
983
- sessionId?: string
984
- ): Promise<DeleteFileResponse> {
560
+ async moveFile(
561
+ sourcePath: string,
562
+ destinationPath: string,
563
+ sessionId: string
564
+ ): Promise<MoveFileResponse> {
985
565
  try {
986
- const targetSessionId = sessionId || this.sessionId;
987
-
988
- const response = await this.doFetch(`/api/delete`, {
566
+ const response = await this.doFetch(`/api/move`, {
989
567
  body: JSON.stringify({
990
- path,
991
- sessionId: targetSessionId,
992
- }),
568
+ destinationPath,
569
+ sourcePath,
570
+ sessionId,
571
+ } as MoveFileRequest),
993
572
  headers: {
994
573
  "Content-Type": "application/json",
995
574
  },
@@ -1005,27 +584,33 @@ export class HttpClient {
1005
584
  );
1006
585
  }
1007
586
 
1008
- const data: DeleteFileResponse = await response.json();
587
+ const data: MoveFileResponse = await response.json();
1009
588
  console.log(
1010
- `[HTTP Client] File deleted: ${path}, Success: ${data.success}`
589
+ `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1011
590
  );
1012
591
 
1013
592
  return data;
1014
593
  } catch (error) {
1015
- console.error("[HTTP Client] Error deleting file:", error);
594
+ console.error("[HTTP Client] Error moving file:", error);
1016
595
  throw error;
1017
596
  }
1018
597
  }
1019
598
 
1020
- async deleteFileStream(path: string, sessionId?: string): Promise<void> {
599
+ async listFiles(
600
+ path: string,
601
+ sessionId: string,
602
+ options?: {
603
+ recursive?: boolean;
604
+ includeHidden?: boolean;
605
+ }
606
+ ): Promise<ListFilesResponse> {
1021
607
  try {
1022
- const targetSessionId = sessionId || this.sessionId;
1023
-
1024
- const response = await this.doFetch(`/api/delete/stream`, {
608
+ const response = await this.doFetch(`/api/list-files`, {
1025
609
  body: JSON.stringify({
1026
610
  path,
1027
- sessionId: targetSessionId,
1028
- }),
611
+ options,
612
+ sessionId,
613
+ } as ListFilesRequest),
1029
614
  headers: {
1030
615
  "Content-Type": "application/json",
1031
616
  },
@@ -1041,100 +626,24 @@ export class HttpClient {
1041
626
  );
1042
627
  }
1043
628
 
1044
- if (!response.body) {
1045
- throw new Error("No response body for streaming request");
1046
- }
629
+ const data: ListFilesResponse = await response.json();
630
+ console.log(
631
+ `[HTTP Client] Listed ${data.files.length} files in: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
632
+ );
1047
633
 
1048
- const reader = response.body.getReader();
1049
- const decoder = new TextDecoder();
1050
-
1051
- try {
1052
- while (true) {
1053
- const { done, value } = await reader.read();
1054
-
1055
- if (done) {
1056
- break;
1057
- }
1058
-
1059
- const chunk = decoder.decode(value, { stream: true });
1060
- const lines = chunk.split("\n");
1061
-
1062
- for (const line of lines) {
1063
- if (line.startsWith("data: ")) {
1064
- try {
1065
- const eventData = line.slice(6); // Remove 'data: ' prefix
1066
- const event: StreamEvent = JSON.parse(eventData);
1067
-
1068
- console.log(
1069
- `[HTTP Client] Delete file stream event: ${event.type}`
1070
- );
1071
- this.options.onStreamEvent?.(event);
1072
-
1073
- switch (event.type) {
1074
- case "command_start":
1075
- console.log(
1076
- `[HTTP Client] Delete file started: ${event.path}`
1077
- );
1078
- this.options.onCommandStart?.("delete", [path]);
1079
- break;
1080
-
1081
- case "command_complete":
1082
- console.log(
1083
- `[HTTP Client] Delete file completed: ${event.path}, Success: ${event.success}`
1084
- );
1085
- this.options.onCommandComplete?.(
1086
- event.success!,
1087
- 0,
1088
- "",
1089
- "",
1090
- "delete",
1091
- [path]
1092
- );
1093
- break;
1094
-
1095
- case "error":
1096
- console.error(
1097
- `[HTTP Client] Delete file error: ${event.error}`
1098
- );
1099
- this.options.onError?.(event.error!, "delete", [path]);
1100
- break;
1101
- }
1102
- } catch (parseError) {
1103
- console.warn(
1104
- "[HTTP Client] Failed to parse delete file stream event:",
1105
- parseError
1106
- );
1107
- }
1108
- }
1109
- }
1110
- }
1111
- } finally {
1112
- reader.releaseLock();
1113
- }
634
+ return data;
1114
635
  } catch (error) {
1115
- console.error("[HTTP Client] Error in streaming delete file:", error);
1116
- this.options.onError?.(
1117
- error instanceof Error ? error.message : "Unknown error",
1118
- "delete",
1119
- [path]
1120
- );
636
+ console.error("[HTTP Client] Error listing files:", error);
1121
637
  throw error;
1122
638
  }
1123
639
  }
1124
640
 
1125
- async renameFile(
1126
- oldPath: string,
1127
- newPath: string,
1128
- sessionId?: string
1129
- ): Promise<RenameFileResponse> {
641
+ async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
1130
642
  try {
1131
- const targetSessionId = sessionId || this.sessionId;
1132
-
1133
- const response = await this.doFetch(`/api/rename`, {
643
+ const response = await this.doFetch(`/api/expose-port`, {
1134
644
  body: JSON.stringify({
1135
- newPath,
1136
- oldPath,
1137
- sessionId: targetSessionId,
645
+ port,
646
+ name,
1138
647
  }),
1139
648
  headers: {
1140
649
  "Content-Type": "application/json",
@@ -1146,41 +655,36 @@ export class HttpClient {
1146
655
  const errorData = (await response.json().catch(() => ({}))) as {
1147
656
  error?: string;
1148
657
  };
658
+ console.log(errorData);
1149
659
  throw new Error(
1150
660
  errorData.error || `HTTP error! status: ${response.status}`
1151
661
  );
1152
662
  }
1153
663
 
1154
- const data: RenameFileResponse = await response.json();
664
+ const data: ExposePortResponse = await response.json();
1155
665
  console.log(
1156
- `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}`
666
+ `[HTTP Client] Port exposed: ${port}${
667
+ name ? ` (${name})` : ""
668
+ }, Success: ${data.success}`
1157
669
  );
1158
670
 
1159
671
  return data;
1160
672
  } catch (error) {
1161
- console.error("[HTTP Client] Error renaming file:", error);
673
+ console.error("[HTTP Client] Error exposing port:", error);
1162
674
  throw error;
1163
675
  }
1164
676
  }
1165
677
 
1166
- async renameFileStream(
1167
- oldPath: string,
1168
- newPath: string,
1169
- sessionId?: string
1170
- ): Promise<void> {
678
+ async unexposePort(port: number): Promise<UnexposePortResponse> {
1171
679
  try {
1172
- const targetSessionId = sessionId || this.sessionId;
1173
-
1174
- const response = await this.doFetch(`/api/rename/stream`, {
680
+ const response = await this.doFetch(`/api/unexpose-port`, {
1175
681
  body: JSON.stringify({
1176
- newPath,
1177
- oldPath,
1178
- sessionId: targetSessionId,
682
+ port,
1179
683
  }),
1180
684
  headers: {
1181
685
  "Content-Type": "application/json",
1182
686
  },
1183
- method: "POST",
687
+ method: "DELETE",
1184
688
  });
1185
689
 
1186
690
  if (!response.ok) {
@@ -1192,108 +696,25 @@ export class HttpClient {
1192
696
  );
1193
697
  }
1194
698
 
1195
- if (!response.body) {
1196
- throw new Error("No response body for streaming request");
1197
- }
699
+ const data: UnexposePortResponse = await response.json();
700
+ console.log(
701
+ `[HTTP Client] Port unexposed: ${port}, Success: ${data.success}`
702
+ );
1198
703
 
1199
- const reader = response.body.getReader();
1200
- const decoder = new TextDecoder();
1201
-
1202
- try {
1203
- while (true) {
1204
- const { done, value } = await reader.read();
1205
-
1206
- if (done) {
1207
- break;
1208
- }
1209
-
1210
- const chunk = decoder.decode(value, { stream: true });
1211
- const lines = chunk.split("\n");
1212
-
1213
- for (const line of lines) {
1214
- if (line.startsWith("data: ")) {
1215
- try {
1216
- const eventData = line.slice(6); // Remove 'data: ' prefix
1217
- const event: StreamEvent = JSON.parse(eventData);
1218
-
1219
- console.log(
1220
- `[HTTP Client] Rename file stream event: ${event.type}`
1221
- );
1222
- this.options.onStreamEvent?.(event);
1223
-
1224
- switch (event.type) {
1225
- case "command_start":
1226
- console.log(
1227
- `[HTTP Client] Rename file started: ${event.oldPath} -> ${event.newPath}`
1228
- );
1229
- this.options.onCommandStart?.("rename", [oldPath, newPath]);
1230
- break;
1231
-
1232
- case "command_complete":
1233
- console.log(
1234
- `[HTTP Client] Rename file completed: ${event.oldPath} -> ${event.newPath}, Success: ${event.success}`
1235
- );
1236
- this.options.onCommandComplete?.(
1237
- event.success!,
1238
- 0,
1239
- "",
1240
- "",
1241
- "rename",
1242
- [oldPath, newPath]
1243
- );
1244
- break;
1245
-
1246
- case "error":
1247
- console.error(
1248
- `[HTTP Client] Rename file error: ${event.error}`
1249
- );
1250
- this.options.onError?.(event.error!, "rename", [
1251
- oldPath,
1252
- newPath,
1253
- ]);
1254
- break;
1255
- }
1256
- } catch (parseError) {
1257
- console.warn(
1258
- "[HTTP Client] Failed to parse rename file stream event:",
1259
- parseError
1260
- );
1261
- }
1262
- }
1263
- }
1264
- }
1265
- } finally {
1266
- reader.releaseLock();
1267
- }
704
+ return data;
1268
705
  } catch (error) {
1269
- console.error("[HTTP Client] Error in streaming rename file:", error);
1270
- this.options.onError?.(
1271
- error instanceof Error ? error.message : "Unknown error",
1272
- "rename",
1273
- [oldPath, newPath]
1274
- );
706
+ console.error("[HTTP Client] Error unexposing port:", error);
1275
707
  throw error;
1276
708
  }
1277
709
  }
1278
710
 
1279
- async moveFile(
1280
- sourcePath: string,
1281
- destinationPath: string,
1282
- sessionId?: string
1283
- ): Promise<MoveFileResponse> {
711
+ async getExposedPorts(): Promise<GetExposedPortsResponse> {
1284
712
  try {
1285
- const targetSessionId = sessionId || this.sessionId;
1286
-
1287
- const response = await this.doFetch(`/api/move`, {
1288
- body: JSON.stringify({
1289
- destinationPath,
1290
- sessionId: targetSessionId,
1291
- sourcePath,
1292
- }),
713
+ const response = await this.doFetch(`/api/exposed-ports`, {
1293
714
  headers: {
1294
715
  "Content-Type": "application/json",
1295
716
  },
1296
- method: "POST",
717
+ method: "GET",
1297
718
  });
1298
719
 
1299
720
  if (!response.ok) {
@@ -1305,32 +726,59 @@ export class HttpClient {
1305
726
  );
1306
727
  }
1307
728
 
1308
- const data: MoveFileResponse = await response.json();
1309
- console.log(
1310
- `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}`
1311
- );
729
+ const data: GetExposedPortsResponse = await response.json();
730
+ console.log(`[HTTP Client] Got ${data.count} exposed ports`);
1312
731
 
1313
732
  return data;
1314
733
  } catch (error) {
1315
- console.error("[HTTP Client] Error moving file:", error);
734
+ console.error("[HTTP Client] Error getting exposed ports:", error);
1316
735
  throw error;
1317
736
  }
1318
737
  }
1319
738
 
1320
- async moveFileStream(
1321
- sourcePath: string,
1322
- destinationPath: string,
1323
- sessionId?: string
1324
- ): Promise<void> {
739
+ async ping(): Promise<string> {
1325
740
  try {
1326
- 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
+ });
1327
747
 
1328
- const response = await this.doFetch(`/api/move/stream`, {
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", {
1329
777
  body: JSON.stringify({
1330
- destinationPath,
1331
- sessionId: targetSessionId,
1332
- sourcePath,
1333
- }),
778
+ command,
779
+ sessionId,
780
+ options,
781
+ } as StartProcessRequest),
1334
782
  headers: {
1335
783
  "Content-Type": "application/json",
1336
784
  },
@@ -1346,96 +794,24 @@ export class HttpClient {
1346
794
  );
1347
795
  }
1348
796
 
1349
- if (!response.body) {
1350
- throw new Error("No response body for streaming request");
1351
- }
797
+ const data: StartProcessResponse = await response.json();
798
+ console.log(
799
+ `[HTTP Client] Process started: ${command}, ID: ${data.process.id}`
800
+ );
1352
801
 
1353
- const reader = response.body.getReader();
1354
- const decoder = new TextDecoder();
1355
-
1356
- try {
1357
- while (true) {
1358
- const { done, value } = await reader.read();
1359
-
1360
- if (done) {
1361
- break;
1362
- }
1363
-
1364
- const chunk = decoder.decode(value, { stream: true });
1365
- const lines = chunk.split("\n");
1366
-
1367
- for (const line of lines) {
1368
- if (line.startsWith("data: ")) {
1369
- try {
1370
- const eventData = line.slice(6); // Remove 'data: ' prefix
1371
- const event: StreamEvent = JSON.parse(eventData);
1372
-
1373
- console.log(
1374
- `[HTTP Client] Move file stream event: ${event.type}`
1375
- );
1376
- this.options.onStreamEvent?.(event);
1377
-
1378
- switch (event.type) {
1379
- case "command_start":
1380
- console.log(
1381
- `[HTTP Client] Move file started: ${event.sourcePath} -> ${event.destinationPath}`
1382
- );
1383
- this.options.onCommandStart?.("move", [
1384
- sourcePath,
1385
- destinationPath,
1386
- ]);
1387
- break;
1388
-
1389
- case "command_complete":
1390
- console.log(
1391
- `[HTTP Client] Move file completed: ${event.sourcePath} -> ${event.destinationPath}, Success: ${event.success}`
1392
- );
1393
- this.options.onCommandComplete?.(
1394
- event.success!,
1395
- 0,
1396
- "",
1397
- "",
1398
- "move",
1399
- [sourcePath, destinationPath]
1400
- );
1401
- break;
1402
-
1403
- case "error":
1404
- console.error(
1405
- `[HTTP Client] Move file error: ${event.error}`
1406
- );
1407
- this.options.onError?.(event.error!, "move", [
1408
- sourcePath,
1409
- destinationPath,
1410
- ]);
1411
- break;
1412
- }
1413
- } catch (parseError) {
1414
- console.warn(
1415
- "[HTTP Client] Failed to parse move file stream event:",
1416
- parseError
1417
- );
1418
- }
1419
- }
1420
- }
1421
- }
1422
- } finally {
1423
- reader.releaseLock();
1424
- }
802
+ return data;
1425
803
  } catch (error) {
1426
- console.error("[HTTP Client] Error in streaming move file:", error);
1427
- this.options.onError?.(
1428
- error instanceof Error ? error.message : "Unknown error",
1429
- "move",
1430
- [sourcePath, destinationPath]
1431
- );
804
+ console.error("[HTTP Client] Error starting process:", error);
1432
805
  throw error;
1433
806
  }
1434
807
  }
1435
808
 
1436
- async ping(): Promise<string> {
809
+ async listProcesses(sessionId?: string): Promise<ListProcessesResponse> {
1437
810
  try {
1438
- const response = await this.doFetch(`/api/ping`, {
811
+ const url = sessionId
812
+ ? `/api/process/list?session=${encodeURIComponent(sessionId)}`
813
+ : "/api/process/list";
814
+ const response = await this.doFetch(url, {
1439
815
  headers: {
1440
816
  "Content-Type": "application/json",
1441
817
  },
@@ -1443,21 +819,27 @@ export class HttpClient {
1443
819
  });
1444
820
 
1445
821
  if (!response.ok) {
1446
- throw new Error(`HTTP error! status: ${response.status}`);
822
+ const errorData = (await response.json().catch(() => ({}))) as {
823
+ error?: string;
824
+ };
825
+ throw new Error(
826
+ errorData.error || `HTTP error! status: ${response.status}`
827
+ );
1447
828
  }
1448
829
 
1449
- const data: PingResponse = await response.json();
1450
- console.log(`[HTTP Client] Ping response: ${data.message}`);
1451
- return data.timestamp;
830
+ const data: ListProcessesResponse = await response.json();
831
+ console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
832
+
833
+ return data;
1452
834
  } catch (error) {
1453
- console.error("[HTTP Client] Error pinging server:", error);
835
+ console.error("[HTTP Client] Error listing processes:", error);
1454
836
  throw error;
1455
837
  }
1456
838
  }
1457
839
 
1458
- async getCommands(): Promise<string[]> {
840
+ async getProcess(processId: string): Promise<GetProcessResponse> {
1459
841
  try {
1460
- const response = await fetch(`${this.baseUrl}/api/commands`, {
842
+ const response = await this.doFetch(`/api/process/${processId}`, {
1461
843
  headers: {
1462
844
  "Content-Type": "application/json",
1463
845
  },
@@ -1465,260 +847,163 @@ export class HttpClient {
1465
847
  });
1466
848
 
1467
849
  if (!response.ok) {
1468
- throw new Error(`HTTP error! status: ${response.status}`);
850
+ const errorData = (await response.json().catch(() => ({}))) as {
851
+ error?: string;
852
+ };
853
+ throw new Error(
854
+ errorData.error || `HTTP error! status: ${response.status}`
855
+ );
1469
856
  }
1470
857
 
1471
- const data: CommandsResponse = await response.json();
858
+ const data: GetProcessResponse = await response.json();
1472
859
  console.log(
1473
- `[HTTP Client] Available commands: ${data.availableCommands.length}`
860
+ `[HTTP Client] Got process ${processId}: ${
861
+ data.process?.status || "not found"
862
+ }`
1474
863
  );
1475
- return data.availableCommands;
864
+
865
+ return data;
1476
866
  } catch (error) {
1477
- console.error("[HTTP Client] Error getting commands:", error);
867
+ console.error("[HTTP Client] Error getting process:", error);
1478
868
  throw error;
1479
869
  }
1480
870
  }
1481
871
 
1482
- getSessionId(): string | null {
1483
- return this.sessionId;
1484
- }
1485
-
1486
- setSessionId(sessionId: string): void {
1487
- this.sessionId = sessionId;
1488
- }
872
+ async killProcess(
873
+ processId: string
874
+ ): Promise<{ success: boolean; message: string }> {
875
+ try {
876
+ const response = await this.doFetch(`/api/process/${processId}`, {
877
+ headers: {
878
+ "Content-Type": "application/json",
879
+ },
880
+ method: "DELETE",
881
+ });
1489
882
 
1490
- clearSession(): void {
1491
- this.sessionId = null;
1492
- }
1493
- }
883
+ if (!response.ok) {
884
+ const errorData = (await response.json().catch(() => ({}))) as {
885
+ error?: string;
886
+ };
887
+ throw new Error(
888
+ errorData.error || `HTTP error! status: ${response.status}`
889
+ );
890
+ }
1494
891
 
1495
- // Example usage and utility functions
1496
- export function createClient(options?: HttpClientOptions): HttpClient {
1497
- return new HttpClient(options);
1498
- }
892
+ const data = (await response.json()) as {
893
+ success: boolean;
894
+ message: string;
895
+ };
896
+ console.log(`[HTTP Client] Killed process ${processId}`);
1499
897
 
1500
- // Convenience function for quick command execution
1501
- export async function quickExecute(
1502
- command: string,
1503
- args: string[] = [],
1504
- options?: HttpClientOptions
1505
- ): Promise<ExecuteResponse> {
1506
- const client = createClient(options);
1507
- await client.createSession();
1508
-
1509
- try {
1510
- return await client.execute(command, args);
1511
- } finally {
1512
- client.clearSession();
898
+ return data;
899
+ } catch (error) {
900
+ console.error("[HTTP Client] Error killing process:", error);
901
+ throw error;
902
+ }
1513
903
  }
1514
- }
1515
904
 
1516
- // Convenience function for quick streaming command execution
1517
- export async function quickExecuteStream(
1518
- command: string,
1519
- args: string[] = [],
1520
- options?: HttpClientOptions
1521
- ): Promise<void> {
1522
- const client = createClient(options);
1523
- await client.createSession();
1524
-
1525
- try {
1526
- await client.executeStream(command, args);
1527
- } finally {
1528
- client.clearSession();
1529
- }
1530
- }
905
+ async killAllProcesses(sessionId?: string): Promise<{
906
+ success: boolean;
907
+ killedCount: number;
908
+ message: string;
909
+ }> {
910
+ try {
911
+ const url = sessionId
912
+ ? `/api/process/kill-all?session=${encodeURIComponent(sessionId)}`
913
+ : "/api/process/kill-all";
914
+ const response = await this.doFetch(url, {
915
+ headers: {
916
+ "Content-Type": "application/json",
917
+ },
918
+ method: "DELETE",
919
+ });
1531
920
 
1532
- // Convenience function for quick git checkout
1533
- export async function quickGitCheckout(
1534
- repoUrl: string,
1535
- branch: string = "main",
1536
- targetDir?: string,
1537
- options?: HttpClientOptions
1538
- ): Promise<GitCheckoutResponse> {
1539
- const client = createClient(options);
1540
- await client.createSession();
1541
-
1542
- try {
1543
- return await client.gitCheckout(repoUrl, branch, targetDir);
1544
- } finally {
1545
- client.clearSession();
1546
- }
1547
- }
921
+ if (!response.ok) {
922
+ const errorData = (await response.json().catch(() => ({}))) as {
923
+ error?: string;
924
+ };
925
+ throw new Error(
926
+ errorData.error || `HTTP error! status: ${response.status}`
927
+ );
928
+ }
1548
929
 
1549
- // Convenience function for quick directory creation
1550
- export async function quickMkdir(
1551
- path: string,
1552
- recursive: boolean = false,
1553
- options?: HttpClientOptions
1554
- ): Promise<MkdirResponse> {
1555
- const client = createClient(options);
1556
- await client.createSession();
1557
-
1558
- try {
1559
- return await client.mkdir(path, recursive);
1560
- } finally {
1561
- client.clearSession();
1562
- }
1563
- }
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`);
1564
936
 
1565
- // Convenience function for quick streaming git checkout
1566
- export async function quickGitCheckoutStream(
1567
- repoUrl: string,
1568
- branch: string = "main",
1569
- targetDir?: string,
1570
- options?: HttpClientOptions
1571
- ): Promise<void> {
1572
- const client = createClient(options);
1573
- await client.createSession();
1574
-
1575
- try {
1576
- await client.gitCheckoutStream(repoUrl, branch, targetDir);
1577
- } finally {
1578
- client.clearSession();
937
+ return data;
938
+ } catch (error) {
939
+ console.error("[HTTP Client] Error killing all processes:", error);
940
+ throw error;
941
+ }
1579
942
  }
1580
- }
1581
943
 
1582
- // Convenience function for quick streaming directory creation
1583
- export async function quickMkdirStream(
1584
- path: string,
1585
- recursive: boolean = false,
1586
- options?: HttpClientOptions
1587
- ): Promise<void> {
1588
- const client = createClient(options);
1589
- await client.createSession();
1590
-
1591
- try {
1592
- await client.mkdirStream(path, recursive);
1593
- } finally {
1594
- client.clearSession();
1595
- }
1596
- }
944
+ async getProcessLogs(processId: string): Promise<GetProcessLogsResponse> {
945
+ try {
946
+ const response = await this.doFetch(`/api/process/${processId}/logs`, {
947
+ headers: {
948
+ "Content-Type": "application/json",
949
+ },
950
+ method: "GET",
951
+ });
1597
952
 
1598
- // Convenience function for quick file writing
1599
- export async function quickWriteFile(
1600
- path: string,
1601
- content: string,
1602
- encoding: string = "utf-8",
1603
- options?: HttpClientOptions
1604
- ): Promise<WriteFileResponse> {
1605
- const client = createClient(options);
1606
- await client.createSession();
1607
-
1608
- try {
1609
- return await client.writeFile(path, content, encoding);
1610
- } finally {
1611
- client.clearSession();
1612
- }
1613
- }
953
+ if (!response.ok) {
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
+ );
960
+ }
1614
961
 
1615
- // Convenience function for quick streaming file writing
1616
- export async function quickWriteFileStream(
1617
- path: string,
1618
- content: string,
1619
- encoding: string = "utf-8",
1620
- options?: HttpClientOptions
1621
- ): Promise<void> {
1622
- const client = createClient(options);
1623
- await client.createSession();
1624
-
1625
- try {
1626
- await client.writeFileStream(path, content, encoding);
1627
- } finally {
1628
- client.clearSession();
1629
- }
1630
- }
962
+ const data: GetProcessLogsResponse = await response.json();
963
+ console.log(`[HTTP Client] Got logs for process ${processId}`);
1631
964
 
1632
- // Convenience function for quick file deletion
1633
- export async function quickDeleteFile(
1634
- path: string,
1635
- options?: HttpClientOptions
1636
- ): Promise<DeleteFileResponse> {
1637
- const client = createClient(options);
1638
- await client.createSession();
1639
-
1640
- try {
1641
- return await client.deleteFile(path);
1642
- } finally {
1643
- client.clearSession();
965
+ return data;
966
+ } catch (error) {
967
+ console.error("[HTTP Client] Error getting process logs:", error);
968
+ throw error;
969
+ }
1644
970
  }
1645
- }
1646
971
 
1647
- // Convenience function for quick streaming file deletion
1648
- export async function quickDeleteFileStream(
1649
- path: string,
1650
- options?: HttpClientOptions
1651
- ): Promise<void> {
1652
- const client = createClient(options);
1653
- await client.createSession();
1654
-
1655
- try {
1656
- await client.deleteFileStream(path);
1657
- } finally {
1658
- client.clearSession();
1659
- }
1660
- }
972
+ async streamProcessLogs(
973
+ processId: string,
974
+ options?: { signal?: AbortSignal }
975
+ ): Promise<ReadableStream<Uint8Array>> {
976
+ try {
977
+ const response = await this.doFetch(`/api/process/${processId}/stream`, {
978
+ headers: {
979
+ Accept: "text/event-stream",
980
+ "Cache-Control": "no-cache",
981
+ },
982
+ method: "GET",
983
+ signal: options?.signal,
984
+ });
1661
985
 
1662
- // Convenience function for quick file renaming
1663
- export async function quickRenameFile(
1664
- oldPath: string,
1665
- newPath: string,
1666
- options?: HttpClientOptions
1667
- ): Promise<RenameFileResponse> {
1668
- const client = createClient(options);
1669
- await client.createSession();
1670
-
1671
- try {
1672
- return await client.renameFile(oldPath, newPath);
1673
- } finally {
1674
- client.clearSession();
1675
- }
1676
- }
986
+ if (!response.ok) {
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
+ }
1677
994
 
1678
- // Convenience function for quick streaming file renaming
1679
- export async function quickRenameFileStream(
1680
- oldPath: string,
1681
- newPath: string,
1682
- options?: HttpClientOptions
1683
- ): Promise<void> {
1684
- const client = createClient(options);
1685
- await client.createSession();
1686
-
1687
- try {
1688
- await client.renameFileStream(oldPath, newPath);
1689
- } finally {
1690
- client.clearSession();
1691
- }
1692
- }
995
+ if (!response.body) {
996
+ throw new Error("No response body for streaming request");
997
+ }
1693
998
 
1694
- // Convenience function for quick file moving
1695
- export async function quickMoveFile(
1696
- sourcePath: string,
1697
- destinationPath: string,
1698
- options?: HttpClientOptions
1699
- ): Promise<MoveFileResponse> {
1700
- const client = createClient(options);
1701
- await client.createSession();
1702
-
1703
- try {
1704
- return await client.moveFile(sourcePath, destinationPath);
1705
- } finally {
1706
- client.clearSession();
1707
- }
1708
- }
999
+ console.log(
1000
+ `[HTTP Client] Started streaming logs for process ${processId}`
1001
+ );
1709
1002
 
1710
- // Convenience function for quick streaming file moving
1711
- export async function quickMoveFileStream(
1712
- sourcePath: string,
1713
- destinationPath: string,
1714
- options?: HttpClientOptions
1715
- ): Promise<void> {
1716
- const client = createClient(options);
1717
- await client.createSession();
1718
-
1719
- try {
1720
- await client.moveFileStream(sourcePath, destinationPath);
1721
- } finally {
1722
- client.clearSession();
1003
+ return response.body;
1004
+ } catch (error) {
1005
+ console.error("[HTTP Client] Error streaming process logs:", error);
1006
+ throw error;
1007
+ }
1723
1008
  }
1724
1009
  }