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.
package/dist/index.cjs CHANGED
@@ -31,10 +31,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/defaults.ts
34
- var API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
34
+ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
35
35
  var init_defaults = __esm({
36
36
  "src/defaults.ts"() {
37
37
  "use strict";
38
+ SDK_VERSION = "0.4.49";
38
39
  API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
39
40
  API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
40
41
  NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
@@ -132,6 +133,12 @@ var init_errors = __esm({
132
133
  });
133
134
 
134
135
  // src/http.ts
136
+ function withTraceId(value, traceId) {
137
+ if (value == null) {
138
+ return { traceId };
139
+ }
140
+ return Object.assign(value, { traceId });
141
+ }
135
142
  function hasHeader(headers, name) {
136
143
  const lowered = name.toLowerCase();
137
144
  return Object.keys(headers).some((key) => key.toLowerCase() === lowered);
@@ -201,7 +208,7 @@ var init_http = __esm({
201
208
  allowH2: true
202
209
  })
203
210
  );
204
- HttpClient = class {
211
+ HttpClient = class _HttpClient {
205
212
  baseUrl;
206
213
  headers;
207
214
  maxRetries;
@@ -214,7 +221,9 @@ var init_http = __esm({
214
221
  this.maxRetries = options.maxRetries ?? MAX_RETRIES;
215
222
  this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
216
223
  this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
217
- this.headers = {};
224
+ this.headers = {
225
+ "User-Agent": `tensorlake-typescript-sdk/${SDK_VERSION}`
226
+ };
218
227
  if (options.apiKey) {
219
228
  this.headers["Authorization"] = `Bearer ${options.apiKey}`;
220
229
  }
@@ -243,8 +252,8 @@ var init_http = __esm({
243
252
  signal: options?.signal
244
253
  });
245
254
  const text = await response.text();
246
- if (!text) return void 0;
247
- return JSON.parse(text);
255
+ const data = text ? JSON.parse(text) : void 0;
256
+ return withTraceId(data, response.traceId);
248
257
  }
249
258
  /** Make a request returning raw bytes. */
250
259
  async requestBytes(method, path2, options) {
@@ -258,7 +267,7 @@ var init_http = __esm({
258
267
  signal: options?.signal
259
268
  });
260
269
  const buffer = await response.arrayBuffer();
261
- return new Uint8Array(buffer);
270
+ return withTraceId(new Uint8Array(buffer), response.traceId);
262
271
  }
263
272
  /** Make a request and return the response body as an SSE stream. */
264
273
  async requestStream(method, path2, options) {
@@ -273,7 +282,7 @@ var init_http = __esm({
273
282
  "No response body for SSE stream"
274
283
  );
275
284
  }
276
- return response.body;
285
+ return withTraceId(response.body, response.traceId);
277
286
  }
278
287
  /** Make a request and return the raw Response. */
279
288
  async requestResponse(method, path2, options) {
@@ -286,7 +295,7 @@ var init_http = __esm({
286
295
  headers["Content-Type"] = "application/json";
287
296
  }
288
297
  const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
289
- return this.doFetch(
298
+ const { response, traceId } = await this.doFetch(
290
299
  method,
291
300
  path2,
292
301
  body,
@@ -294,9 +303,18 @@ var init_http = __esm({
294
303
  options?.signal,
295
304
  options?.allowHttpErrors ?? false
296
305
  );
306
+ return withTraceId(response, traceId);
307
+ }
308
+ static makeTraceparent() {
309
+ const randomHex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map((b) => b.toString(16).padStart(2, "0")).join("");
310
+ const traceId = randomHex(16);
311
+ const spanId = randomHex(8);
312
+ return { traceparent: `00-${traceId}-${spanId}-01`, traceId };
297
313
  }
298
314
  async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
299
315
  const url = `${this.baseUrl}${path2}`;
316
+ const { traceparent, traceId } = _HttpClient.makeTraceparent();
317
+ headers["traceparent"] = traceparent;
300
318
  let lastError;
301
319
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
302
320
  if (attempt > 0) {
@@ -317,7 +335,7 @@ var init_http = __esm({
317
335
  signal: combinedSignal
318
336
  });
319
337
  clearTimeout(timeoutId);
320
- if (response.ok) return response;
338
+ if (response.ok) return { response, traceId };
321
339
  if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
322
340
  lastError = new RemoteAPIError(
323
341
  response.status,
@@ -326,7 +344,7 @@ var init_http = __esm({
326
344
  continue;
327
345
  }
328
346
  if (allowHttpErrors) {
329
- return response;
347
+ return { response, traceId };
330
348
  }
331
349
  const errorBody = await response.text().catch(() => "");
332
350
  throwMappedError(response.status, errorBody, path2);
@@ -396,20 +414,20 @@ var SandboxStatus, SnapshotStatus, ProcessStatus, StdinMode, OutputMode, Contain
396
414
  var init_models = __esm({
397
415
  "src/models.ts"() {
398
416
  "use strict";
399
- SandboxStatus = /* @__PURE__ */ ((SandboxStatus2) => {
400
- SandboxStatus2["PENDING"] = "pending";
401
- SandboxStatus2["RUNNING"] = "running";
402
- SandboxStatus2["SNAPSHOTTING"] = "snapshotting";
403
- SandboxStatus2["SUSPENDING"] = "suspending";
404
- SandboxStatus2["SUSPENDED"] = "suspended";
405
- SandboxStatus2["TERMINATED"] = "terminated";
406
- return SandboxStatus2;
417
+ SandboxStatus = /* @__PURE__ */ ((SandboxStatus3) => {
418
+ SandboxStatus3["PENDING"] = "pending";
419
+ SandboxStatus3["RUNNING"] = "running";
420
+ SandboxStatus3["SNAPSHOTTING"] = "snapshotting";
421
+ SandboxStatus3["SUSPENDING"] = "suspending";
422
+ SandboxStatus3["SUSPENDED"] = "suspended";
423
+ SandboxStatus3["TERMINATED"] = "terminated";
424
+ return SandboxStatus3;
407
425
  })(SandboxStatus || {});
408
- SnapshotStatus = /* @__PURE__ */ ((SnapshotStatus2) => {
409
- SnapshotStatus2["IN_PROGRESS"] = "in_progress";
410
- SnapshotStatus2["COMPLETED"] = "completed";
411
- SnapshotStatus2["FAILED"] = "failed";
412
- return SnapshotStatus2;
426
+ SnapshotStatus = /* @__PURE__ */ ((SnapshotStatus3) => {
427
+ SnapshotStatus3["IN_PROGRESS"] = "in_progress";
428
+ SnapshotStatus3["COMPLETED"] = "completed";
429
+ SnapshotStatus3["FAILED"] = "failed";
430
+ return SnapshotStatus3;
413
431
  })(SnapshotStatus || {});
414
432
  ProcessStatus = /* @__PURE__ */ ((ProcessStatus2) => {
415
433
  ProcessStatus2["RUNNING"] = "running";
@@ -3158,13 +3176,17 @@ var init_sandbox = __esm({
3158
3176
  };
3159
3177
  Sandbox = class {
3160
3178
  sandboxId;
3179
+ traceId = null;
3161
3180
  http;
3162
3181
  baseUrl;
3163
3182
  wsHeaders;
3164
3183
  ownsSandbox = false;
3165
3184
  lifecycleClient = null;
3185
+ lifecycleIdentifier;
3186
+ sandboxName = null;
3166
3187
  constructor(options) {
3167
3188
  this.sandboxId = options.sandboxId;
3189
+ this.lifecycleIdentifier = options.sandboxId;
3168
3190
  const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
3169
3191
  const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
3170
3192
  this.baseUrl = baseUrl;
@@ -3190,21 +3212,182 @@ var init_sandbox = __esm({
3190
3212
  routingHint: options.routingHint
3191
3213
  });
3192
3214
  }
3215
+ get name() {
3216
+ return this.sandboxName;
3217
+ }
3218
+ /** @internal Used by client wiring to keep locally cached name in sync. */
3219
+ _setName(name) {
3220
+ this.sandboxName = name;
3221
+ }
3222
+ /** @internal Used by lifecycle operations to pin to canonical sandbox ID. */
3223
+ _setLifecycleIdentifier(identifier) {
3224
+ this.lifecycleIdentifier = identifier;
3225
+ }
3193
3226
  /** @internal Used by SandboxClient.createAndConnect to set ownership. */
3194
3227
  _setOwner(client) {
3195
3228
  this.ownsSandbox = true;
3196
3229
  this.lifecycleClient = client;
3197
3230
  }
3231
+ // --- Static factory methods ---
3232
+ /**
3233
+ * Create a new sandbox and return a connected, running handle.
3234
+ *
3235
+ * Covers both fresh sandbox creation and restore-from-snapshot (set
3236
+ * `snapshotId`). Blocks until the sandbox is `Running`.
3237
+ */
3238
+ static async create(options) {
3239
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3240
+ const client = new SandboxClient2(
3241
+ options,
3242
+ /* _internal */
3243
+ true
3244
+ );
3245
+ const sandbox = await client.createAndConnect(options);
3246
+ sandbox.lifecycleClient = client;
3247
+ return sandbox;
3248
+ }
3249
+ /**
3250
+ * Attach to an existing sandbox and return a connected handle.
3251
+ *
3252
+ * Verifies the sandbox exists via a server GET call, then returns a handle
3253
+ * in whatever state the sandbox is in. Does **not** auto-resume a suspended
3254
+ * sandbox — call `sandbox.resume()` explicitly.
3255
+ */
3256
+ static async connect(options) {
3257
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3258
+ const client = new SandboxClient2(
3259
+ options,
3260
+ /* _internal */
3261
+ true
3262
+ );
3263
+ const info = await client.get(options.sandboxId);
3264
+ const sandbox = client.connect(
3265
+ info.sandboxId,
3266
+ options.proxyUrl,
3267
+ options.routingHint ?? info.routingHint
3268
+ );
3269
+ sandbox.lifecycleClient = client;
3270
+ sandbox._setLifecycleIdentifier(info.sandboxId);
3271
+ sandbox._setName(info.name ?? null);
3272
+ return sandbox;
3273
+ }
3274
+ // --- Static snapshot management ---
3275
+ /** Get information about a snapshot by ID. No sandbox handle needed. */
3276
+ static async getSnapshot(snapshotId, options) {
3277
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3278
+ const client = new SandboxClient2(
3279
+ options,
3280
+ /* _internal */
3281
+ true
3282
+ );
3283
+ return client.getSnapshot(snapshotId);
3284
+ }
3285
+ /** Delete a snapshot by ID. No sandbox handle needed. */
3286
+ static async deleteSnapshot(snapshotId, options) {
3287
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3288
+ const client = new SandboxClient2(
3289
+ options,
3290
+ /* _internal */
3291
+ true
3292
+ );
3293
+ await client.deleteSnapshot(snapshotId);
3294
+ }
3295
+ // --- Instance lifecycle methods ---
3296
+ requireLifecycleClient(operation) {
3297
+ if (!this.lifecycleClient) {
3298
+ throw new SandboxError(
3299
+ `Cannot ${operation}: no lifecycle client available. Use Sandbox.create() or Sandbox.connect() to get a lifecycle-aware handle.`
3300
+ );
3301
+ }
3302
+ return this.lifecycleClient;
3303
+ }
3304
+ /**
3305
+ * Fetch the current sandbox status from the server.
3306
+ *
3307
+ * Always hits the network — the value is not cached locally because the
3308
+ * status changes over the sandbox's lifecycle.
3309
+ */
3310
+ async status() {
3311
+ const client = this.requireLifecycleClient("read_status");
3312
+ const info = await client.get(this.lifecycleIdentifier);
3313
+ this._setLifecycleIdentifier(info.sandboxId);
3314
+ this._setName(info.name ?? null);
3315
+ return info.status;
3316
+ }
3317
+ /**
3318
+ * Update this sandbox's properties (name, exposed ports, proxy auth).
3319
+ *
3320
+ * Naming an ephemeral sandbox makes it non-ephemeral and enables
3321
+ * suspend/resume.
3322
+ */
3323
+ async update(options) {
3324
+ const client = this.requireLifecycleClient("update");
3325
+ const info = await client.update(this.lifecycleIdentifier, options);
3326
+ this._setLifecycleIdentifier(info.sandboxId);
3327
+ this._setName(info.name ?? null);
3328
+ return info;
3329
+ }
3330
+ /**
3331
+ * Suspend this sandbox.
3332
+ *
3333
+ * By default blocks until the sandbox is fully `Suspended`. Pass
3334
+ * `{ wait: false }` for fire-and-return.
3335
+ */
3336
+ async suspend(options) {
3337
+ const client = this.requireLifecycleClient("suspend");
3338
+ await client.suspend(this.lifecycleIdentifier, options);
3339
+ }
3340
+ /**
3341
+ * Resume this sandbox.
3342
+ *
3343
+ * By default blocks until the sandbox is `Running` and routable. Pass
3344
+ * `{ wait: false }` for fire-and-return.
3345
+ */
3346
+ async resume(options) {
3347
+ const client = this.requireLifecycleClient("resume");
3348
+ await client.resume(this.lifecycleIdentifier, options);
3349
+ }
3350
+ /**
3351
+ * Create a snapshot of this sandbox's filesystem and wait for it to
3352
+ * be committed.
3353
+ *
3354
+ * By default blocks until the snapshot artifact is ready and returns
3355
+ * the completed `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
3356
+ * (returns `undefined`).
3357
+ */
3358
+ async checkpoint(options) {
3359
+ const client = this.requireLifecycleClient("checkpoint");
3360
+ if (options?.wait === false) {
3361
+ await client.snapshot(this.lifecycleIdentifier, { contentMode: options.contentMode });
3362
+ return void 0;
3363
+ }
3364
+ return client.snapshotAndWait(this.lifecycleIdentifier, {
3365
+ timeout: options?.timeout,
3366
+ pollInterval: options?.pollInterval,
3367
+ contentMode: options?.contentMode
3368
+ });
3369
+ }
3370
+ /**
3371
+ * List snapshots taken from this sandbox.
3372
+ */
3373
+ async listSnapshots() {
3374
+ const client = this.requireLifecycleClient("listSnapshots");
3375
+ const all = await client.listSnapshots();
3376
+ const filtered = all.filter((s) => s.sandboxId === this.lifecycleIdentifier);
3377
+ return Object.assign(filtered, { traceId: all.traceId });
3378
+ }
3379
+ /** Close the HTTP client. The sandbox keeps running. */
3198
3380
  close() {
3199
3381
  this.http.close();
3200
3382
  }
3383
+ /** Terminate the sandbox and release all resources. */
3201
3384
  async terminate() {
3202
3385
  const client = this.lifecycleClient;
3203
3386
  this.ownsSandbox = false;
3204
3387
  this.lifecycleClient = null;
3205
3388
  this.close();
3206
3389
  if (client) {
3207
- await client.delete(this.sandboxId);
3390
+ await client.delete(this.lifecycleIdentifier);
3208
3391
  }
3209
3392
  }
3210
3393
  // --- High-level convenience ---
@@ -3250,6 +3433,14 @@ var init_sandbox = __esm({
3250
3433
  };
3251
3434
  }
3252
3435
  // --- Process management ---
3436
+ /**
3437
+ * Start a process in the sandbox without waiting for it to exit.
3438
+ *
3439
+ * Returns a `ProcessInfo` with the assigned `pid`. Use `getProcess()` to
3440
+ * poll status, or `followStdout()` / `followOutput()` to stream output
3441
+ * until the process exits. Use `run()` instead to block until completion
3442
+ * and get combined output in one call.
3443
+ */
3253
3444
  async startProcess(command, options) {
3254
3445
  const payload = { command };
3255
3446
  if (options?.args != null) payload.args = options.args;
@@ -3271,13 +3462,16 @@ var init_sandbox = __esm({
3271
3462
  );
3272
3463
  return fromSnakeKeys(raw);
3273
3464
  }
3465
+ /** List all processes (running and exited) tracked by the sandbox daemon. */
3274
3466
  async listProcesses() {
3275
3467
  const raw = await this.http.requestJson(
3276
3468
  "GET",
3277
3469
  "/api/v1/processes"
3278
3470
  );
3279
- return (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3471
+ const processes = (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3472
+ return Object.assign(processes, { traceId: raw.traceId });
3280
3473
  }
3474
+ /** Get current status and metadata for a process by PID. */
3281
3475
  async getProcess(pid) {
3282
3476
  const raw = await this.http.requestJson(
3283
3477
  "GET",
@@ -3285,9 +3479,11 @@ var init_sandbox = __esm({
3285
3479
  );
3286
3480
  return fromSnakeKeys(raw);
3287
3481
  }
3482
+ /** Send SIGKILL to a process. */
3288
3483
  async killProcess(pid) {
3289
3484
  await this.http.requestJson("DELETE", `/api/v1/processes/${pid}`);
3290
3485
  }
3486
+ /** Send an arbitrary signal to a process (e.g. `15` for SIGTERM, `9` for SIGKILL). */
3291
3487
  async sendSignal(pid, signal) {
3292
3488
  const raw = await this.http.requestJson(
3293
3489
  "POST",
@@ -3297,15 +3493,18 @@ var init_sandbox = __esm({
3297
3493
  return fromSnakeKeys(raw);
3298
3494
  }
3299
3495
  // --- Process I/O ---
3496
+ /** Write bytes to a process's stdin. The process must have been started with `stdinMode: StdinMode.PIPE`. */
3300
3497
  async writeStdin(pid, data) {
3301
3498
  await this.http.requestBytes("POST", `/api/v1/processes/${pid}/stdin`, {
3302
3499
  body: data,
3303
3500
  contentType: "application/octet-stream"
3304
3501
  });
3305
3502
  }
3503
+ /** Close a process's stdin pipe, signalling EOF to the process. */
3306
3504
  async closeStdin(pid) {
3307
3505
  await this.http.requestJson("POST", `/api/v1/processes/${pid}/stdin/close`);
3308
3506
  }
3507
+ /** Return all captured stdout lines produced so far by a process. */
3309
3508
  async getStdout(pid) {
3310
3509
  const raw = await this.http.requestJson(
3311
3510
  "GET",
@@ -3313,6 +3512,7 @@ var init_sandbox = __esm({
3313
3512
  );
3314
3513
  return fromSnakeKeys(raw);
3315
3514
  }
3515
+ /** Return all captured stderr lines produced so far by a process. */
3316
3516
  async getStderr(pid) {
3317
3517
  const raw = await this.http.requestJson(
3318
3518
  "GET",
@@ -3320,6 +3520,7 @@ var init_sandbox = __esm({
3320
3520
  );
3321
3521
  return fromSnakeKeys(raw);
3322
3522
  }
3523
+ /** Return all captured stdout+stderr lines produced so far by a process. */
3323
3524
  async getOutput(pid) {
3324
3525
  const raw = await this.http.requestJson(
3325
3526
  "GET",
@@ -3328,6 +3529,7 @@ var init_sandbox = __esm({
3328
3529
  return fromSnakeKeys(raw);
3329
3530
  }
3330
3531
  // --- Streaming (SSE) ---
3532
+ /** Stream stdout events from a process until it exits. Yields one `OutputEvent` per line. */
3331
3533
  async *followStdout(pid, options) {
3332
3534
  const stream = await this.http.requestStream(
3333
3535
  "GET",
@@ -3341,6 +3543,7 @@ var init_sandbox = __esm({
3341
3543
  yield fromSnakeKeys(raw);
3342
3544
  }
3343
3545
  }
3546
+ /** Stream stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3344
3547
  async *followStderr(pid, options) {
3345
3548
  const stream = await this.http.requestStream(
3346
3549
  "GET",
@@ -3354,6 +3557,7 @@ var init_sandbox = __esm({
3354
3557
  yield fromSnakeKeys(raw);
3355
3558
  }
3356
3559
  }
3560
+ /** Stream combined stdout+stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3357
3561
  async *followOutput(pid, options) {
3358
3562
  const stream = await this.http.requestStream(
3359
3563
  "GET",
@@ -3368,12 +3572,14 @@ var init_sandbox = __esm({
3368
3572
  }
3369
3573
  }
3370
3574
  // --- File operations ---
3575
+ /** Read a file from the sandbox and return its raw bytes. */
3371
3576
  async readFile(path2) {
3372
3577
  return this.http.requestBytes(
3373
3578
  "GET",
3374
3579
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3375
3580
  );
3376
3581
  }
3582
+ /** Write raw bytes to a file in the sandbox, creating it if it does not exist. */
3377
3583
  async writeFile(path2, content) {
3378
3584
  await this.http.requestBytes(
3379
3585
  "PUT",
@@ -3381,12 +3587,14 @@ var init_sandbox = __esm({
3381
3587
  { body: content, contentType: "application/octet-stream" }
3382
3588
  );
3383
3589
  }
3590
+ /** Delete a file from the sandbox. */
3384
3591
  async deleteFile(path2) {
3385
3592
  await this.http.requestJson(
3386
3593
  "DELETE",
3387
3594
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3388
3595
  );
3389
3596
  }
3597
+ /** List the contents of a directory in the sandbox. */
3390
3598
  async listDirectory(path2) {
3391
3599
  const raw = await this.http.requestJson(
3392
3600
  "GET",
@@ -3395,6 +3603,7 @@ var init_sandbox = __esm({
3395
3603
  return fromSnakeKeys(raw);
3396
3604
  }
3397
3605
  // --- PTY ---
3606
+ /** Create an interactive PTY session. Returns a `sessionId` and `token` for WebSocket connection via `connectPty()`. */
3398
3607
  async createPtySession(options) {
3399
3608
  const payload = {
3400
3609
  command: options.command,
@@ -3411,6 +3620,7 @@ var init_sandbox = __esm({
3411
3620
  );
3412
3621
  return fromSnakeKeys(raw);
3413
3622
  }
3623
+ /** Create a PTY session and connect to it immediately. Cleans up the session if the WebSocket connection fails. */
3414
3624
  async createPty(options) {
3415
3625
  const { onData, onExit, ...createOptions } = options;
3416
3626
  const session = await this.createPtySession(createOptions);
@@ -3424,6 +3634,7 @@ var init_sandbox = __esm({
3424
3634
  throw error;
3425
3635
  }
3426
3636
  }
3637
+ /** Attach to an existing PTY session by ID and token and return a connected `Pty` handle. */
3427
3638
  async connectPty(sessionId, token, options) {
3428
3639
  const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
3429
3640
  const authToken = wsUrl.searchParams.get("token") ?? token;
@@ -3448,6 +3659,7 @@ var init_sandbox = __esm({
3448
3659
  await pty.connect();
3449
3660
  return pty;
3450
3661
  }
3662
+ /** Open a TCP tunnel to a port inside the sandbox and return the local listener. */
3451
3663
  async createTunnel(remotePort, options) {
3452
3664
  return TcpTunnel.listen({
3453
3665
  baseUrl: this.baseUrl,
@@ -3458,6 +3670,7 @@ var init_sandbox = __esm({
3458
3670
  connectTimeout: options?.connectTimeout
3459
3671
  });
3460
3672
  }
3673
+ /** Connect to a sandbox VNC session for programmatic desktop control. */
3461
3674
  async connectDesktop(options) {
3462
3675
  return Desktop.connect({
3463
3676
  baseUrl: this.baseUrl,
@@ -3480,6 +3693,7 @@ var init_sandbox = __esm({
3480
3693
  return `${wsBase}/api/v1/pty/${sessionId}/ws?token=${token}`;
3481
3694
  }
3482
3695
  // --- Health ---
3696
+ /** Check the sandbox daemon health. */
3483
3697
  async health() {
3484
3698
  const raw = await this.http.requestJson(
3485
3699
  "GET",
@@ -3487,6 +3701,7 @@ var init_sandbox = __esm({
3487
3701
  );
3488
3702
  return fromSnakeKeys(raw);
3489
3703
  }
3704
+ /** Get sandbox daemon info (version, uptime, process counts). */
3490
3705
  async info() {
3491
3706
  const raw = await this.http.requestJson(
3492
3707
  "GET",
@@ -3499,6 +3714,10 @@ var init_sandbox = __esm({
3499
3714
  });
3500
3715
 
3501
3716
  // src/client.ts
3717
+ var client_exports = {};
3718
+ __export(client_exports, {
3719
+ SandboxClient: () => SandboxClient
3720
+ });
3502
3721
  function sleep2(ms) {
3503
3722
  return new Promise((resolve) => setTimeout(resolve, ms));
3504
3723
  }
@@ -3535,7 +3754,13 @@ var init_client = __esm({
3535
3754
  projectId;
3536
3755
  namespace;
3537
3756
  local;
3538
- constructor(options) {
3757
+ /** @internal Pass `true` to suppress the deprecation warning when used by `Sandbox.create()` / `Sandbox.connect()`. */
3758
+ constructor(options, _internal = false) {
3759
+ if (!_internal) {
3760
+ console.warn(
3761
+ "[tensorlake] SandboxClient is deprecated; use Sandbox.create() / Sandbox.connect() instead."
3762
+ );
3763
+ }
3539
3764
  this.apiUrl = options?.apiUrl ?? API_URL;
3540
3765
  this.apiKey = options?.apiKey ?? API_KEY;
3541
3766
  this.organizationId = options?.organizationId;
@@ -3575,12 +3800,13 @@ var init_client = __esm({
3575
3800
  return lifecyclePath(subpath, this.local, this.namespace);
3576
3801
  }
3577
3802
  // --- Sandbox CRUD ---
3803
+ /** Create a new sandbox. Returns immediately; the sandbox may still be starting. Use `createAndConnect()` for a blocking, ready-to-use handle. */
3578
3804
  async create(options) {
3579
3805
  const body = {
3580
3806
  resources: {
3581
3807
  cpus: options?.cpus ?? 1,
3582
3808
  memory_mb: options?.memoryMb ?? 1024,
3583
- ephemeral_disk_mb: options?.ephemeralDiskMb ?? 1024
3809
+ ...options?.diskMb != null ? { disk_mb: options.diskMb } : {}
3584
3810
  }
3585
3811
  };
3586
3812
  if (options?.image != null) body.image = options.image;
@@ -3601,8 +3827,10 @@ var init_client = __esm({
3601
3827
  this.path("sandboxes"),
3602
3828
  { body }
3603
3829
  );
3604
- return fromSnakeKeys(raw, "sandboxId");
3830
+ const result = fromSnakeKeys(raw, "sandboxId");
3831
+ return Object.assign(result, { traceId: raw.traceId });
3605
3832
  }
3833
+ /** Get current state and metadata for a sandbox by ID. */
3606
3834
  async get(sandboxId) {
3607
3835
  const raw = await this.http.requestJson(
3608
3836
  "GET",
@@ -3610,15 +3838,18 @@ var init_client = __esm({
3610
3838
  );
3611
3839
  return fromSnakeKeys(raw, "sandboxId");
3612
3840
  }
3841
+ /** List all sandboxes in the namespace. */
3613
3842
  async list() {
3614
3843
  const raw = await this.http.requestJson(
3615
3844
  "GET",
3616
3845
  this.path("sandboxes")
3617
3846
  );
3618
- return (raw.sandboxes ?? []).map(
3847
+ const sandboxes = (raw.sandboxes ?? []).map(
3619
3848
  (s) => fromSnakeKeys(s, "sandboxId")
3620
3849
  );
3850
+ return Object.assign(sandboxes, { traceId: raw.traceId });
3621
3851
  }
3852
+ /** Update sandbox properties such as name, exposed ports, and proxy auth settings. */
3622
3853
  async update(sandboxId, options) {
3623
3854
  const body = {};
3624
3855
  if (options.name != null) body.name = options.name;
@@ -3638,6 +3869,7 @@ var init_client = __esm({
3638
3869
  );
3639
3870
  return fromSnakeKeys(raw, "sandboxId");
3640
3871
  }
3872
+ /** Get the current proxy port settings for a sandbox. */
3641
3873
  async getPortAccess(sandboxId) {
3642
3874
  const info = await this.get(sandboxId);
3643
3875
  return {
@@ -3646,6 +3878,7 @@ var init_client = __esm({
3646
3878
  sandboxUrl: info.sandboxUrl
3647
3879
  };
3648
3880
  }
3881
+ /** Add one or more user ports to the sandbox proxy allowlist. */
3649
3882
  async exposePorts(sandboxId, ports, options) {
3650
3883
  const requestedPorts = normalizeUserPorts(ports);
3651
3884
  const current = await this.getPortAccess(sandboxId);
@@ -3658,6 +3891,7 @@ var init_client = __esm({
3658
3891
  exposedPorts: desiredPorts
3659
3892
  });
3660
3893
  }
3894
+ /** Remove one or more user ports from the sandbox proxy allowlist. */
3661
3895
  async unexposePorts(sandboxId, ports) {
3662
3896
  const requestedPorts = normalizeUserPorts(ports);
3663
3897
  const current = await this.getPortAccess(sandboxId);
@@ -3668,32 +3902,98 @@ var init_client = __esm({
3668
3902
  exposedPorts: desiredPorts
3669
3903
  });
3670
3904
  }
3905
+ /** Terminate and delete a sandbox. */
3671
3906
  async delete(sandboxId) {
3672
3907
  await this.http.requestJson(
3673
3908
  "DELETE",
3674
3909
  this.path(`sandboxes/${sandboxId}`)
3675
3910
  );
3676
3911
  }
3677
- async suspend(sandboxId) {
3912
+ /**
3913
+ * Suspend a named sandbox, preserving its state for later resume.
3914
+ *
3915
+ * Only sandboxes created with a `name` can be suspended; ephemeral sandboxes
3916
+ * cannot. By default blocks until the sandbox is fully `Suspended`. Pass
3917
+ * `{ wait: false }` to return immediately after the request is sent
3918
+ * (fire-and-return); the server processes the suspend asynchronously.
3919
+ *
3920
+ * @param sandboxId - ID or name of the sandbox.
3921
+ * @param options.wait - If `true` (default), poll until `Suspended`. Pass `false` to fire-and-return.
3922
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3923
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3924
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Suspended` within `timeout`.
3925
+ */
3926
+ async suspend(sandboxId, options) {
3678
3927
  await this.http.requestResponse(
3679
3928
  "POST",
3680
3929
  this.path(`sandboxes/${sandboxId}/suspend`)
3681
3930
  );
3931
+ if (options?.wait === false) return;
3932
+ const timeout = options?.timeout ?? 300;
3933
+ const pollInterval = options?.pollInterval ?? 1;
3934
+ const deadline = Date.now() + timeout * 1e3;
3935
+ while (Date.now() < deadline) {
3936
+ const info = await this.get(sandboxId);
3937
+ if (info.status === "suspended" /* SUSPENDED */) return;
3938
+ if (info.status === "terminated" /* TERMINATED */) {
3939
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for suspend`);
3940
+ }
3941
+ await sleep2(pollInterval * 1e3);
3942
+ }
3943
+ throw new SandboxError(`Sandbox ${sandboxId} did not suspend within ${timeout}s`);
3682
3944
  }
3683
- async resume(sandboxId) {
3945
+ /**
3946
+ * Resume a suspended sandbox and bring it back to `Running`.
3947
+ *
3948
+ * By default blocks until the sandbox is `Running` and routable. Pass
3949
+ * `{ wait: false }` to return immediately after the request is sent
3950
+ * (fire-and-return); the server processes the resume asynchronously.
3951
+ *
3952
+ * @param sandboxId - ID or name of the sandbox.
3953
+ * @param options.wait - If `true` (default), poll until `Running`. Pass `false` to fire-and-return.
3954
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3955
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3956
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Running` within `timeout`.
3957
+ */
3958
+ async resume(sandboxId, options) {
3684
3959
  await this.http.requestResponse(
3685
3960
  "POST",
3686
3961
  this.path(`sandboxes/${sandboxId}/resume`)
3687
3962
  );
3963
+ if (options?.wait === false) return;
3964
+ const timeout = options?.timeout ?? 300;
3965
+ const pollInterval = options?.pollInterval ?? 1;
3966
+ const deadline = Date.now() + timeout * 1e3;
3967
+ while (Date.now() < deadline) {
3968
+ const info = await this.get(sandboxId);
3969
+ if (info.status === "running" /* RUNNING */) return;
3970
+ if (info.status === "terminated" /* TERMINATED */) {
3971
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for resume`);
3972
+ }
3973
+ await sleep2(pollInterval * 1e3);
3974
+ }
3975
+ throw new SandboxError(`Sandbox ${sandboxId} did not resume within ${timeout}s`);
3688
3976
  }
3977
+ /** Claim a warm sandbox from a pool, creating one if no warm containers are available. */
3689
3978
  async claim(poolId) {
3690
3979
  const raw = await this.http.requestJson(
3691
3980
  "POST",
3692
3981
  this.path(`sandbox-pools/${poolId}/sandboxes`)
3693
3982
  );
3694
- return fromSnakeKeys(raw, "sandboxId");
3983
+ const result = fromSnakeKeys(raw, "sandboxId");
3984
+ return Object.assign(result, { traceId: raw.traceId });
3695
3985
  }
3696
3986
  // --- Snapshots ---
3987
+ /**
3988
+ * Request a snapshot of a running sandbox's filesystem.
3989
+ *
3990
+ * This call **returns immediately** with a `snapshotId` and `in_progress`
3991
+ * status — the snapshot is created asynchronously. Poll `getSnapshot()` until
3992
+ * `completed` or `failed`, or use `snapshotAndWait()` to block automatically.
3993
+ *
3994
+ * @param options.contentMode - `"filesystem_only"` for cold-boot snapshots (e.g. image builds).
3995
+ * Omit to use the server default (full VM snapshot).
3996
+ */
3697
3997
  async snapshot(sandboxId, options) {
3698
3998
  const requestOptions = options?.contentMode != null ? { body: { snapshot_content_mode: options.contentMode } } : void 0;
3699
3999
  const raw = await this.http.requestJson(
@@ -3703,6 +4003,7 @@ var init_client = __esm({
3703
4003
  );
3704
4004
  return fromSnakeKeys(raw, "snapshotId");
3705
4005
  }
4006
+ /** Get current status and metadata for a snapshot by ID. */
3706
4007
  async getSnapshot(snapshotId) {
3707
4008
  const raw = await this.http.requestJson(
3708
4009
  "GET",
@@ -3710,21 +4011,37 @@ var init_client = __esm({
3710
4011
  );
3711
4012
  return fromSnakeKeys(raw, "snapshotId");
3712
4013
  }
4014
+ /** List all snapshots in the namespace. */
3713
4015
  async listSnapshots() {
3714
4016
  const raw = await this.http.requestJson(
3715
4017
  "GET",
3716
4018
  this.path("snapshots")
3717
4019
  );
3718
- return (raw.snapshots ?? []).map(
4020
+ const snapshots = (raw.snapshots ?? []).map(
3719
4021
  (s) => fromSnakeKeys(s, "snapshotId")
3720
4022
  );
4023
+ return Object.assign(snapshots, { traceId: raw.traceId });
3721
4024
  }
4025
+ /** Delete a snapshot by ID. */
3722
4026
  async deleteSnapshot(snapshotId) {
3723
4027
  await this.http.requestJson(
3724
4028
  "DELETE",
3725
4029
  this.path(`snapshots/${snapshotId}`)
3726
4030
  );
3727
4031
  }
4032
+ /**
4033
+ * Create a snapshot and block until it is committed.
4034
+ *
4035
+ * Combines `snapshot()` with polling `getSnapshot()` until `completed`.
4036
+ * Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
4037
+ * without managing the client separately.
4038
+ *
4039
+ * @param sandboxId - ID of the running sandbox to snapshot.
4040
+ * @param options.timeout - Max seconds to wait (default 300).
4041
+ * @param options.pollInterval - Seconds between status polls (default 1).
4042
+ * @param options.contentMode - Content mode passed through to `snapshot()`.
4043
+ * @throws {SandboxError} If the snapshot fails or `timeout` elapses.
4044
+ */
3728
4045
  async snapshotAndWait(sandboxId, options) {
3729
4046
  const timeout = options?.timeout ?? 300;
3730
4047
  const pollInterval = options?.pollInterval ?? 1;
@@ -3747,6 +4064,7 @@ var init_client = __esm({
3747
4064
  );
3748
4065
  }
3749
4066
  // --- Pools ---
4067
+ /** Create a new sandbox pool with warm pre-booted containers. */
3750
4068
  async createPool(options) {
3751
4069
  const body = {
3752
4070
  image: options.image,
@@ -3768,6 +4086,7 @@ var init_client = __esm({
3768
4086
  );
3769
4087
  return fromSnakeKeys(raw, "poolId");
3770
4088
  }
4089
+ /** Get current state and metadata for a sandbox pool by ID. */
3771
4090
  async getPool(poolId) {
3772
4091
  const raw = await this.http.requestJson(
3773
4092
  "GET",
@@ -3775,15 +4094,18 @@ var init_client = __esm({
3775
4094
  );
3776
4095
  return fromSnakeKeys(raw, "poolId");
3777
4096
  }
4097
+ /** List all sandbox pools in the namespace. */
3778
4098
  async listPools() {
3779
4099
  const raw = await this.http.requestJson(
3780
4100
  "GET",
3781
4101
  this.path("sandbox-pools")
3782
4102
  );
3783
- return (raw.pools ?? []).map(
4103
+ const pools = (raw.pools ?? []).map(
3784
4104
  (p) => fromSnakeKeys(p, "poolId")
3785
4105
  );
4106
+ return Object.assign(pools, { traceId: raw.traceId });
3786
4107
  }
4108
+ /** Replace the configuration of an existing sandbox pool. */
3787
4109
  async updatePool(poolId, options) {
3788
4110
  const body = {
3789
4111
  image: options.image,
@@ -3805,6 +4127,7 @@ var init_client = __esm({
3805
4127
  );
3806
4128
  return fromSnakeKeys(raw, "poolId");
3807
4129
  }
4130
+ /** Delete a sandbox pool. Fails if the pool has active containers. */
3808
4131
  async deletePool(poolId) {
3809
4132
  await this.http.requestJson(
3810
4133
  "DELETE",
@@ -3812,6 +4135,7 @@ var init_client = __esm({
3812
4135
  );
3813
4136
  }
3814
4137
  // --- Connect ---
4138
+ /** Return a `Sandbox` handle for an existing running sandbox without verifying it exists. */
3815
4139
  connect(identifier, proxyUrl, routingHint) {
3816
4140
  const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
3817
4141
  return new Sandbox({
@@ -3823,26 +4147,35 @@ var init_client = __esm({
3823
4147
  routingHint
3824
4148
  });
3825
4149
  }
4150
+ /**
4151
+ * Create a sandbox, wait for it to reach `Running`, and return a connected handle.
4152
+ *
4153
+ * Blocks until the sandbox is ready or `startupTimeout` elapses. The returned
4154
+ * `Sandbox` auto-terminates when `terminate()` is called.
4155
+ *
4156
+ * @param options.startupTimeout - Max seconds to wait for `Running` status (default 60).
4157
+ * @throws {SandboxError} If the sandbox terminates during startup or the timeout elapses.
4158
+ */
3826
4159
  async createAndConnect(options) {
3827
4160
  const startupTimeout = options?.startupTimeout ?? 60;
3828
- let result;
3829
- if (options?.poolId != null) {
3830
- result = await this.claim(options.poolId);
3831
- } else {
3832
- result = await this.create(options);
3833
- }
3834
- if (result.status === "running" /* RUNNING */) {
3835
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, result.routingHint);
4161
+ const result = options?.poolId != null ? await this.claim(options.poolId) : await this.create(options);
4162
+ const requestedName = options?.poolId != null ? null : options?.name ?? null;
4163
+ const finishConnect = (routingHint, name) => {
4164
+ const sandbox = this.connect(result.sandboxId, options?.proxyUrl, routingHint);
3836
4165
  sandbox._setOwner(this);
4166
+ sandbox.traceId = result.traceId;
4167
+ sandbox._setLifecycleIdentifier(result.sandboxId);
4168
+ sandbox._setName(name ?? requestedName);
3837
4169
  return sandbox;
4170
+ };
4171
+ if (result.status === "running" /* RUNNING */) {
4172
+ return finishConnect(result.routingHint, result.name);
3838
4173
  }
3839
4174
  const deadline = Date.now() + startupTimeout * 1e3;
3840
4175
  while (Date.now() < deadline) {
3841
4176
  const info = await this.get(result.sandboxId);
3842
4177
  if (info.status === "running" /* RUNNING */) {
3843
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, info.routingHint);
3844
- sandbox._setOwner(this);
3845
- return sandbox;
4178
+ return finishConnect(info.routingHint, info.name);
3846
4179
  }
3847
4180
  if (info.status === "terminated" /* TERMINATED */) {
3848
4181
  throw new SandboxError(
@@ -4681,7 +5014,8 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4681
5014
  sandbox = await client.createAndConnect({
4682
5015
  ...plan.baseImage == null ? {} : { image: plan.baseImage },
4683
5016
  cpus: options.cpus ?? 2,
4684
- memoryMb: options.memoryMb ?? 4096
5017
+ memoryMb: options.memoryMb ?? 4096,
5018
+ ...options.diskMb != null ? { diskMb: options.diskMb } : {}
4685
5019
  });
4686
5020
  emit({
4687
5021
  type: "status",
@@ -4694,8 +5028,7 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4694
5028
  });
4695
5029
  emit({
4696
5030
  type: "snapshot_created",
4697
- snapshot_id: snapshot.snapshotId,
4698
- snapshot_uri: snapshot.snapshotUri ?? null
5031
+ snapshot_id: snapshot.snapshotId
4699
5032
  });
4700
5033
  if (!snapshot.snapshotUri) {
4701
5034
  throw new Error(
@@ -4739,27 +5072,33 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
4739
5072
  name: { type: "string", short: "n" },
4740
5073
  cpus: { type: "string" },
4741
5074
  memory: { type: "string" },
5075
+ disk: { type: "string" },
4742
5076
  public: { type: "boolean", default: false }
4743
5077
  }
4744
5078
  });
4745
5079
  const dockerfilePath = parsed.positionals[0];
4746
5080
  if (!dockerfilePath) {
4747
- throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--public]");
5081
+ throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk GB] [--public]");
4748
5082
  }
4749
5083
  const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
4750
5084
  const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
5085
+ const diskGb = parsed.values.disk != null ? Number(parsed.values.disk) : void 0;
4751
5086
  if (cpus != null && !Number.isFinite(cpus)) {
4752
5087
  throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
4753
5088
  }
4754
5089
  if (memoryMb != null && !Number.isInteger(memoryMb)) {
4755
5090
  throw new Error(`Invalid --memory value: ${parsed.values.memory}`);
4756
5091
  }
5092
+ if (diskGb != null && !Number.isInteger(diskGb)) {
5093
+ throw new Error(`Invalid --disk value: ${parsed.values.disk}`);
5094
+ }
4757
5095
  await createSandboxImage(
4758
5096
  dockerfilePath,
4759
5097
  {
4760
5098
  registeredName: parsed.values.name,
4761
5099
  cpus,
4762
5100
  memoryMb,
5101
+ diskMb: diskGb != null ? diskGb * 1024 : void 0,
4763
5102
  isPublic: parsed.values.public
4764
5103
  },
4765
5104
  { emit: ndjsonStdoutEmit }