@cloudflare/sandbox 0.0.0-4bedc3a → 0.0.0-50bc24c

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 (71) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/Dockerfile +104 -65
  3. package/README.md +87 -772
  4. package/dist/index.d.ts +1907 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +3159 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +16 -8
  9. package/src/clients/base-client.ts +280 -0
  10. package/src/clients/command-client.ts +115 -0
  11. package/src/clients/file-client.ts +295 -0
  12. package/src/clients/git-client.ts +92 -0
  13. package/src/clients/index.ts +64 -0
  14. package/src/{jupyter-client.ts → clients/interpreter-client.ts} +148 -168
  15. package/src/clients/port-client.ts +105 -0
  16. package/src/clients/process-client.ts +177 -0
  17. package/src/clients/sandbox-client.ts +41 -0
  18. package/src/clients/types.ts +84 -0
  19. package/src/clients/utility-client.ts +119 -0
  20. package/src/errors/adapter.ts +180 -0
  21. package/src/errors/classes.ts +469 -0
  22. package/src/errors/index.ts +105 -0
  23. package/src/file-stream.ts +164 -0
  24. package/src/index.ts +83 -54
  25. package/src/interpreter.ts +22 -13
  26. package/src/request-handler.ts +80 -44
  27. package/src/sandbox.ts +883 -530
  28. package/src/security.ts +14 -23
  29. package/src/sse-parser.ts +4 -8
  30. package/src/version.ts +6 -0
  31. package/startup.sh +3 -0
  32. package/tests/base-client.test.ts +328 -0
  33. package/tests/command-client.test.ts +407 -0
  34. package/tests/file-client.test.ts +719 -0
  35. package/tests/file-stream.test.ts +306 -0
  36. package/tests/get-sandbox.test.ts +149 -0
  37. package/tests/git-client.test.ts +328 -0
  38. package/tests/port-client.test.ts +301 -0
  39. package/tests/process-client.test.ts +658 -0
  40. package/tests/request-handler.test.ts +240 -0
  41. package/tests/sandbox.test.ts +641 -0
  42. package/tests/sse-parser.test.ts +290 -0
  43. package/tests/utility-client.test.ts +332 -0
  44. package/tests/version.test.ts +16 -0
  45. package/tests/wrangler.jsonc +35 -0
  46. package/tsconfig.json +9 -1
  47. package/tsdown.config.ts +12 -0
  48. package/vitest.config.ts +31 -0
  49. package/container_src/bun.lock +0 -122
  50. package/container_src/circuit-breaker.ts +0 -121
  51. package/container_src/control-process.ts +0 -784
  52. package/container_src/handler/exec.ts +0 -185
  53. package/container_src/handler/file.ts +0 -406
  54. package/container_src/handler/git.ts +0 -130
  55. package/container_src/handler/ports.ts +0 -314
  56. package/container_src/handler/process.ts +0 -568
  57. package/container_src/handler/session.ts +0 -92
  58. package/container_src/index.ts +0 -601
  59. package/container_src/isolation.ts +0 -1039
  60. package/container_src/jupyter-server.ts +0 -579
  61. package/container_src/jupyter-service.ts +0 -461
  62. package/container_src/jupyter_config.py +0 -48
  63. package/container_src/mime-processor.ts +0 -255
  64. package/container_src/package.json +0 -18
  65. package/container_src/shell-escape.ts +0 -42
  66. package/container_src/startup.sh +0 -84
  67. package/container_src/types.ts +0 -131
  68. package/src/client.ts +0 -1009
  69. package/src/errors.ts +0 -218
  70. package/src/interpreter-types.ts +0 -383
  71. package/src/types.ts +0 -502
@@ -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,295 @@
1
+ import type {
2
+ DeleteFileResult,
3
+ FileExistsResult,
4
+ ListFilesOptions,
5
+ ListFilesResult,
6
+ MkdirResult,
7
+ MoveFileResult,
8
+ ReadFileResult,
9
+ RenameFileResult,
10
+ WriteFileResult
11
+ } from '@repo/shared';
12
+ import { BaseHttpClient } from './base-client';
13
+ import type { HttpClientOptions, SessionRequest } from './types';
14
+
15
+ /**
16
+ * Request interface for creating directories
17
+ */
18
+ export interface MkdirRequest extends SessionRequest {
19
+ path: string;
20
+ recursive?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Request interface for writing files
25
+ */
26
+ export interface WriteFileRequest extends SessionRequest {
27
+ path: string;
28
+ content: string;
29
+ encoding?: string;
30
+ }
31
+
32
+ /**
33
+ * Request interface for reading files
34
+ */
35
+ export interface ReadFileRequest extends SessionRequest {
36
+ path: string;
37
+ encoding?: string;
38
+ }
39
+
40
+ /**
41
+ * Request interface for file operations (delete, rename, move)
42
+ */
43
+ export interface FileOperationRequest extends SessionRequest {
44
+ path: string;
45
+ newPath?: string; // For rename/move operations
46
+ }
47
+
48
+ /**
49
+ * Client for file system operations
50
+ */
51
+ export class FileClient extends BaseHttpClient {
52
+
53
+ /**
54
+ * Create a directory
55
+ * @param path - Directory path to create
56
+ * @param sessionId - The session ID for this operation
57
+ * @param options - Optional settings (recursive)
58
+ */
59
+ async mkdir(
60
+ path: string,
61
+ sessionId: string,
62
+ options?: { recursive?: boolean }
63
+ ): Promise<MkdirResult> {
64
+ try {
65
+ const data = {
66
+ path,
67
+ sessionId,
68
+ recursive: options?.recursive ?? false,
69
+ };
70
+
71
+ const response = await this.post<MkdirResult>('/api/mkdir', data);
72
+
73
+ this.logSuccess('Directory created', `${path} (recursive: ${data.recursive})`);
74
+ return response;
75
+ } catch (error) {
76
+ this.logError('mkdir', error);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Write content to a file
83
+ * @param path - File path to write to
84
+ * @param content - Content to write
85
+ * @param sessionId - The session ID for this operation
86
+ * @param options - Optional settings (encoding)
87
+ */
88
+ async writeFile(
89
+ path: string,
90
+ content: string,
91
+ sessionId: string,
92
+ options?: { encoding?: string }
93
+ ): Promise<WriteFileResult> {
94
+ try {
95
+ const data = {
96
+ path,
97
+ content,
98
+ sessionId,
99
+ encoding: options?.encoding ?? 'utf8',
100
+ };
101
+
102
+ const response = await this.post<WriteFileResult>('/api/write', data);
103
+
104
+ this.logSuccess('File written', `${path} (${content.length} chars)`);
105
+ return response;
106
+ } catch (error) {
107
+ this.logError('writeFile', error);
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Read content from a file
114
+ * @param path - File path to read from
115
+ * @param sessionId - The session ID for this operation
116
+ * @param options - Optional settings (encoding)
117
+ */
118
+ async readFile(
119
+ path: string,
120
+ sessionId: string,
121
+ options?: { encoding?: string }
122
+ ): Promise<ReadFileResult> {
123
+ try {
124
+ const data = {
125
+ path,
126
+ sessionId,
127
+ encoding: options?.encoding ?? 'utf8',
128
+ };
129
+
130
+ const response = await this.post<ReadFileResult>('/api/read', data);
131
+
132
+ this.logSuccess('File read', `${path} (${response.content.length} chars)`);
133
+ return response;
134
+ } catch (error) {
135
+ this.logError('readFile', error);
136
+ throw error;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Stream a file using Server-Sent Events
142
+ * Returns a ReadableStream of SSE events containing metadata, chunks, and completion
143
+ * @param path - File path to stream
144
+ * @param sessionId - The session ID for this operation
145
+ */
146
+ async readFileStream(
147
+ path: string,
148
+ sessionId: string
149
+ ): Promise<ReadableStream<Uint8Array>> {
150
+ try {
151
+ const data = {
152
+ path,
153
+ sessionId,
154
+ };
155
+
156
+ const response = await this.doFetch('/api/read/stream', {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ },
161
+ body: JSON.stringify(data),
162
+ });
163
+
164
+ const stream = await this.handleStreamResponse(response);
165
+ this.logSuccess('File stream started', path);
166
+ return stream;
167
+ } catch (error) {
168
+ this.logError('readFileStream', error);
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Delete a file
175
+ * @param path - File path to delete
176
+ * @param sessionId - The session ID for this operation
177
+ */
178
+ async deleteFile(
179
+ path: string,
180
+ sessionId: string
181
+ ): Promise<DeleteFileResult> {
182
+ try {
183
+ const data = { path, sessionId };
184
+
185
+ const response = await this.post<DeleteFileResult>('/api/delete', data);
186
+
187
+ this.logSuccess('File deleted', path);
188
+ return response;
189
+ } catch (error) {
190
+ this.logError('deleteFile', error);
191
+ throw error;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Rename a file
197
+ * @param path - Current file path
198
+ * @param newPath - New file path
199
+ * @param sessionId - The session ID for this operation
200
+ */
201
+ async renameFile(
202
+ path: string,
203
+ newPath: string,
204
+ sessionId: string
205
+ ): Promise<RenameFileResult> {
206
+ try {
207
+ const data = { oldPath: path, newPath, sessionId };
208
+
209
+ const response = await this.post<RenameFileResult>('/api/rename', data);
210
+
211
+ this.logSuccess('File renamed', `${path} -> ${newPath}`);
212
+ return response;
213
+ } catch (error) {
214
+ this.logError('renameFile', error);
215
+ throw error;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Move a file
221
+ * @param path - Current file path
222
+ * @param newPath - Destination file path
223
+ * @param sessionId - The session ID for this operation
224
+ */
225
+ async moveFile(
226
+ path: string,
227
+ newPath: string,
228
+ sessionId: string
229
+ ): Promise<MoveFileResult> {
230
+ try {
231
+ const data = { sourcePath: path, destinationPath: newPath, sessionId };
232
+
233
+ const response = await this.post<MoveFileResult>('/api/move', data);
234
+
235
+ this.logSuccess('File moved', `${path} -> ${newPath}`);
236
+ return response;
237
+ } catch (error) {
238
+ this.logError('moveFile', error);
239
+ throw error;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * List files in a directory
245
+ * @param path - Directory path to list
246
+ * @param sessionId - The session ID for this operation
247
+ * @param options - Optional settings (recursive, includeHidden)
248
+ */
249
+ async listFiles(
250
+ path: string,
251
+ sessionId: string,
252
+ options?: ListFilesOptions
253
+ ): Promise<ListFilesResult> {
254
+ try {
255
+ const data = {
256
+ path,
257
+ sessionId,
258
+ options: options || {},
259
+ };
260
+
261
+ const response = await this.post<ListFilesResult>('/api/list-files', data);
262
+
263
+ this.logSuccess('Files listed', `${path} (${response.count} files)`);
264
+ return response;
265
+ } catch (error) {
266
+ this.logError('listFiles', error);
267
+ throw error;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Check if a file or directory exists
273
+ * @param path - Path to check
274
+ * @param sessionId - The session ID for this operation
275
+ */
276
+ async exists(
277
+ path: string,
278
+ sessionId: string
279
+ ): Promise<FileExistsResult> {
280
+ try {
281
+ const data = {
282
+ path,
283
+ sessionId,
284
+ };
285
+
286
+ const response = await this.post<FileExistsResult>('/api/exists', data);
287
+
288
+ this.logSuccess('Path existence checked', `${path} (exists: ${response.exists})`);
289
+ return response;
290
+ } catch (error) {
291
+ this.logError('exists', error);
292
+ throw error;
293
+ }
294
+ }
295
+ }