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.js CHANGED
@@ -9,10 +9,11 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/defaults.ts
12
- var API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
12
+ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
13
13
  var init_defaults = __esm({
14
14
  "src/defaults.ts"() {
15
15
  "use strict";
16
+ SDK_VERSION = "0.4.49";
16
17
  API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
17
18
  API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
18
19
  NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
@@ -115,6 +116,12 @@ import {
115
116
  fetch as undiciFetch,
116
117
  setGlobalDispatcher
117
118
  } from "undici";
119
+ function withTraceId(value, traceId) {
120
+ if (value == null) {
121
+ return { traceId };
122
+ }
123
+ return Object.assign(value, { traceId });
124
+ }
118
125
  function hasHeader(headers, name) {
119
126
  const lowered = name.toLowerCase();
120
127
  return Object.keys(headers).some((key) => key.toLowerCase() === lowered);
@@ -183,7 +190,7 @@ var init_http = __esm({
183
190
  allowH2: true
184
191
  })
185
192
  );
186
- HttpClient = class {
193
+ HttpClient = class _HttpClient {
187
194
  baseUrl;
188
195
  headers;
189
196
  maxRetries;
@@ -196,7 +203,9 @@ var init_http = __esm({
196
203
  this.maxRetries = options.maxRetries ?? MAX_RETRIES;
197
204
  this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
198
205
  this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
199
- this.headers = {};
206
+ this.headers = {
207
+ "User-Agent": `tensorlake-typescript-sdk/${SDK_VERSION}`
208
+ };
200
209
  if (options.apiKey) {
201
210
  this.headers["Authorization"] = `Bearer ${options.apiKey}`;
202
211
  }
@@ -225,8 +234,8 @@ var init_http = __esm({
225
234
  signal: options?.signal
226
235
  });
227
236
  const text = await response.text();
228
- if (!text) return void 0;
229
- return JSON.parse(text);
237
+ const data = text ? JSON.parse(text) : void 0;
238
+ return withTraceId(data, response.traceId);
230
239
  }
231
240
  /** Make a request returning raw bytes. */
232
241
  async requestBytes(method, path2, options) {
@@ -240,7 +249,7 @@ var init_http = __esm({
240
249
  signal: options?.signal
241
250
  });
242
251
  const buffer = await response.arrayBuffer();
243
- return new Uint8Array(buffer);
252
+ return withTraceId(new Uint8Array(buffer), response.traceId);
244
253
  }
245
254
  /** Make a request and return the response body as an SSE stream. */
246
255
  async requestStream(method, path2, options) {
@@ -255,7 +264,7 @@ var init_http = __esm({
255
264
  "No response body for SSE stream"
256
265
  );
257
266
  }
258
- return response.body;
267
+ return withTraceId(response.body, response.traceId);
259
268
  }
260
269
  /** Make a request and return the raw Response. */
261
270
  async requestResponse(method, path2, options) {
@@ -268,7 +277,7 @@ var init_http = __esm({
268
277
  headers["Content-Type"] = "application/json";
269
278
  }
270
279
  const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
271
- return this.doFetch(
280
+ const { response, traceId } = await this.doFetch(
272
281
  method,
273
282
  path2,
274
283
  body,
@@ -276,9 +285,18 @@ var init_http = __esm({
276
285
  options?.signal,
277
286
  options?.allowHttpErrors ?? false
278
287
  );
288
+ return withTraceId(response, traceId);
289
+ }
290
+ static makeTraceparent() {
291
+ const randomHex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map((b) => b.toString(16).padStart(2, "0")).join("");
292
+ const traceId = randomHex(16);
293
+ const spanId = randomHex(8);
294
+ return { traceparent: `00-${traceId}-${spanId}-01`, traceId };
279
295
  }
280
296
  async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
281
297
  const url = `${this.baseUrl}${path2}`;
298
+ const { traceparent, traceId } = _HttpClient.makeTraceparent();
299
+ headers["traceparent"] = traceparent;
282
300
  let lastError;
283
301
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
284
302
  if (attempt > 0) {
@@ -299,7 +317,7 @@ var init_http = __esm({
299
317
  signal: combinedSignal
300
318
  });
301
319
  clearTimeout(timeoutId);
302
- if (response.ok) return response;
320
+ if (response.ok) return { response, traceId };
303
321
  if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
304
322
  lastError = new RemoteAPIError(
305
323
  response.status,
@@ -308,7 +326,7 @@ var init_http = __esm({
308
326
  continue;
309
327
  }
310
328
  if (allowHttpErrors) {
311
- return response;
329
+ return { response, traceId };
312
330
  }
313
331
  const errorBody = await response.text().catch(() => "");
314
332
  throwMappedError(response.status, errorBody, path2);
@@ -378,20 +396,20 @@ var SandboxStatus, SnapshotStatus, ProcessStatus, StdinMode, OutputMode, Contain
378
396
  var init_models = __esm({
379
397
  "src/models.ts"() {
380
398
  "use strict";
381
- SandboxStatus = /* @__PURE__ */ ((SandboxStatus2) => {
382
- SandboxStatus2["PENDING"] = "pending";
383
- SandboxStatus2["RUNNING"] = "running";
384
- SandboxStatus2["SNAPSHOTTING"] = "snapshotting";
385
- SandboxStatus2["SUSPENDING"] = "suspending";
386
- SandboxStatus2["SUSPENDED"] = "suspended";
387
- SandboxStatus2["TERMINATED"] = "terminated";
388
- return SandboxStatus2;
399
+ SandboxStatus = /* @__PURE__ */ ((SandboxStatus3) => {
400
+ SandboxStatus3["PENDING"] = "pending";
401
+ SandboxStatus3["RUNNING"] = "running";
402
+ SandboxStatus3["SNAPSHOTTING"] = "snapshotting";
403
+ SandboxStatus3["SUSPENDING"] = "suspending";
404
+ SandboxStatus3["SUSPENDED"] = "suspended";
405
+ SandboxStatus3["TERMINATED"] = "terminated";
406
+ return SandboxStatus3;
389
407
  })(SandboxStatus || {});
390
- SnapshotStatus = /* @__PURE__ */ ((SnapshotStatus2) => {
391
- SnapshotStatus2["IN_PROGRESS"] = "in_progress";
392
- SnapshotStatus2["COMPLETED"] = "completed";
393
- SnapshotStatus2["FAILED"] = "failed";
394
- return SnapshotStatus2;
408
+ SnapshotStatus = /* @__PURE__ */ ((SnapshotStatus3) => {
409
+ SnapshotStatus3["IN_PROGRESS"] = "in_progress";
410
+ SnapshotStatus3["COMPLETED"] = "completed";
411
+ SnapshotStatus3["FAILED"] = "failed";
412
+ return SnapshotStatus3;
395
413
  })(SnapshotStatus || {});
396
414
  ProcessStatus = /* @__PURE__ */ ((ProcessStatus2) => {
397
415
  ProcessStatus2["RUNNING"] = "running";
@@ -3140,13 +3158,17 @@ var init_sandbox = __esm({
3140
3158
  };
3141
3159
  Sandbox = class {
3142
3160
  sandboxId;
3161
+ traceId = null;
3143
3162
  http;
3144
3163
  baseUrl;
3145
3164
  wsHeaders;
3146
3165
  ownsSandbox = false;
3147
3166
  lifecycleClient = null;
3167
+ lifecycleIdentifier;
3168
+ sandboxName = null;
3148
3169
  constructor(options) {
3149
3170
  this.sandboxId = options.sandboxId;
3171
+ this.lifecycleIdentifier = options.sandboxId;
3150
3172
  const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
3151
3173
  const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
3152
3174
  this.baseUrl = baseUrl;
@@ -3172,21 +3194,182 @@ var init_sandbox = __esm({
3172
3194
  routingHint: options.routingHint
3173
3195
  });
3174
3196
  }
3197
+ get name() {
3198
+ return this.sandboxName;
3199
+ }
3200
+ /** @internal Used by client wiring to keep locally cached name in sync. */
3201
+ _setName(name) {
3202
+ this.sandboxName = name;
3203
+ }
3204
+ /** @internal Used by lifecycle operations to pin to canonical sandbox ID. */
3205
+ _setLifecycleIdentifier(identifier) {
3206
+ this.lifecycleIdentifier = identifier;
3207
+ }
3175
3208
  /** @internal Used by SandboxClient.createAndConnect to set ownership. */
3176
3209
  _setOwner(client) {
3177
3210
  this.ownsSandbox = true;
3178
3211
  this.lifecycleClient = client;
3179
3212
  }
3213
+ // --- Static factory methods ---
3214
+ /**
3215
+ * Create a new sandbox and return a connected, running handle.
3216
+ *
3217
+ * Covers both fresh sandbox creation and restore-from-snapshot (set
3218
+ * `snapshotId`). Blocks until the sandbox is `Running`.
3219
+ */
3220
+ static async create(options) {
3221
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3222
+ const client = new SandboxClient2(
3223
+ options,
3224
+ /* _internal */
3225
+ true
3226
+ );
3227
+ const sandbox = await client.createAndConnect(options);
3228
+ sandbox.lifecycleClient = client;
3229
+ return sandbox;
3230
+ }
3231
+ /**
3232
+ * Attach to an existing sandbox and return a connected handle.
3233
+ *
3234
+ * Verifies the sandbox exists via a server GET call, then returns a handle
3235
+ * in whatever state the sandbox is in. Does **not** auto-resume a suspended
3236
+ * sandbox — call `sandbox.resume()` explicitly.
3237
+ */
3238
+ static async connect(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 info = await client.get(options.sandboxId);
3246
+ const sandbox = client.connect(
3247
+ info.sandboxId,
3248
+ options.proxyUrl,
3249
+ options.routingHint ?? info.routingHint
3250
+ );
3251
+ sandbox.lifecycleClient = client;
3252
+ sandbox._setLifecycleIdentifier(info.sandboxId);
3253
+ sandbox._setName(info.name ?? null);
3254
+ return sandbox;
3255
+ }
3256
+ // --- Static snapshot management ---
3257
+ /** Get information about a snapshot by ID. No sandbox handle needed. */
3258
+ static async getSnapshot(snapshotId, options) {
3259
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3260
+ const client = new SandboxClient2(
3261
+ options,
3262
+ /* _internal */
3263
+ true
3264
+ );
3265
+ return client.getSnapshot(snapshotId);
3266
+ }
3267
+ /** Delete a snapshot by ID. No sandbox handle needed. */
3268
+ static async deleteSnapshot(snapshotId, options) {
3269
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3270
+ const client = new SandboxClient2(
3271
+ options,
3272
+ /* _internal */
3273
+ true
3274
+ );
3275
+ await client.deleteSnapshot(snapshotId);
3276
+ }
3277
+ // --- Instance lifecycle methods ---
3278
+ requireLifecycleClient(operation) {
3279
+ if (!this.lifecycleClient) {
3280
+ throw new SandboxError(
3281
+ `Cannot ${operation}: no lifecycle client available. Use Sandbox.create() or Sandbox.connect() to get a lifecycle-aware handle.`
3282
+ );
3283
+ }
3284
+ return this.lifecycleClient;
3285
+ }
3286
+ /**
3287
+ * Fetch the current sandbox status from the server.
3288
+ *
3289
+ * Always hits the network — the value is not cached locally because the
3290
+ * status changes over the sandbox's lifecycle.
3291
+ */
3292
+ async status() {
3293
+ const client = this.requireLifecycleClient("read_status");
3294
+ const info = await client.get(this.lifecycleIdentifier);
3295
+ this._setLifecycleIdentifier(info.sandboxId);
3296
+ this._setName(info.name ?? null);
3297
+ return info.status;
3298
+ }
3299
+ /**
3300
+ * Update this sandbox's properties (name, exposed ports, proxy auth).
3301
+ *
3302
+ * Naming an ephemeral sandbox makes it non-ephemeral and enables
3303
+ * suspend/resume.
3304
+ */
3305
+ async update(options) {
3306
+ const client = this.requireLifecycleClient("update");
3307
+ const info = await client.update(this.lifecycleIdentifier, options);
3308
+ this._setLifecycleIdentifier(info.sandboxId);
3309
+ this._setName(info.name ?? null);
3310
+ return info;
3311
+ }
3312
+ /**
3313
+ * Suspend this sandbox.
3314
+ *
3315
+ * By default blocks until the sandbox is fully `Suspended`. Pass
3316
+ * `{ wait: false }` for fire-and-return.
3317
+ */
3318
+ async suspend(options) {
3319
+ const client = this.requireLifecycleClient("suspend");
3320
+ await client.suspend(this.lifecycleIdentifier, options);
3321
+ }
3322
+ /**
3323
+ * Resume this sandbox.
3324
+ *
3325
+ * By default blocks until the sandbox is `Running` and routable. Pass
3326
+ * `{ wait: false }` for fire-and-return.
3327
+ */
3328
+ async resume(options) {
3329
+ const client = this.requireLifecycleClient("resume");
3330
+ await client.resume(this.lifecycleIdentifier, options);
3331
+ }
3332
+ /**
3333
+ * Create a snapshot of this sandbox's filesystem and wait for it to
3334
+ * be committed.
3335
+ *
3336
+ * By default blocks until the snapshot artifact is ready and returns
3337
+ * the completed `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
3338
+ * (returns `undefined`).
3339
+ */
3340
+ async checkpoint(options) {
3341
+ const client = this.requireLifecycleClient("checkpoint");
3342
+ if (options?.wait === false) {
3343
+ await client.snapshot(this.lifecycleIdentifier, { contentMode: options.contentMode });
3344
+ return void 0;
3345
+ }
3346
+ return client.snapshotAndWait(this.lifecycleIdentifier, {
3347
+ timeout: options?.timeout,
3348
+ pollInterval: options?.pollInterval,
3349
+ contentMode: options?.contentMode
3350
+ });
3351
+ }
3352
+ /**
3353
+ * List snapshots taken from this sandbox.
3354
+ */
3355
+ async listSnapshots() {
3356
+ const client = this.requireLifecycleClient("listSnapshots");
3357
+ const all = await client.listSnapshots();
3358
+ const filtered = all.filter((s) => s.sandboxId === this.lifecycleIdentifier);
3359
+ return Object.assign(filtered, { traceId: all.traceId });
3360
+ }
3361
+ /** Close the HTTP client. The sandbox keeps running. */
3180
3362
  close() {
3181
3363
  this.http.close();
3182
3364
  }
3365
+ /** Terminate the sandbox and release all resources. */
3183
3366
  async terminate() {
3184
3367
  const client = this.lifecycleClient;
3185
3368
  this.ownsSandbox = false;
3186
3369
  this.lifecycleClient = null;
3187
3370
  this.close();
3188
3371
  if (client) {
3189
- await client.delete(this.sandboxId);
3372
+ await client.delete(this.lifecycleIdentifier);
3190
3373
  }
3191
3374
  }
3192
3375
  // --- High-level convenience ---
@@ -3232,6 +3415,14 @@ var init_sandbox = __esm({
3232
3415
  };
3233
3416
  }
3234
3417
  // --- Process management ---
3418
+ /**
3419
+ * Start a process in the sandbox without waiting for it to exit.
3420
+ *
3421
+ * Returns a `ProcessInfo` with the assigned `pid`. Use `getProcess()` to
3422
+ * poll status, or `followStdout()` / `followOutput()` to stream output
3423
+ * until the process exits. Use `run()` instead to block until completion
3424
+ * and get combined output in one call.
3425
+ */
3235
3426
  async startProcess(command, options) {
3236
3427
  const payload = { command };
3237
3428
  if (options?.args != null) payload.args = options.args;
@@ -3253,13 +3444,16 @@ var init_sandbox = __esm({
3253
3444
  );
3254
3445
  return fromSnakeKeys(raw);
3255
3446
  }
3447
+ /** List all processes (running and exited) tracked by the sandbox daemon. */
3256
3448
  async listProcesses() {
3257
3449
  const raw = await this.http.requestJson(
3258
3450
  "GET",
3259
3451
  "/api/v1/processes"
3260
3452
  );
3261
- return (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3453
+ const processes = (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3454
+ return Object.assign(processes, { traceId: raw.traceId });
3262
3455
  }
3456
+ /** Get current status and metadata for a process by PID. */
3263
3457
  async getProcess(pid) {
3264
3458
  const raw = await this.http.requestJson(
3265
3459
  "GET",
@@ -3267,9 +3461,11 @@ var init_sandbox = __esm({
3267
3461
  );
3268
3462
  return fromSnakeKeys(raw);
3269
3463
  }
3464
+ /** Send SIGKILL to a process. */
3270
3465
  async killProcess(pid) {
3271
3466
  await this.http.requestJson("DELETE", `/api/v1/processes/${pid}`);
3272
3467
  }
3468
+ /** Send an arbitrary signal to a process (e.g. `15` for SIGTERM, `9` for SIGKILL). */
3273
3469
  async sendSignal(pid, signal) {
3274
3470
  const raw = await this.http.requestJson(
3275
3471
  "POST",
@@ -3279,15 +3475,18 @@ var init_sandbox = __esm({
3279
3475
  return fromSnakeKeys(raw);
3280
3476
  }
3281
3477
  // --- Process I/O ---
3478
+ /** Write bytes to a process's stdin. The process must have been started with `stdinMode: StdinMode.PIPE`. */
3282
3479
  async writeStdin(pid, data) {
3283
3480
  await this.http.requestBytes("POST", `/api/v1/processes/${pid}/stdin`, {
3284
3481
  body: data,
3285
3482
  contentType: "application/octet-stream"
3286
3483
  });
3287
3484
  }
3485
+ /** Close a process's stdin pipe, signalling EOF to the process. */
3288
3486
  async closeStdin(pid) {
3289
3487
  await this.http.requestJson("POST", `/api/v1/processes/${pid}/stdin/close`);
3290
3488
  }
3489
+ /** Return all captured stdout lines produced so far by a process. */
3291
3490
  async getStdout(pid) {
3292
3491
  const raw = await this.http.requestJson(
3293
3492
  "GET",
@@ -3295,6 +3494,7 @@ var init_sandbox = __esm({
3295
3494
  );
3296
3495
  return fromSnakeKeys(raw);
3297
3496
  }
3497
+ /** Return all captured stderr lines produced so far by a process. */
3298
3498
  async getStderr(pid) {
3299
3499
  const raw = await this.http.requestJson(
3300
3500
  "GET",
@@ -3302,6 +3502,7 @@ var init_sandbox = __esm({
3302
3502
  );
3303
3503
  return fromSnakeKeys(raw);
3304
3504
  }
3505
+ /** Return all captured stdout+stderr lines produced so far by a process. */
3305
3506
  async getOutput(pid) {
3306
3507
  const raw = await this.http.requestJson(
3307
3508
  "GET",
@@ -3310,6 +3511,7 @@ var init_sandbox = __esm({
3310
3511
  return fromSnakeKeys(raw);
3311
3512
  }
3312
3513
  // --- Streaming (SSE) ---
3514
+ /** Stream stdout events from a process until it exits. Yields one `OutputEvent` per line. */
3313
3515
  async *followStdout(pid, options) {
3314
3516
  const stream = await this.http.requestStream(
3315
3517
  "GET",
@@ -3323,6 +3525,7 @@ var init_sandbox = __esm({
3323
3525
  yield fromSnakeKeys(raw);
3324
3526
  }
3325
3527
  }
3528
+ /** Stream stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3326
3529
  async *followStderr(pid, options) {
3327
3530
  const stream = await this.http.requestStream(
3328
3531
  "GET",
@@ -3336,6 +3539,7 @@ var init_sandbox = __esm({
3336
3539
  yield fromSnakeKeys(raw);
3337
3540
  }
3338
3541
  }
3542
+ /** Stream combined stdout+stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3339
3543
  async *followOutput(pid, options) {
3340
3544
  const stream = await this.http.requestStream(
3341
3545
  "GET",
@@ -3350,12 +3554,14 @@ var init_sandbox = __esm({
3350
3554
  }
3351
3555
  }
3352
3556
  // --- File operations ---
3557
+ /** Read a file from the sandbox and return its raw bytes. */
3353
3558
  async readFile(path2) {
3354
3559
  return this.http.requestBytes(
3355
3560
  "GET",
3356
3561
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3357
3562
  );
3358
3563
  }
3564
+ /** Write raw bytes to a file in the sandbox, creating it if it does not exist. */
3359
3565
  async writeFile(path2, content) {
3360
3566
  await this.http.requestBytes(
3361
3567
  "PUT",
@@ -3363,12 +3569,14 @@ var init_sandbox = __esm({
3363
3569
  { body: content, contentType: "application/octet-stream" }
3364
3570
  );
3365
3571
  }
3572
+ /** Delete a file from the sandbox. */
3366
3573
  async deleteFile(path2) {
3367
3574
  await this.http.requestJson(
3368
3575
  "DELETE",
3369
3576
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3370
3577
  );
3371
3578
  }
3579
+ /** List the contents of a directory in the sandbox. */
3372
3580
  async listDirectory(path2) {
3373
3581
  const raw = await this.http.requestJson(
3374
3582
  "GET",
@@ -3377,6 +3585,7 @@ var init_sandbox = __esm({
3377
3585
  return fromSnakeKeys(raw);
3378
3586
  }
3379
3587
  // --- PTY ---
3588
+ /** Create an interactive PTY session. Returns a `sessionId` and `token` for WebSocket connection via `connectPty()`. */
3380
3589
  async createPtySession(options) {
3381
3590
  const payload = {
3382
3591
  command: options.command,
@@ -3393,6 +3602,7 @@ var init_sandbox = __esm({
3393
3602
  );
3394
3603
  return fromSnakeKeys(raw);
3395
3604
  }
3605
+ /** Create a PTY session and connect to it immediately. Cleans up the session if the WebSocket connection fails. */
3396
3606
  async createPty(options) {
3397
3607
  const { onData, onExit, ...createOptions } = options;
3398
3608
  const session = await this.createPtySession(createOptions);
@@ -3406,6 +3616,7 @@ var init_sandbox = __esm({
3406
3616
  throw error;
3407
3617
  }
3408
3618
  }
3619
+ /** Attach to an existing PTY session by ID and token and return a connected `Pty` handle. */
3409
3620
  async connectPty(sessionId, token, options) {
3410
3621
  const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
3411
3622
  const authToken = wsUrl.searchParams.get("token") ?? token;
@@ -3430,6 +3641,7 @@ var init_sandbox = __esm({
3430
3641
  await pty.connect();
3431
3642
  return pty;
3432
3643
  }
3644
+ /** Open a TCP tunnel to a port inside the sandbox and return the local listener. */
3433
3645
  async createTunnel(remotePort, options) {
3434
3646
  return TcpTunnel.listen({
3435
3647
  baseUrl: this.baseUrl,
@@ -3440,6 +3652,7 @@ var init_sandbox = __esm({
3440
3652
  connectTimeout: options?.connectTimeout
3441
3653
  });
3442
3654
  }
3655
+ /** Connect to a sandbox VNC session for programmatic desktop control. */
3443
3656
  async connectDesktop(options) {
3444
3657
  return Desktop.connect({
3445
3658
  baseUrl: this.baseUrl,
@@ -3462,6 +3675,7 @@ var init_sandbox = __esm({
3462
3675
  return `${wsBase}/api/v1/pty/${sessionId}/ws?token=${token}`;
3463
3676
  }
3464
3677
  // --- Health ---
3678
+ /** Check the sandbox daemon health. */
3465
3679
  async health() {
3466
3680
  const raw = await this.http.requestJson(
3467
3681
  "GET",
@@ -3469,6 +3683,7 @@ var init_sandbox = __esm({
3469
3683
  );
3470
3684
  return fromSnakeKeys(raw);
3471
3685
  }
3686
+ /** Get sandbox daemon info (version, uptime, process counts). */
3472
3687
  async info() {
3473
3688
  const raw = await this.http.requestJson(
3474
3689
  "GET",
@@ -3481,6 +3696,10 @@ var init_sandbox = __esm({
3481
3696
  });
3482
3697
 
3483
3698
  // src/client.ts
3699
+ var client_exports = {};
3700
+ __export(client_exports, {
3701
+ SandboxClient: () => SandboxClient
3702
+ });
3484
3703
  function sleep2(ms) {
3485
3704
  return new Promise((resolve) => setTimeout(resolve, ms));
3486
3705
  }
@@ -3517,7 +3736,13 @@ var init_client = __esm({
3517
3736
  projectId;
3518
3737
  namespace;
3519
3738
  local;
3520
- constructor(options) {
3739
+ /** @internal Pass `true` to suppress the deprecation warning when used by `Sandbox.create()` / `Sandbox.connect()`. */
3740
+ constructor(options, _internal = false) {
3741
+ if (!_internal) {
3742
+ console.warn(
3743
+ "[tensorlake] SandboxClient is deprecated; use Sandbox.create() / Sandbox.connect() instead."
3744
+ );
3745
+ }
3521
3746
  this.apiUrl = options?.apiUrl ?? API_URL;
3522
3747
  this.apiKey = options?.apiKey ?? API_KEY;
3523
3748
  this.organizationId = options?.organizationId;
@@ -3557,12 +3782,13 @@ var init_client = __esm({
3557
3782
  return lifecyclePath(subpath, this.local, this.namespace);
3558
3783
  }
3559
3784
  // --- Sandbox CRUD ---
3785
+ /** Create a new sandbox. Returns immediately; the sandbox may still be starting. Use `createAndConnect()` for a blocking, ready-to-use handle. */
3560
3786
  async create(options) {
3561
3787
  const body = {
3562
3788
  resources: {
3563
3789
  cpus: options?.cpus ?? 1,
3564
3790
  memory_mb: options?.memoryMb ?? 1024,
3565
- ephemeral_disk_mb: options?.ephemeralDiskMb ?? 1024
3791
+ ...options?.diskMb != null ? { disk_mb: options.diskMb } : {}
3566
3792
  }
3567
3793
  };
3568
3794
  if (options?.image != null) body.image = options.image;
@@ -3583,8 +3809,10 @@ var init_client = __esm({
3583
3809
  this.path("sandboxes"),
3584
3810
  { body }
3585
3811
  );
3586
- return fromSnakeKeys(raw, "sandboxId");
3812
+ const result = fromSnakeKeys(raw, "sandboxId");
3813
+ return Object.assign(result, { traceId: raw.traceId });
3587
3814
  }
3815
+ /** Get current state and metadata for a sandbox by ID. */
3588
3816
  async get(sandboxId) {
3589
3817
  const raw = await this.http.requestJson(
3590
3818
  "GET",
@@ -3592,15 +3820,18 @@ var init_client = __esm({
3592
3820
  );
3593
3821
  return fromSnakeKeys(raw, "sandboxId");
3594
3822
  }
3823
+ /** List all sandboxes in the namespace. */
3595
3824
  async list() {
3596
3825
  const raw = await this.http.requestJson(
3597
3826
  "GET",
3598
3827
  this.path("sandboxes")
3599
3828
  );
3600
- return (raw.sandboxes ?? []).map(
3829
+ const sandboxes = (raw.sandboxes ?? []).map(
3601
3830
  (s) => fromSnakeKeys(s, "sandboxId")
3602
3831
  );
3832
+ return Object.assign(sandboxes, { traceId: raw.traceId });
3603
3833
  }
3834
+ /** Update sandbox properties such as name, exposed ports, and proxy auth settings. */
3604
3835
  async update(sandboxId, options) {
3605
3836
  const body = {};
3606
3837
  if (options.name != null) body.name = options.name;
@@ -3620,6 +3851,7 @@ var init_client = __esm({
3620
3851
  );
3621
3852
  return fromSnakeKeys(raw, "sandboxId");
3622
3853
  }
3854
+ /** Get the current proxy port settings for a sandbox. */
3623
3855
  async getPortAccess(sandboxId) {
3624
3856
  const info = await this.get(sandboxId);
3625
3857
  return {
@@ -3628,6 +3860,7 @@ var init_client = __esm({
3628
3860
  sandboxUrl: info.sandboxUrl
3629
3861
  };
3630
3862
  }
3863
+ /** Add one or more user ports to the sandbox proxy allowlist. */
3631
3864
  async exposePorts(sandboxId, ports, options) {
3632
3865
  const requestedPorts = normalizeUserPorts(ports);
3633
3866
  const current = await this.getPortAccess(sandboxId);
@@ -3640,6 +3873,7 @@ var init_client = __esm({
3640
3873
  exposedPorts: desiredPorts
3641
3874
  });
3642
3875
  }
3876
+ /** Remove one or more user ports from the sandbox proxy allowlist. */
3643
3877
  async unexposePorts(sandboxId, ports) {
3644
3878
  const requestedPorts = normalizeUserPorts(ports);
3645
3879
  const current = await this.getPortAccess(sandboxId);
@@ -3650,32 +3884,98 @@ var init_client = __esm({
3650
3884
  exposedPorts: desiredPorts
3651
3885
  });
3652
3886
  }
3887
+ /** Terminate and delete a sandbox. */
3653
3888
  async delete(sandboxId) {
3654
3889
  await this.http.requestJson(
3655
3890
  "DELETE",
3656
3891
  this.path(`sandboxes/${sandboxId}`)
3657
3892
  );
3658
3893
  }
3659
- async suspend(sandboxId) {
3894
+ /**
3895
+ * Suspend a named sandbox, preserving its state for later resume.
3896
+ *
3897
+ * Only sandboxes created with a `name` can be suspended; ephemeral sandboxes
3898
+ * cannot. By default blocks until the sandbox is fully `Suspended`. Pass
3899
+ * `{ wait: false }` to return immediately after the request is sent
3900
+ * (fire-and-return); the server processes the suspend asynchronously.
3901
+ *
3902
+ * @param sandboxId - ID or name of the sandbox.
3903
+ * @param options.wait - If `true` (default), poll until `Suspended`. Pass `false` to fire-and-return.
3904
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3905
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3906
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Suspended` within `timeout`.
3907
+ */
3908
+ async suspend(sandboxId, options) {
3660
3909
  await this.http.requestResponse(
3661
3910
  "POST",
3662
3911
  this.path(`sandboxes/${sandboxId}/suspend`)
3663
3912
  );
3913
+ if (options?.wait === false) return;
3914
+ const timeout = options?.timeout ?? 300;
3915
+ const pollInterval = options?.pollInterval ?? 1;
3916
+ const deadline = Date.now() + timeout * 1e3;
3917
+ while (Date.now() < deadline) {
3918
+ const info = await this.get(sandboxId);
3919
+ if (info.status === "suspended" /* SUSPENDED */) return;
3920
+ if (info.status === "terminated" /* TERMINATED */) {
3921
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for suspend`);
3922
+ }
3923
+ await sleep2(pollInterval * 1e3);
3924
+ }
3925
+ throw new SandboxError(`Sandbox ${sandboxId} did not suspend within ${timeout}s`);
3664
3926
  }
3665
- async resume(sandboxId) {
3927
+ /**
3928
+ * Resume a suspended sandbox and bring it back to `Running`.
3929
+ *
3930
+ * By default blocks until the sandbox is `Running` and routable. Pass
3931
+ * `{ wait: false }` to return immediately after the request is sent
3932
+ * (fire-and-return); the server processes the resume asynchronously.
3933
+ *
3934
+ * @param sandboxId - ID or name of the sandbox.
3935
+ * @param options.wait - If `true` (default), poll until `Running`. Pass `false` to fire-and-return.
3936
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3937
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3938
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Running` within `timeout`.
3939
+ */
3940
+ async resume(sandboxId, options) {
3666
3941
  await this.http.requestResponse(
3667
3942
  "POST",
3668
3943
  this.path(`sandboxes/${sandboxId}/resume`)
3669
3944
  );
3945
+ if (options?.wait === false) return;
3946
+ const timeout = options?.timeout ?? 300;
3947
+ const pollInterval = options?.pollInterval ?? 1;
3948
+ const deadline = Date.now() + timeout * 1e3;
3949
+ while (Date.now() < deadline) {
3950
+ const info = await this.get(sandboxId);
3951
+ if (info.status === "running" /* RUNNING */) return;
3952
+ if (info.status === "terminated" /* TERMINATED */) {
3953
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for resume`);
3954
+ }
3955
+ await sleep2(pollInterval * 1e3);
3956
+ }
3957
+ throw new SandboxError(`Sandbox ${sandboxId} did not resume within ${timeout}s`);
3670
3958
  }
3959
+ /** Claim a warm sandbox from a pool, creating one if no warm containers are available. */
3671
3960
  async claim(poolId) {
3672
3961
  const raw = await this.http.requestJson(
3673
3962
  "POST",
3674
3963
  this.path(`sandbox-pools/${poolId}/sandboxes`)
3675
3964
  );
3676
- return fromSnakeKeys(raw, "sandboxId");
3965
+ const result = fromSnakeKeys(raw, "sandboxId");
3966
+ return Object.assign(result, { traceId: raw.traceId });
3677
3967
  }
3678
3968
  // --- Snapshots ---
3969
+ /**
3970
+ * Request a snapshot of a running sandbox's filesystem.
3971
+ *
3972
+ * This call **returns immediately** with a `snapshotId` and `in_progress`
3973
+ * status — the snapshot is created asynchronously. Poll `getSnapshot()` until
3974
+ * `completed` or `failed`, or use `snapshotAndWait()` to block automatically.
3975
+ *
3976
+ * @param options.contentMode - `"filesystem_only"` for cold-boot snapshots (e.g. image builds).
3977
+ * Omit to use the server default (full VM snapshot).
3978
+ */
3679
3979
  async snapshot(sandboxId, options) {
3680
3980
  const requestOptions = options?.contentMode != null ? { body: { snapshot_content_mode: options.contentMode } } : void 0;
3681
3981
  const raw = await this.http.requestJson(
@@ -3685,6 +3985,7 @@ var init_client = __esm({
3685
3985
  );
3686
3986
  return fromSnakeKeys(raw, "snapshotId");
3687
3987
  }
3988
+ /** Get current status and metadata for a snapshot by ID. */
3688
3989
  async getSnapshot(snapshotId) {
3689
3990
  const raw = await this.http.requestJson(
3690
3991
  "GET",
@@ -3692,21 +3993,37 @@ var init_client = __esm({
3692
3993
  );
3693
3994
  return fromSnakeKeys(raw, "snapshotId");
3694
3995
  }
3996
+ /** List all snapshots in the namespace. */
3695
3997
  async listSnapshots() {
3696
3998
  const raw = await this.http.requestJson(
3697
3999
  "GET",
3698
4000
  this.path("snapshots")
3699
4001
  );
3700
- return (raw.snapshots ?? []).map(
4002
+ const snapshots = (raw.snapshots ?? []).map(
3701
4003
  (s) => fromSnakeKeys(s, "snapshotId")
3702
4004
  );
4005
+ return Object.assign(snapshots, { traceId: raw.traceId });
3703
4006
  }
4007
+ /** Delete a snapshot by ID. */
3704
4008
  async deleteSnapshot(snapshotId) {
3705
4009
  await this.http.requestJson(
3706
4010
  "DELETE",
3707
4011
  this.path(`snapshots/${snapshotId}`)
3708
4012
  );
3709
4013
  }
4014
+ /**
4015
+ * Create a snapshot and block until it is committed.
4016
+ *
4017
+ * Combines `snapshot()` with polling `getSnapshot()` until `completed`.
4018
+ * Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
4019
+ * without managing the client separately.
4020
+ *
4021
+ * @param sandboxId - ID of the running sandbox to snapshot.
4022
+ * @param options.timeout - Max seconds to wait (default 300).
4023
+ * @param options.pollInterval - Seconds between status polls (default 1).
4024
+ * @param options.contentMode - Content mode passed through to `snapshot()`.
4025
+ * @throws {SandboxError} If the snapshot fails or `timeout` elapses.
4026
+ */
3710
4027
  async snapshotAndWait(sandboxId, options) {
3711
4028
  const timeout = options?.timeout ?? 300;
3712
4029
  const pollInterval = options?.pollInterval ?? 1;
@@ -3729,6 +4046,7 @@ var init_client = __esm({
3729
4046
  );
3730
4047
  }
3731
4048
  // --- Pools ---
4049
+ /** Create a new sandbox pool with warm pre-booted containers. */
3732
4050
  async createPool(options) {
3733
4051
  const body = {
3734
4052
  image: options.image,
@@ -3750,6 +4068,7 @@ var init_client = __esm({
3750
4068
  );
3751
4069
  return fromSnakeKeys(raw, "poolId");
3752
4070
  }
4071
+ /** Get current state and metadata for a sandbox pool by ID. */
3753
4072
  async getPool(poolId) {
3754
4073
  const raw = await this.http.requestJson(
3755
4074
  "GET",
@@ -3757,15 +4076,18 @@ var init_client = __esm({
3757
4076
  );
3758
4077
  return fromSnakeKeys(raw, "poolId");
3759
4078
  }
4079
+ /** List all sandbox pools in the namespace. */
3760
4080
  async listPools() {
3761
4081
  const raw = await this.http.requestJson(
3762
4082
  "GET",
3763
4083
  this.path("sandbox-pools")
3764
4084
  );
3765
- return (raw.pools ?? []).map(
4085
+ const pools = (raw.pools ?? []).map(
3766
4086
  (p) => fromSnakeKeys(p, "poolId")
3767
4087
  );
4088
+ return Object.assign(pools, { traceId: raw.traceId });
3768
4089
  }
4090
+ /** Replace the configuration of an existing sandbox pool. */
3769
4091
  async updatePool(poolId, options) {
3770
4092
  const body = {
3771
4093
  image: options.image,
@@ -3787,6 +4109,7 @@ var init_client = __esm({
3787
4109
  );
3788
4110
  return fromSnakeKeys(raw, "poolId");
3789
4111
  }
4112
+ /** Delete a sandbox pool. Fails if the pool has active containers. */
3790
4113
  async deletePool(poolId) {
3791
4114
  await this.http.requestJson(
3792
4115
  "DELETE",
@@ -3794,6 +4117,7 @@ var init_client = __esm({
3794
4117
  );
3795
4118
  }
3796
4119
  // --- Connect ---
4120
+ /** Return a `Sandbox` handle for an existing running sandbox without verifying it exists. */
3797
4121
  connect(identifier, proxyUrl, routingHint) {
3798
4122
  const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
3799
4123
  return new Sandbox({
@@ -3805,26 +4129,35 @@ var init_client = __esm({
3805
4129
  routingHint
3806
4130
  });
3807
4131
  }
4132
+ /**
4133
+ * Create a sandbox, wait for it to reach `Running`, and return a connected handle.
4134
+ *
4135
+ * Blocks until the sandbox is ready or `startupTimeout` elapses. The returned
4136
+ * `Sandbox` auto-terminates when `terminate()` is called.
4137
+ *
4138
+ * @param options.startupTimeout - Max seconds to wait for `Running` status (default 60).
4139
+ * @throws {SandboxError} If the sandbox terminates during startup or the timeout elapses.
4140
+ */
3808
4141
  async createAndConnect(options) {
3809
4142
  const startupTimeout = options?.startupTimeout ?? 60;
3810
- let result;
3811
- if (options?.poolId != null) {
3812
- result = await this.claim(options.poolId);
3813
- } else {
3814
- result = await this.create(options);
3815
- }
3816
- if (result.status === "running" /* RUNNING */) {
3817
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, result.routingHint);
4143
+ const result = options?.poolId != null ? await this.claim(options.poolId) : await this.create(options);
4144
+ const requestedName = options?.poolId != null ? null : options?.name ?? null;
4145
+ const finishConnect = (routingHint, name) => {
4146
+ const sandbox = this.connect(result.sandboxId, options?.proxyUrl, routingHint);
3818
4147
  sandbox._setOwner(this);
4148
+ sandbox.traceId = result.traceId;
4149
+ sandbox._setLifecycleIdentifier(result.sandboxId);
4150
+ sandbox._setName(name ?? requestedName);
3819
4151
  return sandbox;
4152
+ };
4153
+ if (result.status === "running" /* RUNNING */) {
4154
+ return finishConnect(result.routingHint, result.name);
3820
4155
  }
3821
4156
  const deadline = Date.now() + startupTimeout * 1e3;
3822
4157
  while (Date.now() < deadline) {
3823
4158
  const info = await this.get(result.sandboxId);
3824
4159
  if (info.status === "running" /* RUNNING */) {
3825
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, info.routingHint);
3826
- sandbox._setOwner(this);
3827
- return sandbox;
4160
+ return finishConnect(info.routingHint, info.name);
3828
4161
  }
3829
4162
  if (info.status === "terminated" /* TERMINATED */) {
3830
4163
  throw new SandboxError(
@@ -4666,7 +4999,8 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4666
4999
  sandbox = await client.createAndConnect({
4667
5000
  ...plan.baseImage == null ? {} : { image: plan.baseImage },
4668
5001
  cpus: options.cpus ?? 2,
4669
- memoryMb: options.memoryMb ?? 4096
5002
+ memoryMb: options.memoryMb ?? 4096,
5003
+ ...options.diskMb != null ? { diskMb: options.diskMb } : {}
4670
5004
  });
4671
5005
  emit({
4672
5006
  type: "status",
@@ -4679,8 +5013,7 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4679
5013
  });
4680
5014
  emit({
4681
5015
  type: "snapshot_created",
4682
- snapshot_id: snapshot.snapshotId,
4683
- snapshot_uri: snapshot.snapshotUri ?? null
5016
+ snapshot_id: snapshot.snapshotId
4684
5017
  });
4685
5018
  if (!snapshot.snapshotUri) {
4686
5019
  throw new Error(
@@ -4724,27 +5057,33 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
4724
5057
  name: { type: "string", short: "n" },
4725
5058
  cpus: { type: "string" },
4726
5059
  memory: { type: "string" },
5060
+ disk: { type: "string" },
4727
5061
  public: { type: "boolean", default: false }
4728
5062
  }
4729
5063
  });
4730
5064
  const dockerfilePath = parsed.positionals[0];
4731
5065
  if (!dockerfilePath) {
4732
- throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--public]");
5066
+ throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk GB] [--public]");
4733
5067
  }
4734
5068
  const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
4735
5069
  const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
5070
+ const diskGb = parsed.values.disk != null ? Number(parsed.values.disk) : void 0;
4736
5071
  if (cpus != null && !Number.isFinite(cpus)) {
4737
5072
  throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
4738
5073
  }
4739
5074
  if (memoryMb != null && !Number.isInteger(memoryMb)) {
4740
5075
  throw new Error(`Invalid --memory value: ${parsed.values.memory}`);
4741
5076
  }
5077
+ if (diskGb != null && !Number.isInteger(diskGb)) {
5078
+ throw new Error(`Invalid --disk value: ${parsed.values.disk}`);
5079
+ }
4742
5080
  await createSandboxImage(
4743
5081
  dockerfilePath,
4744
5082
  {
4745
5083
  registeredName: parsed.values.name,
4746
5084
  cpus,
4747
5085
  memoryMb,
5086
+ diskMb: diskGb != null ? diskGb * 1024 : void 0,
4748
5087
  isPublic: parsed.values.public
4749
5088
  },
4750
5089
  { emit: ndjsonStdoutEmit }