tensorlake 0.4.50 → 0.5.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.
@@ -79,10 +79,11 @@ var init_models = __esm({
79
79
  });
80
80
 
81
81
  // src/defaults.ts
82
- var API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
82
+ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
83
83
  var init_defaults = __esm({
84
84
  "src/defaults.ts"() {
85
85
  "use strict";
86
+ SDK_VERSION = "0.4.49";
86
87
  API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
87
88
  API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
88
89
  NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
@@ -156,6 +157,12 @@ var init_errors = __esm({
156
157
  });
157
158
 
158
159
  // src/http.ts
160
+ function withTraceId(value, traceId) {
161
+ if (value == null) {
162
+ return { traceId };
163
+ }
164
+ return Object.assign(value, { traceId });
165
+ }
159
166
  function hasHeader(headers, name) {
160
167
  const lowered = name.toLowerCase();
161
168
  return Object.keys(headers).some((key) => key.toLowerCase() === lowered);
@@ -225,7 +232,7 @@ var init_http = __esm({
225
232
  allowH2: true
226
233
  })
227
234
  );
228
- HttpClient = class {
235
+ HttpClient = class _HttpClient {
229
236
  baseUrl;
230
237
  headers;
231
238
  maxRetries;
@@ -238,7 +245,9 @@ var init_http = __esm({
238
245
  this.maxRetries = options.maxRetries ?? MAX_RETRIES;
239
246
  this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
240
247
  this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
241
- this.headers = {};
248
+ this.headers = {
249
+ "User-Agent": `tensorlake-typescript-sdk/${SDK_VERSION}`
250
+ };
242
251
  if (options.apiKey) {
243
252
  this.headers["Authorization"] = `Bearer ${options.apiKey}`;
244
253
  }
@@ -267,8 +276,8 @@ var init_http = __esm({
267
276
  signal: options?.signal
268
277
  });
269
278
  const text = await response.text();
270
- if (!text) return void 0;
271
- return JSON.parse(text);
279
+ const data = text ? JSON.parse(text) : void 0;
280
+ return withTraceId(data, response.traceId);
272
281
  }
273
282
  /** Make a request returning raw bytes. */
274
283
  async requestBytes(method, path2, options) {
@@ -282,7 +291,7 @@ var init_http = __esm({
282
291
  signal: options?.signal
283
292
  });
284
293
  const buffer = await response.arrayBuffer();
285
- return new Uint8Array(buffer);
294
+ return withTraceId(new Uint8Array(buffer), response.traceId);
286
295
  }
287
296
  /** Make a request and return the response body as an SSE stream. */
288
297
  async requestStream(method, path2, options) {
@@ -297,7 +306,7 @@ var init_http = __esm({
297
306
  "No response body for SSE stream"
298
307
  );
299
308
  }
300
- return response.body;
309
+ return withTraceId(response.body, response.traceId);
301
310
  }
302
311
  /** Make a request and return the raw Response. */
303
312
  async requestResponse(method, path2, options) {
@@ -310,7 +319,7 @@ var init_http = __esm({
310
319
  headers["Content-Type"] = "application/json";
311
320
  }
312
321
  const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
313
- return this.doFetch(
322
+ const { response, traceId } = await this.doFetch(
314
323
  method,
315
324
  path2,
316
325
  body,
@@ -318,9 +327,18 @@ var init_http = __esm({
318
327
  options?.signal,
319
328
  options?.allowHttpErrors ?? false
320
329
  );
330
+ return withTraceId(response, traceId);
331
+ }
332
+ static makeTraceparent() {
333
+ const randomHex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map((b) => b.toString(16).padStart(2, "0")).join("");
334
+ const traceId = randomHex(16);
335
+ const spanId = randomHex(8);
336
+ return { traceparent: `00-${traceId}-${spanId}-01`, traceId };
321
337
  }
322
338
  async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
323
339
  const url = `${this.baseUrl}${path2}`;
340
+ const { traceparent, traceId } = _HttpClient.makeTraceparent();
341
+ headers["traceparent"] = traceparent;
324
342
  let lastError;
325
343
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
326
344
  if (attempt > 0) {
@@ -341,7 +359,7 @@ var init_http = __esm({
341
359
  signal: combinedSignal
342
360
  });
343
361
  clearTimeout(timeoutId);
344
- if (response.ok) return response;
362
+ if (response.ok) return { response, traceId };
345
363
  if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
346
364
  lastError = new RemoteAPIError(
347
365
  response.status,
@@ -350,7 +368,7 @@ var init_http = __esm({
350
368
  continue;
351
369
  }
352
370
  if (allowHttpErrors) {
353
- return response;
371
+ return { response, traceId };
354
372
  }
355
373
  const errorBody = await response.text().catch(() => "");
356
374
  throwMappedError(response.status, errorBody, path2);
@@ -3097,13 +3115,17 @@ var init_sandbox = __esm({
3097
3115
  };
3098
3116
  Sandbox = class {
3099
3117
  sandboxId;
3118
+ traceId = null;
3100
3119
  http;
3101
3120
  baseUrl;
3102
3121
  wsHeaders;
3103
3122
  ownsSandbox = false;
3104
3123
  lifecycleClient = null;
3124
+ lifecycleIdentifier;
3125
+ sandboxName = null;
3105
3126
  constructor(options) {
3106
3127
  this.sandboxId = options.sandboxId;
3128
+ this.lifecycleIdentifier = options.sandboxId;
3107
3129
  const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
3108
3130
  const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
3109
3131
  this.baseUrl = baseUrl;
@@ -3129,21 +3151,182 @@ var init_sandbox = __esm({
3129
3151
  routingHint: options.routingHint
3130
3152
  });
3131
3153
  }
3154
+ get name() {
3155
+ return this.sandboxName;
3156
+ }
3157
+ /** @internal Used by client wiring to keep locally cached name in sync. */
3158
+ _setName(name) {
3159
+ this.sandboxName = name;
3160
+ }
3161
+ /** @internal Used by lifecycle operations to pin to canonical sandbox ID. */
3162
+ _setLifecycleIdentifier(identifier) {
3163
+ this.lifecycleIdentifier = identifier;
3164
+ }
3132
3165
  /** @internal Used by SandboxClient.createAndConnect to set ownership. */
3133
3166
  _setOwner(client) {
3134
3167
  this.ownsSandbox = true;
3135
3168
  this.lifecycleClient = client;
3136
3169
  }
3170
+ // --- Static factory methods ---
3171
+ /**
3172
+ * Create a new sandbox and return a connected, running handle.
3173
+ *
3174
+ * Covers both fresh sandbox creation and restore-from-snapshot (set
3175
+ * `snapshotId`). Blocks until the sandbox is `Running`.
3176
+ */
3177
+ static async create(options) {
3178
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3179
+ const client = new SandboxClient2(
3180
+ options,
3181
+ /* _internal */
3182
+ true
3183
+ );
3184
+ const sandbox = await client.createAndConnect(options);
3185
+ sandbox.lifecycleClient = client;
3186
+ return sandbox;
3187
+ }
3188
+ /**
3189
+ * Attach to an existing sandbox and return a connected handle.
3190
+ *
3191
+ * Verifies the sandbox exists via a server GET call, then returns a handle
3192
+ * in whatever state the sandbox is in. Does **not** auto-resume a suspended
3193
+ * sandbox — call `sandbox.resume()` explicitly.
3194
+ */
3195
+ static async connect(options) {
3196
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3197
+ const client = new SandboxClient2(
3198
+ options,
3199
+ /* _internal */
3200
+ true
3201
+ );
3202
+ const info = await client.get(options.sandboxId);
3203
+ const sandbox = client.connect(
3204
+ info.sandboxId,
3205
+ options.proxyUrl,
3206
+ options.routingHint ?? info.routingHint
3207
+ );
3208
+ sandbox.lifecycleClient = client;
3209
+ sandbox._setLifecycleIdentifier(info.sandboxId);
3210
+ sandbox._setName(info.name ?? null);
3211
+ return sandbox;
3212
+ }
3213
+ // --- Static snapshot management ---
3214
+ /** Get information about a snapshot by ID. No sandbox handle needed. */
3215
+ static async getSnapshot(snapshotId, options) {
3216
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3217
+ const client = new SandboxClient2(
3218
+ options,
3219
+ /* _internal */
3220
+ true
3221
+ );
3222
+ return client.getSnapshot(snapshotId);
3223
+ }
3224
+ /** Delete a snapshot by ID. No sandbox handle needed. */
3225
+ static async deleteSnapshot(snapshotId, options) {
3226
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3227
+ const client = new SandboxClient2(
3228
+ options,
3229
+ /* _internal */
3230
+ true
3231
+ );
3232
+ await client.deleteSnapshot(snapshotId);
3233
+ }
3234
+ // --- Instance lifecycle methods ---
3235
+ requireLifecycleClient(operation) {
3236
+ if (!this.lifecycleClient) {
3237
+ throw new SandboxError(
3238
+ `Cannot ${operation}: no lifecycle client available. Use Sandbox.create() or Sandbox.connect() to get a lifecycle-aware handle.`
3239
+ );
3240
+ }
3241
+ return this.lifecycleClient;
3242
+ }
3243
+ /**
3244
+ * Fetch the current sandbox status from the server.
3245
+ *
3246
+ * Always hits the network — the value is not cached locally because the
3247
+ * status changes over the sandbox's lifecycle.
3248
+ */
3249
+ async status() {
3250
+ const client = this.requireLifecycleClient("read_status");
3251
+ const info = await client.get(this.lifecycleIdentifier);
3252
+ this._setLifecycleIdentifier(info.sandboxId);
3253
+ this._setName(info.name ?? null);
3254
+ return info.status;
3255
+ }
3256
+ /**
3257
+ * Update this sandbox's properties (name, exposed ports, proxy auth).
3258
+ *
3259
+ * Naming an ephemeral sandbox makes it non-ephemeral and enables
3260
+ * suspend/resume.
3261
+ */
3262
+ async update(options) {
3263
+ const client = this.requireLifecycleClient("update");
3264
+ const info = await client.update(this.lifecycleIdentifier, options);
3265
+ this._setLifecycleIdentifier(info.sandboxId);
3266
+ this._setName(info.name ?? null);
3267
+ return info;
3268
+ }
3269
+ /**
3270
+ * Suspend this sandbox.
3271
+ *
3272
+ * By default blocks until the sandbox is fully `Suspended`. Pass
3273
+ * `{ wait: false }` for fire-and-return.
3274
+ */
3275
+ async suspend(options) {
3276
+ const client = this.requireLifecycleClient("suspend");
3277
+ await client.suspend(this.lifecycleIdentifier, options);
3278
+ }
3279
+ /**
3280
+ * Resume this sandbox.
3281
+ *
3282
+ * By default blocks until the sandbox is `Running` and routable. Pass
3283
+ * `{ wait: false }` for fire-and-return.
3284
+ */
3285
+ async resume(options) {
3286
+ const client = this.requireLifecycleClient("resume");
3287
+ await client.resume(this.lifecycleIdentifier, options);
3288
+ }
3289
+ /**
3290
+ * Create a snapshot of this sandbox's filesystem and wait for it to
3291
+ * be committed.
3292
+ *
3293
+ * By default blocks until the snapshot artifact is ready and returns
3294
+ * the completed `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
3295
+ * (returns `undefined`).
3296
+ */
3297
+ async checkpoint(options) {
3298
+ const client = this.requireLifecycleClient("checkpoint");
3299
+ if (options?.wait === false) {
3300
+ await client.snapshot(this.lifecycleIdentifier, { contentMode: options.contentMode });
3301
+ return void 0;
3302
+ }
3303
+ return client.snapshotAndWait(this.lifecycleIdentifier, {
3304
+ timeout: options?.timeout,
3305
+ pollInterval: options?.pollInterval,
3306
+ contentMode: options?.contentMode
3307
+ });
3308
+ }
3309
+ /**
3310
+ * List snapshots taken from this sandbox.
3311
+ */
3312
+ async listSnapshots() {
3313
+ const client = this.requireLifecycleClient("listSnapshots");
3314
+ const all = await client.listSnapshots();
3315
+ const filtered = all.filter((s) => s.sandboxId === this.lifecycleIdentifier);
3316
+ return Object.assign(filtered, { traceId: all.traceId });
3317
+ }
3318
+ /** Close the HTTP client. The sandbox keeps running. */
3137
3319
  close() {
3138
3320
  this.http.close();
3139
3321
  }
3322
+ /** Terminate the sandbox and release all resources. */
3140
3323
  async terminate() {
3141
3324
  const client = this.lifecycleClient;
3142
3325
  this.ownsSandbox = false;
3143
3326
  this.lifecycleClient = null;
3144
3327
  this.close();
3145
3328
  if (client) {
3146
- await client.delete(this.sandboxId);
3329
+ await client.delete(this.lifecycleIdentifier);
3147
3330
  }
3148
3331
  }
3149
3332
  // --- High-level convenience ---
@@ -3189,6 +3372,14 @@ var init_sandbox = __esm({
3189
3372
  };
3190
3373
  }
3191
3374
  // --- Process management ---
3375
+ /**
3376
+ * Start a process in the sandbox without waiting for it to exit.
3377
+ *
3378
+ * Returns a `ProcessInfo` with the assigned `pid`. Use `getProcess()` to
3379
+ * poll status, or `followStdout()` / `followOutput()` to stream output
3380
+ * until the process exits. Use `run()` instead to block until completion
3381
+ * and get combined output in one call.
3382
+ */
3192
3383
  async startProcess(command, options) {
3193
3384
  const payload = { command };
3194
3385
  if (options?.args != null) payload.args = options.args;
@@ -3210,13 +3401,16 @@ var init_sandbox = __esm({
3210
3401
  );
3211
3402
  return fromSnakeKeys(raw);
3212
3403
  }
3404
+ /** List all processes (running and exited) tracked by the sandbox daemon. */
3213
3405
  async listProcesses() {
3214
3406
  const raw = await this.http.requestJson(
3215
3407
  "GET",
3216
3408
  "/api/v1/processes"
3217
3409
  );
3218
- return (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3410
+ const processes = (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3411
+ return Object.assign(processes, { traceId: raw.traceId });
3219
3412
  }
3413
+ /** Get current status and metadata for a process by PID. */
3220
3414
  async getProcess(pid) {
3221
3415
  const raw = await this.http.requestJson(
3222
3416
  "GET",
@@ -3224,9 +3418,11 @@ var init_sandbox = __esm({
3224
3418
  );
3225
3419
  return fromSnakeKeys(raw);
3226
3420
  }
3421
+ /** Send SIGKILL to a process. */
3227
3422
  async killProcess(pid) {
3228
3423
  await this.http.requestJson("DELETE", `/api/v1/processes/${pid}`);
3229
3424
  }
3425
+ /** Send an arbitrary signal to a process (e.g. `15` for SIGTERM, `9` for SIGKILL). */
3230
3426
  async sendSignal(pid, signal) {
3231
3427
  const raw = await this.http.requestJson(
3232
3428
  "POST",
@@ -3236,15 +3432,18 @@ var init_sandbox = __esm({
3236
3432
  return fromSnakeKeys(raw);
3237
3433
  }
3238
3434
  // --- Process I/O ---
3435
+ /** Write bytes to a process's stdin. The process must have been started with `stdinMode: StdinMode.PIPE`. */
3239
3436
  async writeStdin(pid, data) {
3240
3437
  await this.http.requestBytes("POST", `/api/v1/processes/${pid}/stdin`, {
3241
3438
  body: data,
3242
3439
  contentType: "application/octet-stream"
3243
3440
  });
3244
3441
  }
3442
+ /** Close a process's stdin pipe, signalling EOF to the process. */
3245
3443
  async closeStdin(pid) {
3246
3444
  await this.http.requestJson("POST", `/api/v1/processes/${pid}/stdin/close`);
3247
3445
  }
3446
+ /** Return all captured stdout lines produced so far by a process. */
3248
3447
  async getStdout(pid) {
3249
3448
  const raw = await this.http.requestJson(
3250
3449
  "GET",
@@ -3252,6 +3451,7 @@ var init_sandbox = __esm({
3252
3451
  );
3253
3452
  return fromSnakeKeys(raw);
3254
3453
  }
3454
+ /** Return all captured stderr lines produced so far by a process. */
3255
3455
  async getStderr(pid) {
3256
3456
  const raw = await this.http.requestJson(
3257
3457
  "GET",
@@ -3259,6 +3459,7 @@ var init_sandbox = __esm({
3259
3459
  );
3260
3460
  return fromSnakeKeys(raw);
3261
3461
  }
3462
+ /** Return all captured stdout+stderr lines produced so far by a process. */
3262
3463
  async getOutput(pid) {
3263
3464
  const raw = await this.http.requestJson(
3264
3465
  "GET",
@@ -3267,6 +3468,7 @@ var init_sandbox = __esm({
3267
3468
  return fromSnakeKeys(raw);
3268
3469
  }
3269
3470
  // --- Streaming (SSE) ---
3471
+ /** Stream stdout events from a process until it exits. Yields one `OutputEvent` per line. */
3270
3472
  async *followStdout(pid, options) {
3271
3473
  const stream = await this.http.requestStream(
3272
3474
  "GET",
@@ -3280,6 +3482,7 @@ var init_sandbox = __esm({
3280
3482
  yield fromSnakeKeys(raw);
3281
3483
  }
3282
3484
  }
3485
+ /** Stream stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3283
3486
  async *followStderr(pid, options) {
3284
3487
  const stream = await this.http.requestStream(
3285
3488
  "GET",
@@ -3293,6 +3496,7 @@ var init_sandbox = __esm({
3293
3496
  yield fromSnakeKeys(raw);
3294
3497
  }
3295
3498
  }
3499
+ /** Stream combined stdout+stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3296
3500
  async *followOutput(pid, options) {
3297
3501
  const stream = await this.http.requestStream(
3298
3502
  "GET",
@@ -3307,12 +3511,14 @@ var init_sandbox = __esm({
3307
3511
  }
3308
3512
  }
3309
3513
  // --- File operations ---
3514
+ /** Read a file from the sandbox and return its raw bytes. */
3310
3515
  async readFile(path2) {
3311
3516
  return this.http.requestBytes(
3312
3517
  "GET",
3313
3518
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3314
3519
  );
3315
3520
  }
3521
+ /** Write raw bytes to a file in the sandbox, creating it if it does not exist. */
3316
3522
  async writeFile(path2, content) {
3317
3523
  await this.http.requestBytes(
3318
3524
  "PUT",
@@ -3320,12 +3526,14 @@ var init_sandbox = __esm({
3320
3526
  { body: content, contentType: "application/octet-stream" }
3321
3527
  );
3322
3528
  }
3529
+ /** Delete a file from the sandbox. */
3323
3530
  async deleteFile(path2) {
3324
3531
  await this.http.requestJson(
3325
3532
  "DELETE",
3326
3533
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3327
3534
  );
3328
3535
  }
3536
+ /** List the contents of a directory in the sandbox. */
3329
3537
  async listDirectory(path2) {
3330
3538
  const raw = await this.http.requestJson(
3331
3539
  "GET",
@@ -3334,6 +3542,7 @@ var init_sandbox = __esm({
3334
3542
  return fromSnakeKeys(raw);
3335
3543
  }
3336
3544
  // --- PTY ---
3545
+ /** Create an interactive PTY session. Returns a `sessionId` and `token` for WebSocket connection via `connectPty()`. */
3337
3546
  async createPtySession(options) {
3338
3547
  const payload = {
3339
3548
  command: options.command,
@@ -3350,6 +3559,7 @@ var init_sandbox = __esm({
3350
3559
  );
3351
3560
  return fromSnakeKeys(raw);
3352
3561
  }
3562
+ /** Create a PTY session and connect to it immediately. Cleans up the session if the WebSocket connection fails. */
3353
3563
  async createPty(options) {
3354
3564
  const { onData, onExit, ...createOptions } = options;
3355
3565
  const session = await this.createPtySession(createOptions);
@@ -3363,6 +3573,7 @@ var init_sandbox = __esm({
3363
3573
  throw error;
3364
3574
  }
3365
3575
  }
3576
+ /** Attach to an existing PTY session by ID and token and return a connected `Pty` handle. */
3366
3577
  async connectPty(sessionId, token, options) {
3367
3578
  const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
3368
3579
  const authToken = wsUrl.searchParams.get("token") ?? token;
@@ -3387,6 +3598,7 @@ var init_sandbox = __esm({
3387
3598
  await pty.connect();
3388
3599
  return pty;
3389
3600
  }
3601
+ /** Open a TCP tunnel to a port inside the sandbox and return the local listener. */
3390
3602
  async createTunnel(remotePort, options) {
3391
3603
  return TcpTunnel.listen({
3392
3604
  baseUrl: this.baseUrl,
@@ -3397,6 +3609,7 @@ var init_sandbox = __esm({
3397
3609
  connectTimeout: options?.connectTimeout
3398
3610
  });
3399
3611
  }
3612
+ /** Connect to a sandbox VNC session for programmatic desktop control. */
3400
3613
  async connectDesktop(options) {
3401
3614
  return Desktop.connect({
3402
3615
  baseUrl: this.baseUrl,
@@ -3419,6 +3632,7 @@ var init_sandbox = __esm({
3419
3632
  return `${wsBase}/api/v1/pty/${sessionId}/ws?token=${token}`;
3420
3633
  }
3421
3634
  // --- Health ---
3635
+ /** Check the sandbox daemon health. */
3422
3636
  async health() {
3423
3637
  const raw = await this.http.requestJson(
3424
3638
  "GET",
@@ -3426,6 +3640,7 @@ var init_sandbox = __esm({
3426
3640
  );
3427
3641
  return fromSnakeKeys(raw);
3428
3642
  }
3643
+ /** Get sandbox daemon info (version, uptime, process counts). */
3429
3644
  async info() {
3430
3645
  const raw = await this.http.requestJson(
3431
3646
  "GET",
@@ -3438,6 +3653,10 @@ var init_sandbox = __esm({
3438
3653
  });
3439
3654
 
3440
3655
  // src/client.ts
3656
+ var client_exports = {};
3657
+ __export(client_exports, {
3658
+ SandboxClient: () => SandboxClient
3659
+ });
3441
3660
  function sleep2(ms) {
3442
3661
  return new Promise((resolve) => setTimeout(resolve, ms));
3443
3662
  }
@@ -3474,7 +3693,13 @@ var init_client = __esm({
3474
3693
  projectId;
3475
3694
  namespace;
3476
3695
  local;
3477
- constructor(options) {
3696
+ /** @internal Pass `true` to suppress the deprecation warning when used by `Sandbox.create()` / `Sandbox.connect()`. */
3697
+ constructor(options, _internal = false) {
3698
+ if (!_internal) {
3699
+ console.warn(
3700
+ "[tensorlake] SandboxClient is deprecated; use Sandbox.create() / Sandbox.connect() instead."
3701
+ );
3702
+ }
3478
3703
  this.apiUrl = options?.apiUrl ?? API_URL;
3479
3704
  this.apiKey = options?.apiKey ?? API_KEY;
3480
3705
  this.organizationId = options?.organizationId;
@@ -3514,12 +3739,13 @@ var init_client = __esm({
3514
3739
  return lifecyclePath(subpath, this.local, this.namespace);
3515
3740
  }
3516
3741
  // --- Sandbox CRUD ---
3742
+ /** Create a new sandbox. Returns immediately; the sandbox may still be starting. Use `createAndConnect()` for a blocking, ready-to-use handle. */
3517
3743
  async create(options) {
3518
3744
  const body = {
3519
3745
  resources: {
3520
3746
  cpus: options?.cpus ?? 1,
3521
3747
  memory_mb: options?.memoryMb ?? 1024,
3522
- ephemeral_disk_mb: options?.ephemeralDiskMb ?? 1024
3748
+ ...options?.diskMb != null ? { disk_mb: options.diskMb } : {}
3523
3749
  }
3524
3750
  };
3525
3751
  if (options?.image != null) body.image = options.image;
@@ -3540,8 +3766,10 @@ var init_client = __esm({
3540
3766
  this.path("sandboxes"),
3541
3767
  { body }
3542
3768
  );
3543
- return fromSnakeKeys(raw, "sandboxId");
3769
+ const result = fromSnakeKeys(raw, "sandboxId");
3770
+ return Object.assign(result, { traceId: raw.traceId });
3544
3771
  }
3772
+ /** Get current state and metadata for a sandbox by ID. */
3545
3773
  async get(sandboxId) {
3546
3774
  const raw = await this.http.requestJson(
3547
3775
  "GET",
@@ -3549,15 +3777,18 @@ var init_client = __esm({
3549
3777
  );
3550
3778
  return fromSnakeKeys(raw, "sandboxId");
3551
3779
  }
3780
+ /** List all sandboxes in the namespace. */
3552
3781
  async list() {
3553
3782
  const raw = await this.http.requestJson(
3554
3783
  "GET",
3555
3784
  this.path("sandboxes")
3556
3785
  );
3557
- return (raw.sandboxes ?? []).map(
3786
+ const sandboxes = (raw.sandboxes ?? []).map(
3558
3787
  (s) => fromSnakeKeys(s, "sandboxId")
3559
3788
  );
3789
+ return Object.assign(sandboxes, { traceId: raw.traceId });
3560
3790
  }
3791
+ /** Update sandbox properties such as name, exposed ports, and proxy auth settings. */
3561
3792
  async update(sandboxId, options) {
3562
3793
  const body = {};
3563
3794
  if (options.name != null) body.name = options.name;
@@ -3577,6 +3808,7 @@ var init_client = __esm({
3577
3808
  );
3578
3809
  return fromSnakeKeys(raw, "sandboxId");
3579
3810
  }
3811
+ /** Get the current proxy port settings for a sandbox. */
3580
3812
  async getPortAccess(sandboxId) {
3581
3813
  const info = await this.get(sandboxId);
3582
3814
  return {
@@ -3585,6 +3817,7 @@ var init_client = __esm({
3585
3817
  sandboxUrl: info.sandboxUrl
3586
3818
  };
3587
3819
  }
3820
+ /** Add one or more user ports to the sandbox proxy allowlist. */
3588
3821
  async exposePorts(sandboxId, ports, options) {
3589
3822
  const requestedPorts = normalizeUserPorts(ports);
3590
3823
  const current = await this.getPortAccess(sandboxId);
@@ -3597,6 +3830,7 @@ var init_client = __esm({
3597
3830
  exposedPorts: desiredPorts
3598
3831
  });
3599
3832
  }
3833
+ /** Remove one or more user ports from the sandbox proxy allowlist. */
3600
3834
  async unexposePorts(sandboxId, ports) {
3601
3835
  const requestedPorts = normalizeUserPorts(ports);
3602
3836
  const current = await this.getPortAccess(sandboxId);
@@ -3607,32 +3841,98 @@ var init_client = __esm({
3607
3841
  exposedPorts: desiredPorts
3608
3842
  });
3609
3843
  }
3844
+ /** Terminate and delete a sandbox. */
3610
3845
  async delete(sandboxId) {
3611
3846
  await this.http.requestJson(
3612
3847
  "DELETE",
3613
3848
  this.path(`sandboxes/${sandboxId}`)
3614
3849
  );
3615
3850
  }
3616
- async suspend(sandboxId) {
3851
+ /**
3852
+ * Suspend a named sandbox, preserving its state for later resume.
3853
+ *
3854
+ * Only sandboxes created with a `name` can be suspended; ephemeral sandboxes
3855
+ * cannot. By default blocks until the sandbox is fully `Suspended`. Pass
3856
+ * `{ wait: false }` to return immediately after the request is sent
3857
+ * (fire-and-return); the server processes the suspend asynchronously.
3858
+ *
3859
+ * @param sandboxId - ID or name of the sandbox.
3860
+ * @param options.wait - If `true` (default), poll until `Suspended`. Pass `false` to fire-and-return.
3861
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3862
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3863
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Suspended` within `timeout`.
3864
+ */
3865
+ async suspend(sandboxId, options) {
3617
3866
  await this.http.requestResponse(
3618
3867
  "POST",
3619
3868
  this.path(`sandboxes/${sandboxId}/suspend`)
3620
3869
  );
3870
+ if (options?.wait === false) return;
3871
+ const timeout = options?.timeout ?? 300;
3872
+ const pollInterval = options?.pollInterval ?? 1;
3873
+ const deadline = Date.now() + timeout * 1e3;
3874
+ while (Date.now() < deadline) {
3875
+ const info = await this.get(sandboxId);
3876
+ if (info.status === "suspended" /* SUSPENDED */) return;
3877
+ if (info.status === "terminated" /* TERMINATED */) {
3878
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for suspend`);
3879
+ }
3880
+ await sleep2(pollInterval * 1e3);
3881
+ }
3882
+ throw new SandboxError(`Sandbox ${sandboxId} did not suspend within ${timeout}s`);
3621
3883
  }
3622
- async resume(sandboxId) {
3884
+ /**
3885
+ * Resume a suspended sandbox and bring it back to `Running`.
3886
+ *
3887
+ * By default blocks until the sandbox is `Running` and routable. Pass
3888
+ * `{ wait: false }` to return immediately after the request is sent
3889
+ * (fire-and-return); the server processes the resume asynchronously.
3890
+ *
3891
+ * @param sandboxId - ID or name of the sandbox.
3892
+ * @param options.wait - If `true` (default), poll until `Running`. Pass `false` to fire-and-return.
3893
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3894
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3895
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Running` within `timeout`.
3896
+ */
3897
+ async resume(sandboxId, options) {
3623
3898
  await this.http.requestResponse(
3624
3899
  "POST",
3625
3900
  this.path(`sandboxes/${sandboxId}/resume`)
3626
3901
  );
3902
+ if (options?.wait === false) return;
3903
+ const timeout = options?.timeout ?? 300;
3904
+ const pollInterval = options?.pollInterval ?? 1;
3905
+ const deadline = Date.now() + timeout * 1e3;
3906
+ while (Date.now() < deadline) {
3907
+ const info = await this.get(sandboxId);
3908
+ if (info.status === "running" /* RUNNING */) return;
3909
+ if (info.status === "terminated" /* TERMINATED */) {
3910
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for resume`);
3911
+ }
3912
+ await sleep2(pollInterval * 1e3);
3913
+ }
3914
+ throw new SandboxError(`Sandbox ${sandboxId} did not resume within ${timeout}s`);
3627
3915
  }
3916
+ /** Claim a warm sandbox from a pool, creating one if no warm containers are available. */
3628
3917
  async claim(poolId) {
3629
3918
  const raw = await this.http.requestJson(
3630
3919
  "POST",
3631
3920
  this.path(`sandbox-pools/${poolId}/sandboxes`)
3632
3921
  );
3633
- return fromSnakeKeys(raw, "sandboxId");
3922
+ const result = fromSnakeKeys(raw, "sandboxId");
3923
+ return Object.assign(result, { traceId: raw.traceId });
3634
3924
  }
3635
3925
  // --- Snapshots ---
3926
+ /**
3927
+ * Request a snapshot of a running sandbox's filesystem.
3928
+ *
3929
+ * This call **returns immediately** with a `snapshotId` and `in_progress`
3930
+ * status — the snapshot is created asynchronously. Poll `getSnapshot()` until
3931
+ * `completed` or `failed`, or use `snapshotAndWait()` to block automatically.
3932
+ *
3933
+ * @param options.contentMode - `"filesystem_only"` for cold-boot snapshots (e.g. image builds).
3934
+ * Omit to use the server default (full VM snapshot).
3935
+ */
3636
3936
  async snapshot(sandboxId, options) {
3637
3937
  const requestOptions = options?.contentMode != null ? { body: { snapshot_content_mode: options.contentMode } } : void 0;
3638
3938
  const raw = await this.http.requestJson(
@@ -3642,6 +3942,7 @@ var init_client = __esm({
3642
3942
  );
3643
3943
  return fromSnakeKeys(raw, "snapshotId");
3644
3944
  }
3945
+ /** Get current status and metadata for a snapshot by ID. */
3645
3946
  async getSnapshot(snapshotId) {
3646
3947
  const raw = await this.http.requestJson(
3647
3948
  "GET",
@@ -3649,21 +3950,37 @@ var init_client = __esm({
3649
3950
  );
3650
3951
  return fromSnakeKeys(raw, "snapshotId");
3651
3952
  }
3953
+ /** List all snapshots in the namespace. */
3652
3954
  async listSnapshots() {
3653
3955
  const raw = await this.http.requestJson(
3654
3956
  "GET",
3655
3957
  this.path("snapshots")
3656
3958
  );
3657
- return (raw.snapshots ?? []).map(
3959
+ const snapshots = (raw.snapshots ?? []).map(
3658
3960
  (s) => fromSnakeKeys(s, "snapshotId")
3659
3961
  );
3962
+ return Object.assign(snapshots, { traceId: raw.traceId });
3660
3963
  }
3964
+ /** Delete a snapshot by ID. */
3661
3965
  async deleteSnapshot(snapshotId) {
3662
3966
  await this.http.requestJson(
3663
3967
  "DELETE",
3664
3968
  this.path(`snapshots/${snapshotId}`)
3665
3969
  );
3666
3970
  }
3971
+ /**
3972
+ * Create a snapshot and block until it is committed.
3973
+ *
3974
+ * Combines `snapshot()` with polling `getSnapshot()` until `completed`.
3975
+ * Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
3976
+ * without managing the client separately.
3977
+ *
3978
+ * @param sandboxId - ID of the running sandbox to snapshot.
3979
+ * @param options.timeout - Max seconds to wait (default 300).
3980
+ * @param options.pollInterval - Seconds between status polls (default 1).
3981
+ * @param options.contentMode - Content mode passed through to `snapshot()`.
3982
+ * @throws {SandboxError} If the snapshot fails or `timeout` elapses.
3983
+ */
3667
3984
  async snapshotAndWait(sandboxId, options) {
3668
3985
  const timeout = options?.timeout ?? 300;
3669
3986
  const pollInterval = options?.pollInterval ?? 1;
@@ -3686,6 +4003,7 @@ var init_client = __esm({
3686
4003
  );
3687
4004
  }
3688
4005
  // --- Pools ---
4006
+ /** Create a new sandbox pool with warm pre-booted containers. */
3689
4007
  async createPool(options) {
3690
4008
  const body = {
3691
4009
  image: options.image,
@@ -3707,6 +4025,7 @@ var init_client = __esm({
3707
4025
  );
3708
4026
  return fromSnakeKeys(raw, "poolId");
3709
4027
  }
4028
+ /** Get current state and metadata for a sandbox pool by ID. */
3710
4029
  async getPool(poolId) {
3711
4030
  const raw = await this.http.requestJson(
3712
4031
  "GET",
@@ -3714,15 +4033,18 @@ var init_client = __esm({
3714
4033
  );
3715
4034
  return fromSnakeKeys(raw, "poolId");
3716
4035
  }
4036
+ /** List all sandbox pools in the namespace. */
3717
4037
  async listPools() {
3718
4038
  const raw = await this.http.requestJson(
3719
4039
  "GET",
3720
4040
  this.path("sandbox-pools")
3721
4041
  );
3722
- return (raw.pools ?? []).map(
4042
+ const pools = (raw.pools ?? []).map(
3723
4043
  (p) => fromSnakeKeys(p, "poolId")
3724
4044
  );
4045
+ return Object.assign(pools, { traceId: raw.traceId });
3725
4046
  }
4047
+ /** Replace the configuration of an existing sandbox pool. */
3726
4048
  async updatePool(poolId, options) {
3727
4049
  const body = {
3728
4050
  image: options.image,
@@ -3744,6 +4066,7 @@ var init_client = __esm({
3744
4066
  );
3745
4067
  return fromSnakeKeys(raw, "poolId");
3746
4068
  }
4069
+ /** Delete a sandbox pool. Fails if the pool has active containers. */
3747
4070
  async deletePool(poolId) {
3748
4071
  await this.http.requestJson(
3749
4072
  "DELETE",
@@ -3751,6 +4074,7 @@ var init_client = __esm({
3751
4074
  );
3752
4075
  }
3753
4076
  // --- Connect ---
4077
+ /** Return a `Sandbox` handle for an existing running sandbox without verifying it exists. */
3754
4078
  connect(identifier, proxyUrl, routingHint) {
3755
4079
  const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
3756
4080
  return new Sandbox({
@@ -3762,26 +4086,35 @@ var init_client = __esm({
3762
4086
  routingHint
3763
4087
  });
3764
4088
  }
4089
+ /**
4090
+ * Create a sandbox, wait for it to reach `Running`, and return a connected handle.
4091
+ *
4092
+ * Blocks until the sandbox is ready or `startupTimeout` elapses. The returned
4093
+ * `Sandbox` auto-terminates when `terminate()` is called.
4094
+ *
4095
+ * @param options.startupTimeout - Max seconds to wait for `Running` status (default 60).
4096
+ * @throws {SandboxError} If the sandbox terminates during startup or the timeout elapses.
4097
+ */
3765
4098
  async createAndConnect(options) {
3766
4099
  const startupTimeout = options?.startupTimeout ?? 60;
3767
- let result;
3768
- if (options?.poolId != null) {
3769
- result = await this.claim(options.poolId);
3770
- } else {
3771
- result = await this.create(options);
3772
- }
3773
- if (result.status === "running" /* RUNNING */) {
3774
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, result.routingHint);
4100
+ const result = options?.poolId != null ? await this.claim(options.poolId) : await this.create(options);
4101
+ const requestedName = options?.poolId != null ? null : options?.name ?? null;
4102
+ const finishConnect = (routingHint, name) => {
4103
+ const sandbox = this.connect(result.sandboxId, options?.proxyUrl, routingHint);
3775
4104
  sandbox._setOwner(this);
4105
+ sandbox.traceId = result.traceId;
4106
+ sandbox._setLifecycleIdentifier(result.sandboxId);
4107
+ sandbox._setName(name ?? requestedName);
3776
4108
  return sandbox;
4109
+ };
4110
+ if (result.status === "running" /* RUNNING */) {
4111
+ return finishConnect(result.routingHint, result.name);
3777
4112
  }
3778
4113
  const deadline = Date.now() + startupTimeout * 1e3;
3779
4114
  while (Date.now() < deadline) {
3780
4115
  const info = await this.get(result.sandboxId);
3781
4116
  if (info.status === "running" /* RUNNING */) {
3782
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, info.routingHint);
3783
- sandbox._setOwner(this);
3784
- return sandbox;
4117
+ return finishConnect(info.routingHint, info.name);
3785
4118
  }
3786
4119
  if (info.status === "terminated" /* TERMINATED */) {
3787
4120
  throw new SandboxError(
@@ -4527,7 +4860,8 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4527
4860
  sandbox = await client.createAndConnect({
4528
4861
  ...plan.baseImage == null ? {} : { image: plan.baseImage },
4529
4862
  cpus: options.cpus ?? 2,
4530
- memoryMb: options.memoryMb ?? 4096
4863
+ memoryMb: options.memoryMb ?? 4096,
4864
+ ...options.diskMb != null ? { diskMb: options.diskMb } : {}
4531
4865
  });
4532
4866
  emit({
4533
4867
  type: "status",
@@ -4540,8 +4874,7 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4540
4874
  });
4541
4875
  emit({
4542
4876
  type: "snapshot_created",
4543
- snapshot_id: snapshot.snapshotId,
4544
- snapshot_uri: snapshot.snapshotUri ?? null
4877
+ snapshot_id: snapshot.snapshotId
4545
4878
  });
4546
4879
  if (!snapshot.snapshotUri) {
4547
4880
  throw new Error(
@@ -4585,27 +4918,33 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
4585
4918
  name: { type: "string", short: "n" },
4586
4919
  cpus: { type: "string" },
4587
4920
  memory: { type: "string" },
4921
+ disk: { type: "string" },
4588
4922
  public: { type: "boolean", default: false }
4589
4923
  }
4590
4924
  });
4591
4925
  const dockerfilePath = parsed.positionals[0];
4592
4926
  if (!dockerfilePath) {
4593
- throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--public]");
4927
+ throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk GB] [--public]");
4594
4928
  }
4595
4929
  const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
4596
4930
  const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
4931
+ const diskGb = parsed.values.disk != null ? Number(parsed.values.disk) : void 0;
4597
4932
  if (cpus != null && !Number.isFinite(cpus)) {
4598
4933
  throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
4599
4934
  }
4600
4935
  if (memoryMb != null && !Number.isInteger(memoryMb)) {
4601
4936
  throw new Error(`Invalid --memory value: ${parsed.values.memory}`);
4602
4937
  }
4938
+ if (diskGb != null && !Number.isInteger(diskGb)) {
4939
+ throw new Error(`Invalid --disk value: ${parsed.values.disk}`);
4940
+ }
4603
4941
  await createSandboxImage(
4604
4942
  dockerfilePath,
4605
4943
  {
4606
4944
  registeredName: parsed.values.name,
4607
4945
  cpus,
4608
4946
  memoryMb,
4947
+ diskMb: diskGb != null ? diskGb * 1024 : void 0,
4609
4948
  isPublic: parsed.values.public
4610
4949
  },
4611
4950
  { emit: ndjsonStdoutEmit }