@cloudflare/sandbox 0.3.6 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.turbo/turbo-build.log +44 -0
  2. package/CHANGELOG.md +6 -8
  3. package/Dockerfile +88 -18
  4. package/README.md +89 -824
  5. package/dist/{chunk-JTKON2SH.js → chunk-BCJ7SF3Q.js} +9 -5
  6. package/dist/chunk-BCJ7SF3Q.js.map +1 -0
  7. package/dist/chunk-BFVUNTP4.js +104 -0
  8. package/dist/chunk-BFVUNTP4.js.map +1 -0
  9. package/dist/{chunk-NNGBXDMY.js → chunk-EKSWCBCA.js} +3 -6
  10. package/dist/chunk-EKSWCBCA.js.map +1 -0
  11. package/dist/chunk-HGF554LH.js +2236 -0
  12. package/dist/chunk-HGF554LH.js.map +1 -0
  13. package/dist/{chunk-6UAWTJ5S.js → chunk-Z532A7QC.js} +13 -20
  14. package/dist/{chunk-6UAWTJ5S.js.map → chunk-Z532A7QC.js.map} +1 -1
  15. package/dist/file-stream.d.ts +16 -38
  16. package/dist/file-stream.js +1 -2
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.js +35 -39
  19. package/dist/index.js.map +1 -1
  20. package/dist/interpreter.d.ts +3 -3
  21. package/dist/interpreter.js +2 -2
  22. package/dist/request-handler.d.ts +4 -3
  23. package/dist/request-handler.js +4 -7
  24. package/dist/sandbox-D9K2ypln.d.ts +583 -0
  25. package/dist/sandbox.d.ts +3 -3
  26. package/dist/sandbox.js +4 -7
  27. package/dist/security.d.ts +4 -3
  28. package/dist/security.js +3 -3
  29. package/dist/sse-parser.js +1 -1
  30. package/package.json +11 -5
  31. package/src/clients/base-client.ts +280 -0
  32. package/src/clients/command-client.ts +115 -0
  33. package/src/clients/file-client.ts +269 -0
  34. package/src/clients/git-client.ts +92 -0
  35. package/src/clients/index.ts +63 -0
  36. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
  37. package/src/clients/port-client.ts +105 -0
  38. package/src/clients/process-client.ts +177 -0
  39. package/src/clients/sandbox-client.ts +41 -0
  40. package/src/clients/types.ts +84 -0
  41. package/src/clients/utility-client.ts +94 -0
  42. package/src/errors/adapter.ts +180 -0
  43. package/src/errors/classes.ts +469 -0
  44. package/src/errors/index.ts +105 -0
  45. package/src/file-stream.ts +119 -117
  46. package/src/index.ts +81 -69
  47. package/src/interpreter.ts +17 -8
  48. package/src/request-handler.ts +69 -43
  49. package/src/sandbox.ts +694 -533
  50. package/src/security.ts +14 -23
  51. package/src/sse-parser.ts +4 -8
  52. package/startup.sh +3 -0
  53. package/tests/base-client.test.ts +328 -0
  54. package/tests/command-client.test.ts +407 -0
  55. package/tests/file-client.test.ts +643 -0
  56. package/tests/file-stream.test.ts +306 -0
  57. package/tests/git-client.test.ts +328 -0
  58. package/tests/port-client.test.ts +301 -0
  59. package/tests/process-client.test.ts +658 -0
  60. package/tests/sandbox.test.ts +465 -0
  61. package/tests/sse-parser.test.ts +290 -0
  62. package/tests/utility-client.test.ts +266 -0
  63. package/tests/wrangler.jsonc +35 -0
  64. package/tsconfig.json +9 -1
  65. package/vitest.config.ts +31 -0
  66. package/container_src/bun.lock +0 -76
  67. package/container_src/circuit-breaker.ts +0 -121
  68. package/container_src/control-process.ts +0 -784
  69. package/container_src/handler/exec.ts +0 -185
  70. package/container_src/handler/file.ts +0 -457
  71. package/container_src/handler/git.ts +0 -130
  72. package/container_src/handler/ports.ts +0 -314
  73. package/container_src/handler/process.ts +0 -568
  74. package/container_src/handler/session.ts +0 -92
  75. package/container_src/index.ts +0 -601
  76. package/container_src/interpreter-service.ts +0 -276
  77. package/container_src/isolation.ts +0 -1213
  78. package/container_src/mime-processor.ts +0 -255
  79. package/container_src/package.json +0 -18
  80. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  81. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  82. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  83. package/container_src/runtime/process-pool.ts +0 -464
  84. package/container_src/shell-escape.ts +0 -42
  85. package/container_src/startup.sh +0 -11
  86. package/container_src/types.ts +0 -131
  87. package/dist/chunk-32UDXUPC.js +0 -671
  88. package/dist/chunk-32UDXUPC.js.map +0 -1
  89. package/dist/chunk-5DILEXGY.js +0 -85
  90. package/dist/chunk-5DILEXGY.js.map +0 -1
  91. package/dist/chunk-D3U63BZP.js +0 -240
  92. package/dist/chunk-D3U63BZP.js.map +0 -1
  93. package/dist/chunk-FXYPFGOZ.js +0 -129
  94. package/dist/chunk-FXYPFGOZ.js.map +0 -1
  95. package/dist/chunk-JTKON2SH.js.map +0 -1
  96. package/dist/chunk-NNGBXDMY.js.map +0 -1
  97. package/dist/chunk-SQLJNZ3K.js +0 -674
  98. package/dist/chunk-SQLJNZ3K.js.map +0 -1
  99. package/dist/chunk-W7TVRPBG.js +0 -108
  100. package/dist/chunk-W7TVRPBG.js.map +0 -1
  101. package/dist/client-B3RUab0s.d.ts +0 -225
  102. package/dist/client.d.ts +0 -4
  103. package/dist/client.js +0 -7
  104. package/dist/client.js.map +0 -1
  105. package/dist/errors.d.ts +0 -95
  106. package/dist/errors.js +0 -27
  107. package/dist/errors.js.map +0 -1
  108. package/dist/interpreter-client.d.ts +0 -4
  109. package/dist/interpreter-client.js +0 -9
  110. package/dist/interpreter-client.js.map +0 -1
  111. package/dist/interpreter-types.d.ts +0 -259
  112. package/dist/interpreter-types.js +0 -9
  113. package/dist/interpreter-types.js.map +0 -1
  114. package/dist/types.d.ts +0 -453
  115. package/dist/types.js +0 -45
  116. package/dist/types.js.map +0 -1
  117. package/src/client.ts +0 -1048
  118. package/src/errors.ts +0 -219
  119. package/src/interpreter-types.ts +0 -390
  120. package/src/types.ts +0 -571
@@ -0,0 +1,280 @@
1
+ import type { Logger } from "@repo/shared";
2
+ import { createNoOpLogger } from "@repo/shared";
3
+ import { getHttpStatus } from '@repo/shared/errors';
4
+ import type { ErrorResponse as NewErrorResponse } from '../errors';
5
+ import { createErrorFromResponse, ErrorCode } from '../errors';
6
+ import type { SandboxError } from '../errors/classes';
7
+ import type {
8
+ HttpClientOptions,
9
+ ResponseHandler
10
+ } from './types';
11
+
12
+ // Container provisioning retry configuration
13
+ const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget
14
+ const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay)
15
+
16
+ /**
17
+ * Abstract base class providing common HTTP functionality for all domain clients
18
+ */
19
+ export abstract class BaseHttpClient {
20
+ protected baseUrl: string;
21
+ protected options: HttpClientOptions;
22
+ protected logger: Logger;
23
+
24
+ constructor(options: HttpClientOptions = {}) {
25
+ this.options = options;
26
+ this.logger = options.logger ?? createNoOpLogger();
27
+ this.baseUrl = this.options.baseUrl!;
28
+ }
29
+
30
+ /**
31
+ * Core HTTP request method with automatic retry for container provisioning delays
32
+ */
33
+ protected async doFetch(
34
+ path: string,
35
+ options?: RequestInit
36
+ ): Promise<Response> {
37
+ const startTime = Date.now();
38
+ let attempt = 0;
39
+
40
+ while (true) {
41
+ const response = await this.executeFetch(path, options);
42
+
43
+ // Only retry container provisioning 503s, not user app 503s
44
+ if (response.status === 503) {
45
+ const isContainerProvisioning = await this.isContainerProvisioningError(response);
46
+
47
+ if (isContainerProvisioning) {
48
+ const elapsed = Date.now() - startTime;
49
+ const remaining = TIMEOUT_MS - elapsed;
50
+
51
+ // Check if we have enough time for another attempt
52
+ // (Need at least 10s: 8s for Container timeout + 2s delay)
53
+ if (remaining > MIN_TIME_FOR_RETRY_MS) {
54
+ // Exponential backoff: 2s, 4s, 8s, 16s (capped at 16s)
55
+ const delay = Math.min(2000 * 2 ** attempt, 16000);
56
+
57
+ this.logger.info('Container provisioning in progress, retrying', {
58
+ attempt: attempt + 1,
59
+ delayMs: delay,
60
+ remainingSec: Math.floor(remaining / 1000)
61
+ });
62
+
63
+ await new Promise(resolve => setTimeout(resolve, delay));
64
+ attempt++;
65
+ continue;
66
+ } else {
67
+ // Exhausted retries - log error and return response
68
+ // Let existing error handling convert to proper error
69
+ this.logger.error('Container failed to provision after multiple attempts', new Error(`Failed after ${attempt + 1} attempts over 60s`));
70
+ return response;
71
+ }
72
+ }
73
+ }
74
+
75
+ return response;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Make a POST request with JSON body
81
+ */
82
+ protected async post<T>(
83
+ endpoint: string,
84
+ data: Record<string, any>,
85
+ responseHandler?: ResponseHandler<T>
86
+ ): Promise<T> {
87
+ const response = await this.doFetch(endpoint, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ },
92
+ body: JSON.stringify(data),
93
+ });
94
+
95
+ return this.handleResponse(response, responseHandler);
96
+ }
97
+
98
+ /**
99
+ * Make a GET request
100
+ */
101
+ protected async get<T>(
102
+ endpoint: string,
103
+ responseHandler?: ResponseHandler<T>
104
+ ): Promise<T> {
105
+ const response = await this.doFetch(endpoint, {
106
+ method: 'GET',
107
+ });
108
+
109
+ return this.handleResponse(response, responseHandler);
110
+ }
111
+
112
+ /**
113
+ * Make a DELETE request
114
+ */
115
+ protected async delete<T>(
116
+ endpoint: string,
117
+ responseHandler?: ResponseHandler<T>
118
+ ): Promise<T> {
119
+ const response = await this.doFetch(endpoint, {
120
+ method: 'DELETE',
121
+ });
122
+
123
+ return this.handleResponse(response, responseHandler);
124
+ }
125
+
126
+
127
+ /**
128
+ * Handle HTTP response with error checking and parsing
129
+ */
130
+ protected async handleResponse<T>(
131
+ response: Response,
132
+ customHandler?: ResponseHandler<T>
133
+ ): Promise<T> {
134
+ if (!response.ok) {
135
+ await this.handleErrorResponse(response);
136
+ }
137
+
138
+ if (customHandler) {
139
+ return customHandler(response);
140
+ }
141
+
142
+ try {
143
+ return await response.json();
144
+ } catch (error) {
145
+ // Handle malformed JSON responses gracefully
146
+ const errorResponse: NewErrorResponse = {
147
+ code: ErrorCode.INVALID_JSON_RESPONSE,
148
+ message: `Invalid JSON response: ${error instanceof Error ? error.message : 'Unknown parsing error'}`,
149
+ context: {},
150
+ httpStatus: response.status,
151
+ timestamp: new Date().toISOString()
152
+ };
153
+ throw createErrorFromResponse(errorResponse);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Handle error responses with consistent error throwing
159
+ */
160
+ protected async handleErrorResponse(response: Response): Promise<never> {
161
+ let errorData: NewErrorResponse;
162
+
163
+ try {
164
+ errorData = await response.json();
165
+ } catch {
166
+ // Fallback if response isn't JSON or parsing fails
167
+ errorData = {
168
+ code: ErrorCode.INTERNAL_ERROR,
169
+ message: `HTTP error! status: ${response.status}`,
170
+ context: { statusText: response.statusText },
171
+ httpStatus: response.status,
172
+ timestamp: new Date().toISOString()
173
+ };
174
+ }
175
+
176
+ // Convert ErrorResponse to appropriate Error class
177
+ const error = createErrorFromResponse(errorData);
178
+
179
+ // Call error callback if provided
180
+ this.options.onError?.(errorData.message, undefined);
181
+
182
+ throw error;
183
+ }
184
+
185
+
186
+
187
+ /**
188
+ * Create a streaming response handler for Server-Sent Events
189
+ */
190
+ protected async handleStreamResponse(
191
+ response: Response
192
+ ): Promise<ReadableStream<Uint8Array>> {
193
+ if (!response.ok) {
194
+ await this.handleErrorResponse(response);
195
+ }
196
+
197
+ if (!response.body) {
198
+ throw new Error('No response body for streaming');
199
+ }
200
+
201
+ return response.body;
202
+ }
203
+
204
+ /**
205
+ * Utility method to log successful operations
206
+ */
207
+ protected logSuccess(operation: string, details?: string): void {
208
+ this.logger.info(`${operation} completed successfully`, details ? { details } : undefined);
209
+ }
210
+
211
+ /**
212
+ * Utility method to log errors intelligently
213
+ * Only logs unexpected errors (5xx), not expected errors (4xx)
214
+ *
215
+ * - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
216
+ * - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
217
+ */
218
+ protected logError(operation: string, error: unknown): void {
219
+ // Check if it's a SandboxError with HTTP status
220
+ if (error && typeof error === 'object' && 'httpStatus' in error) {
221
+ const httpStatus = (error as SandboxError).httpStatus;
222
+
223
+ // Only log server errors (5xx), not client errors (4xx)
224
+ if (httpStatus >= 500) {
225
+ this.logger.error(
226
+ `Unexpected error in ${operation}`,
227
+ error instanceof Error ? error : new Error(String(error)),
228
+ { httpStatus }
229
+ );
230
+ }
231
+ // 4xx errors are expected (validation, not found, etc.) - don't log
232
+ } else {
233
+ // Non-SandboxError (unexpected) - log it
234
+ this.logger.error(
235
+ `Error in ${operation}`,
236
+ error instanceof Error ? error : new Error(String(error))
237
+ );
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Check if 503 response is from container provisioning (retryable)
243
+ * vs user application (not retryable)
244
+ */
245
+ private async isContainerProvisioningError(response: Response): Promise<boolean> {
246
+ try {
247
+ // Clone response so we don't consume the original body
248
+ const cloned = response.clone();
249
+ const text = await cloned.text();
250
+
251
+ // Container package returns specific message for provisioning errors
252
+ return text.includes('There is no Container instance available');
253
+ } catch (error) {
254
+ this.logger.error('Error checking response body', error instanceof Error ? error : new Error(String(error)));
255
+ // If we can't read the body, don't retry to be safe
256
+ return false;
257
+ }
258
+ }
259
+
260
+ private async executeFetch(path: string, options?: RequestInit): Promise<Response> {
261
+ const url = this.options.stub
262
+ ? `http://localhost:${this.options.port}${path}`
263
+ : `${this.baseUrl}${path}`;
264
+
265
+ try {
266
+ if (this.options.stub) {
267
+ return await this.options.stub.containerFetch(
268
+ url,
269
+ options || {},
270
+ this.options.port
271
+ );
272
+ } else {
273
+ return await fetch(url, options);
274
+ }
275
+ } catch (error) {
276
+ this.logger.error('HTTP request error', error instanceof Error ? error : new Error(String(error)), { method: options?.method || 'GET', url });
277
+ throw error;
278
+ }
279
+ }
280
+ }
@@ -0,0 +1,115 @@
1
+ import { BaseHttpClient } from './base-client';
2
+ import type { BaseApiResponse, HttpClientOptions, SessionRequest } from './types';
3
+
4
+ /**
5
+ * Request interface for command execution
6
+ */
7
+ export interface ExecuteRequest extends SessionRequest {
8
+ command: string;
9
+ timeoutMs?: number;
10
+ }
11
+
12
+ /**
13
+ * Response interface for command execution
14
+ */
15
+ export interface ExecuteResponse extends BaseApiResponse {
16
+ stdout: string;
17
+ stderr: string;
18
+ exitCode: number;
19
+ command: string;
20
+ }
21
+
22
+ /**
23
+ * Client for command execution operations
24
+ */
25
+ export class CommandClient extends BaseHttpClient {
26
+
27
+ /**
28
+ * Execute a command and return the complete result
29
+ * @param command - The command to execute
30
+ * @param sessionId - The session ID for this command execution
31
+ * @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
32
+ */
33
+ async execute(
34
+ command: string,
35
+ sessionId: string,
36
+ timeoutMs?: number
37
+ ): Promise<ExecuteResponse> {
38
+ try {
39
+ const data: ExecuteRequest = {
40
+ command,
41
+ sessionId,
42
+ ...(timeoutMs !== undefined && { timeoutMs })
43
+ };
44
+
45
+ const response = await this.post<ExecuteResponse>(
46
+ '/api/execute',
47
+ data
48
+ );
49
+
50
+ this.logSuccess(
51
+ 'Command executed',
52
+ `${command}, Success: ${response.success}`
53
+ );
54
+
55
+ // Call the callback if provided
56
+ this.options.onCommandComplete?.(
57
+ response.success,
58
+ response.exitCode,
59
+ response.stdout,
60
+ response.stderr,
61
+ response.command
62
+ );
63
+
64
+ return response;
65
+ } catch (error) {
66
+ this.logError('execute', error);
67
+
68
+ // Call error callback if provided
69
+ this.options.onError?.(
70
+ error instanceof Error ? error.message : String(error),
71
+ command
72
+ );
73
+
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Execute a command and return a stream of events
80
+ * @param command - The command to execute
81
+ * @param sessionId - The session ID for this command execution
82
+ */
83
+ async executeStream(
84
+ command: string,
85
+ sessionId: string
86
+ ): Promise<ReadableStream<Uint8Array>> {
87
+ try {
88
+ const data = { command, sessionId };
89
+
90
+ const response = await this.doFetch('/api/execute/stream', {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ },
95
+ body: JSON.stringify(data),
96
+ });
97
+
98
+ const stream = await this.handleStreamResponse(response);
99
+
100
+ this.logSuccess('Command stream started', command);
101
+
102
+ return stream;
103
+ } catch (error) {
104
+ this.logError('executeStream', error);
105
+
106
+ // Call error callback if provided
107
+ this.options.onError?.(
108
+ error instanceof Error ? error.message : String(error),
109
+ command
110
+ );
111
+
112
+ throw error;
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,269 @@
1
+ import type {
2
+ DeleteFileResult,
3
+ ListFilesOptions,
4
+ ListFilesResult,
5
+ MkdirResult,
6
+ MoveFileResult,
7
+ ReadFileResult,
8
+ RenameFileResult,
9
+ WriteFileResult
10
+ } from '@repo/shared';
11
+ import { BaseHttpClient } from './base-client';
12
+ import type { HttpClientOptions, SessionRequest } from './types';
13
+
14
+ /**
15
+ * Request interface for creating directories
16
+ */
17
+ export interface MkdirRequest extends SessionRequest {
18
+ path: string;
19
+ recursive?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Request interface for writing files
24
+ */
25
+ export interface WriteFileRequest extends SessionRequest {
26
+ path: string;
27
+ content: string;
28
+ encoding?: string;
29
+ }
30
+
31
+ /**
32
+ * Request interface for reading files
33
+ */
34
+ export interface ReadFileRequest extends SessionRequest {
35
+ path: string;
36
+ encoding?: string;
37
+ }
38
+
39
+ /**
40
+ * Request interface for file operations (delete, rename, move)
41
+ */
42
+ export interface FileOperationRequest extends SessionRequest {
43
+ path: string;
44
+ newPath?: string; // For rename/move operations
45
+ }
46
+
47
+ /**
48
+ * Client for file system operations
49
+ */
50
+ export class FileClient extends BaseHttpClient {
51
+
52
+ /**
53
+ * Create a directory
54
+ * @param path - Directory path to create
55
+ * @param sessionId - The session ID for this operation
56
+ * @param options - Optional settings (recursive)
57
+ */
58
+ async mkdir(
59
+ path: string,
60
+ sessionId: string,
61
+ options?: { recursive?: boolean }
62
+ ): Promise<MkdirResult> {
63
+ try {
64
+ const data = {
65
+ path,
66
+ sessionId,
67
+ recursive: options?.recursive ?? false,
68
+ };
69
+
70
+ const response = await this.post<MkdirResult>('/api/mkdir', data);
71
+
72
+ this.logSuccess('Directory created', `${path} (recursive: ${data.recursive})`);
73
+ return response;
74
+ } catch (error) {
75
+ this.logError('mkdir', error);
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Write content to a file
82
+ * @param path - File path to write to
83
+ * @param content - Content to write
84
+ * @param sessionId - The session ID for this operation
85
+ * @param options - Optional settings (encoding)
86
+ */
87
+ async writeFile(
88
+ path: string,
89
+ content: string,
90
+ sessionId: string,
91
+ options?: { encoding?: string }
92
+ ): Promise<WriteFileResult> {
93
+ try {
94
+ const data = {
95
+ path,
96
+ content,
97
+ sessionId,
98
+ encoding: options?.encoding ?? 'utf8',
99
+ };
100
+
101
+ const response = await this.post<WriteFileResult>('/api/write', data);
102
+
103
+ this.logSuccess('File written', `${path} (${content.length} chars)`);
104
+ return response;
105
+ } catch (error) {
106
+ this.logError('writeFile', error);
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Read content from a file
113
+ * @param path - File path to read from
114
+ * @param sessionId - The session ID for this operation
115
+ * @param options - Optional settings (encoding)
116
+ */
117
+ async readFile(
118
+ path: string,
119
+ sessionId: string,
120
+ options?: { encoding?: string }
121
+ ): Promise<ReadFileResult> {
122
+ try {
123
+ const data = {
124
+ path,
125
+ sessionId,
126
+ encoding: options?.encoding ?? 'utf8',
127
+ };
128
+
129
+ const response = await this.post<ReadFileResult>('/api/read', data);
130
+
131
+ this.logSuccess('File read', `${path} (${response.content.length} chars)`);
132
+ return response;
133
+ } catch (error) {
134
+ this.logError('readFile', error);
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Stream a file using Server-Sent Events
141
+ * Returns a ReadableStream of SSE events containing metadata, chunks, and completion
142
+ * @param path - File path to stream
143
+ * @param sessionId - The session ID for this operation
144
+ */
145
+ async readFileStream(
146
+ path: string,
147
+ sessionId: string
148
+ ): Promise<ReadableStream<Uint8Array>> {
149
+ try {
150
+ const data = {
151
+ path,
152
+ sessionId,
153
+ };
154
+
155
+ const response = await this.doFetch('/api/read/stream', {
156
+ method: 'POST',
157
+ headers: {
158
+ 'Content-Type': 'application/json',
159
+ },
160
+ body: JSON.stringify(data),
161
+ });
162
+
163
+ const stream = await this.handleStreamResponse(response);
164
+ this.logSuccess('File stream started', path);
165
+ return stream;
166
+ } catch (error) {
167
+ this.logError('readFileStream', error);
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Delete a file
174
+ * @param path - File path to delete
175
+ * @param sessionId - The session ID for this operation
176
+ */
177
+ async deleteFile(
178
+ path: string,
179
+ sessionId: string
180
+ ): Promise<DeleteFileResult> {
181
+ try {
182
+ const data = { path, sessionId };
183
+
184
+ const response = await this.post<DeleteFileResult>('/api/delete', data);
185
+
186
+ this.logSuccess('File deleted', path);
187
+ return response;
188
+ } catch (error) {
189
+ this.logError('deleteFile', error);
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Rename a file
196
+ * @param path - Current file path
197
+ * @param newPath - New file path
198
+ * @param sessionId - The session ID for this operation
199
+ */
200
+ async renameFile(
201
+ path: string,
202
+ newPath: string,
203
+ sessionId: string
204
+ ): Promise<RenameFileResult> {
205
+ try {
206
+ const data = { oldPath: path, newPath, sessionId };
207
+
208
+ const response = await this.post<RenameFileResult>('/api/rename', data);
209
+
210
+ this.logSuccess('File renamed', `${path} -> ${newPath}`);
211
+ return response;
212
+ } catch (error) {
213
+ this.logError('renameFile', error);
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Move a file
220
+ * @param path - Current file path
221
+ * @param newPath - Destination file path
222
+ * @param sessionId - The session ID for this operation
223
+ */
224
+ async moveFile(
225
+ path: string,
226
+ newPath: string,
227
+ sessionId: string
228
+ ): Promise<MoveFileResult> {
229
+ try {
230
+ const data = { sourcePath: path, destinationPath: newPath, sessionId };
231
+
232
+ const response = await this.post<MoveFileResult>('/api/move', data);
233
+
234
+ this.logSuccess('File moved', `${path} -> ${newPath}`);
235
+ return response;
236
+ } catch (error) {
237
+ this.logError('moveFile', error);
238
+ throw error;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * List files in a directory
244
+ * @param path - Directory path to list
245
+ * @param sessionId - The session ID for this operation
246
+ * @param options - Optional settings (recursive, includeHidden)
247
+ */
248
+ async listFiles(
249
+ path: string,
250
+ sessionId: string,
251
+ options?: ListFilesOptions
252
+ ): Promise<ListFilesResult> {
253
+ try {
254
+ const data = {
255
+ path,
256
+ sessionId,
257
+ options: options || {},
258
+ };
259
+
260
+ const response = await this.post<ListFilesResult>('/api/list-files', data);
261
+
262
+ this.logSuccess('Files listed', `${path} (${response.count} files)`);
263
+ return response;
264
+ } catch (error) {
265
+ this.logError('listFiles', error);
266
+ throw error;
267
+ }
268
+ }
269
+ }