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.
@@ -1,7 +1,12 @@
1
+ var __defProp = Object.defineProperty;
1
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
2
3
  var __esm = (fn, res) => function __init() {
3
4
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
5
  };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
5
10
 
6
11
  // src/models.ts
7
12
  function snakeToCamel(str) {
@@ -52,10 +57,11 @@ var init_models = __esm({
52
57
  });
53
58
 
54
59
  // src/defaults.ts
55
- var API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
60
+ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
56
61
  var init_defaults = __esm({
57
62
  "src/defaults.ts"() {
58
63
  "use strict";
64
+ SDK_VERSION = "0.4.49";
59
65
  API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
60
66
  API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
61
67
  NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
@@ -134,6 +140,12 @@ import {
134
140
  fetch as undiciFetch,
135
141
  setGlobalDispatcher
136
142
  } from "undici";
143
+ function withTraceId(value, traceId) {
144
+ if (value == null) {
145
+ return { traceId };
146
+ }
147
+ return Object.assign(value, { traceId });
148
+ }
137
149
  function hasHeader(headers, name) {
138
150
  const lowered = name.toLowerCase();
139
151
  return Object.keys(headers).some((key) => key.toLowerCase() === lowered);
@@ -202,7 +214,7 @@ var init_http = __esm({
202
214
  allowH2: true
203
215
  })
204
216
  );
205
- HttpClient = class {
217
+ HttpClient = class _HttpClient {
206
218
  baseUrl;
207
219
  headers;
208
220
  maxRetries;
@@ -215,7 +227,9 @@ var init_http = __esm({
215
227
  this.maxRetries = options.maxRetries ?? MAX_RETRIES;
216
228
  this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
217
229
  this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
218
- this.headers = {};
230
+ this.headers = {
231
+ "User-Agent": `tensorlake-typescript-sdk/${SDK_VERSION}`
232
+ };
219
233
  if (options.apiKey) {
220
234
  this.headers["Authorization"] = `Bearer ${options.apiKey}`;
221
235
  }
@@ -244,8 +258,8 @@ var init_http = __esm({
244
258
  signal: options?.signal
245
259
  });
246
260
  const text = await response.text();
247
- if (!text) return void 0;
248
- return JSON.parse(text);
261
+ const data = text ? JSON.parse(text) : void 0;
262
+ return withTraceId(data, response.traceId);
249
263
  }
250
264
  /** Make a request returning raw bytes. */
251
265
  async requestBytes(method, path2, options) {
@@ -259,7 +273,7 @@ var init_http = __esm({
259
273
  signal: options?.signal
260
274
  });
261
275
  const buffer = await response.arrayBuffer();
262
- return new Uint8Array(buffer);
276
+ return withTraceId(new Uint8Array(buffer), response.traceId);
263
277
  }
264
278
  /** Make a request and return the response body as an SSE stream. */
265
279
  async requestStream(method, path2, options) {
@@ -274,7 +288,7 @@ var init_http = __esm({
274
288
  "No response body for SSE stream"
275
289
  );
276
290
  }
277
- return response.body;
291
+ return withTraceId(response.body, response.traceId);
278
292
  }
279
293
  /** Make a request and return the raw Response. */
280
294
  async requestResponse(method, path2, options) {
@@ -287,7 +301,7 @@ var init_http = __esm({
287
301
  headers["Content-Type"] = "application/json";
288
302
  }
289
303
  const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
290
- return this.doFetch(
304
+ const { response, traceId } = await this.doFetch(
291
305
  method,
292
306
  path2,
293
307
  body,
@@ -295,9 +309,18 @@ var init_http = __esm({
295
309
  options?.signal,
296
310
  options?.allowHttpErrors ?? false
297
311
  );
312
+ return withTraceId(response, traceId);
313
+ }
314
+ static makeTraceparent() {
315
+ const randomHex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map((b) => b.toString(16).padStart(2, "0")).join("");
316
+ const traceId = randomHex(16);
317
+ const spanId = randomHex(8);
318
+ return { traceparent: `00-${traceId}-${spanId}-01`, traceId };
298
319
  }
299
320
  async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
300
321
  const url = `${this.baseUrl}${path2}`;
322
+ const { traceparent, traceId } = _HttpClient.makeTraceparent();
323
+ headers["traceparent"] = traceparent;
301
324
  let lastError;
302
325
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
303
326
  if (attempt > 0) {
@@ -318,7 +341,7 @@ var init_http = __esm({
318
341
  signal: combinedSignal
319
342
  });
320
343
  clearTimeout(timeoutId);
321
- if (response.ok) return response;
344
+ if (response.ok) return { response, traceId };
322
345
  if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
323
346
  lastError = new RemoteAPIError(
324
347
  response.status,
@@ -327,7 +350,7 @@ var init_http = __esm({
327
350
  continue;
328
351
  }
329
352
  if (allowHttpErrors) {
330
- return response;
353
+ return { response, traceId };
331
354
  }
332
355
  const errorBody = await response.text().catch(() => "");
333
356
  throwMappedError(response.status, errorBody, path2);
@@ -3074,13 +3097,17 @@ var init_sandbox = __esm({
3074
3097
  };
3075
3098
  Sandbox = class {
3076
3099
  sandboxId;
3100
+ traceId = null;
3077
3101
  http;
3078
3102
  baseUrl;
3079
3103
  wsHeaders;
3080
3104
  ownsSandbox = false;
3081
3105
  lifecycleClient = null;
3106
+ lifecycleIdentifier;
3107
+ sandboxName = null;
3082
3108
  constructor(options) {
3083
3109
  this.sandboxId = options.sandboxId;
3110
+ this.lifecycleIdentifier = options.sandboxId;
3084
3111
  const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
3085
3112
  const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
3086
3113
  this.baseUrl = baseUrl;
@@ -3106,21 +3133,182 @@ var init_sandbox = __esm({
3106
3133
  routingHint: options.routingHint
3107
3134
  });
3108
3135
  }
3136
+ get name() {
3137
+ return this.sandboxName;
3138
+ }
3139
+ /** @internal Used by client wiring to keep locally cached name in sync. */
3140
+ _setName(name) {
3141
+ this.sandboxName = name;
3142
+ }
3143
+ /** @internal Used by lifecycle operations to pin to canonical sandbox ID. */
3144
+ _setLifecycleIdentifier(identifier) {
3145
+ this.lifecycleIdentifier = identifier;
3146
+ }
3109
3147
  /** @internal Used by SandboxClient.createAndConnect to set ownership. */
3110
3148
  _setOwner(client) {
3111
3149
  this.ownsSandbox = true;
3112
3150
  this.lifecycleClient = client;
3113
3151
  }
3152
+ // --- Static factory methods ---
3153
+ /**
3154
+ * Create a new sandbox and return a connected, running handle.
3155
+ *
3156
+ * Covers both fresh sandbox creation and restore-from-snapshot (set
3157
+ * `snapshotId`). Blocks until the sandbox is `Running`.
3158
+ */
3159
+ static async create(options) {
3160
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3161
+ const client = new SandboxClient2(
3162
+ options,
3163
+ /* _internal */
3164
+ true
3165
+ );
3166
+ const sandbox = await client.createAndConnect(options);
3167
+ sandbox.lifecycleClient = client;
3168
+ return sandbox;
3169
+ }
3170
+ /**
3171
+ * Attach to an existing sandbox and return a connected handle.
3172
+ *
3173
+ * Verifies the sandbox exists via a server GET call, then returns a handle
3174
+ * in whatever state the sandbox is in. Does **not** auto-resume a suspended
3175
+ * sandbox — call `sandbox.resume()` explicitly.
3176
+ */
3177
+ static async connect(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 info = await client.get(options.sandboxId);
3185
+ const sandbox = client.connect(
3186
+ info.sandboxId,
3187
+ options.proxyUrl,
3188
+ options.routingHint ?? info.routingHint
3189
+ );
3190
+ sandbox.lifecycleClient = client;
3191
+ sandbox._setLifecycleIdentifier(info.sandboxId);
3192
+ sandbox._setName(info.name ?? null);
3193
+ return sandbox;
3194
+ }
3195
+ // --- Static snapshot management ---
3196
+ /** Get information about a snapshot by ID. No sandbox handle needed. */
3197
+ static async getSnapshot(snapshotId, options) {
3198
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3199
+ const client = new SandboxClient2(
3200
+ options,
3201
+ /* _internal */
3202
+ true
3203
+ );
3204
+ return client.getSnapshot(snapshotId);
3205
+ }
3206
+ /** Delete a snapshot by ID. No sandbox handle needed. */
3207
+ static async deleteSnapshot(snapshotId, options) {
3208
+ const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3209
+ const client = new SandboxClient2(
3210
+ options,
3211
+ /* _internal */
3212
+ true
3213
+ );
3214
+ await client.deleteSnapshot(snapshotId);
3215
+ }
3216
+ // --- Instance lifecycle methods ---
3217
+ requireLifecycleClient(operation) {
3218
+ if (!this.lifecycleClient) {
3219
+ throw new SandboxError(
3220
+ `Cannot ${operation}: no lifecycle client available. Use Sandbox.create() or Sandbox.connect() to get a lifecycle-aware handle.`
3221
+ );
3222
+ }
3223
+ return this.lifecycleClient;
3224
+ }
3225
+ /**
3226
+ * Fetch the current sandbox status from the server.
3227
+ *
3228
+ * Always hits the network — the value is not cached locally because the
3229
+ * status changes over the sandbox's lifecycle.
3230
+ */
3231
+ async status() {
3232
+ const client = this.requireLifecycleClient("read_status");
3233
+ const info = await client.get(this.lifecycleIdentifier);
3234
+ this._setLifecycleIdentifier(info.sandboxId);
3235
+ this._setName(info.name ?? null);
3236
+ return info.status;
3237
+ }
3238
+ /**
3239
+ * Update this sandbox's properties (name, exposed ports, proxy auth).
3240
+ *
3241
+ * Naming an ephemeral sandbox makes it non-ephemeral and enables
3242
+ * suspend/resume.
3243
+ */
3244
+ async update(options) {
3245
+ const client = this.requireLifecycleClient("update");
3246
+ const info = await client.update(this.lifecycleIdentifier, options);
3247
+ this._setLifecycleIdentifier(info.sandboxId);
3248
+ this._setName(info.name ?? null);
3249
+ return info;
3250
+ }
3251
+ /**
3252
+ * Suspend this sandbox.
3253
+ *
3254
+ * By default blocks until the sandbox is fully `Suspended`. Pass
3255
+ * `{ wait: false }` for fire-and-return.
3256
+ */
3257
+ async suspend(options) {
3258
+ const client = this.requireLifecycleClient("suspend");
3259
+ await client.suspend(this.lifecycleIdentifier, options);
3260
+ }
3261
+ /**
3262
+ * Resume this sandbox.
3263
+ *
3264
+ * By default blocks until the sandbox is `Running` and routable. Pass
3265
+ * `{ wait: false }` for fire-and-return.
3266
+ */
3267
+ async resume(options) {
3268
+ const client = this.requireLifecycleClient("resume");
3269
+ await client.resume(this.lifecycleIdentifier, options);
3270
+ }
3271
+ /**
3272
+ * Create a snapshot of this sandbox's filesystem and wait for it to
3273
+ * be committed.
3274
+ *
3275
+ * By default blocks until the snapshot artifact is ready and returns
3276
+ * the completed `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
3277
+ * (returns `undefined`).
3278
+ */
3279
+ async checkpoint(options) {
3280
+ const client = this.requireLifecycleClient("checkpoint");
3281
+ if (options?.wait === false) {
3282
+ await client.snapshot(this.lifecycleIdentifier, { contentMode: options.contentMode });
3283
+ return void 0;
3284
+ }
3285
+ return client.snapshotAndWait(this.lifecycleIdentifier, {
3286
+ timeout: options?.timeout,
3287
+ pollInterval: options?.pollInterval,
3288
+ contentMode: options?.contentMode
3289
+ });
3290
+ }
3291
+ /**
3292
+ * List snapshots taken from this sandbox.
3293
+ */
3294
+ async listSnapshots() {
3295
+ const client = this.requireLifecycleClient("listSnapshots");
3296
+ const all = await client.listSnapshots();
3297
+ const filtered = all.filter((s) => s.sandboxId === this.lifecycleIdentifier);
3298
+ return Object.assign(filtered, { traceId: all.traceId });
3299
+ }
3300
+ /** Close the HTTP client. The sandbox keeps running. */
3114
3301
  close() {
3115
3302
  this.http.close();
3116
3303
  }
3304
+ /** Terminate the sandbox and release all resources. */
3117
3305
  async terminate() {
3118
3306
  const client = this.lifecycleClient;
3119
3307
  this.ownsSandbox = false;
3120
3308
  this.lifecycleClient = null;
3121
3309
  this.close();
3122
3310
  if (client) {
3123
- await client.delete(this.sandboxId);
3311
+ await client.delete(this.lifecycleIdentifier);
3124
3312
  }
3125
3313
  }
3126
3314
  // --- High-level convenience ---
@@ -3166,6 +3354,14 @@ var init_sandbox = __esm({
3166
3354
  };
3167
3355
  }
3168
3356
  // --- Process management ---
3357
+ /**
3358
+ * Start a process in the sandbox without waiting for it to exit.
3359
+ *
3360
+ * Returns a `ProcessInfo` with the assigned `pid`. Use `getProcess()` to
3361
+ * poll status, or `followStdout()` / `followOutput()` to stream output
3362
+ * until the process exits. Use `run()` instead to block until completion
3363
+ * and get combined output in one call.
3364
+ */
3169
3365
  async startProcess(command, options) {
3170
3366
  const payload = { command };
3171
3367
  if (options?.args != null) payload.args = options.args;
@@ -3187,13 +3383,16 @@ var init_sandbox = __esm({
3187
3383
  );
3188
3384
  return fromSnakeKeys(raw);
3189
3385
  }
3386
+ /** List all processes (running and exited) tracked by the sandbox daemon. */
3190
3387
  async listProcesses() {
3191
3388
  const raw = await this.http.requestJson(
3192
3389
  "GET",
3193
3390
  "/api/v1/processes"
3194
3391
  );
3195
- return (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3392
+ const processes = (raw.processes ?? []).map((p) => fromSnakeKeys(p));
3393
+ return Object.assign(processes, { traceId: raw.traceId });
3196
3394
  }
3395
+ /** Get current status and metadata for a process by PID. */
3197
3396
  async getProcess(pid) {
3198
3397
  const raw = await this.http.requestJson(
3199
3398
  "GET",
@@ -3201,9 +3400,11 @@ var init_sandbox = __esm({
3201
3400
  );
3202
3401
  return fromSnakeKeys(raw);
3203
3402
  }
3403
+ /** Send SIGKILL to a process. */
3204
3404
  async killProcess(pid) {
3205
3405
  await this.http.requestJson("DELETE", `/api/v1/processes/${pid}`);
3206
3406
  }
3407
+ /** Send an arbitrary signal to a process (e.g. `15` for SIGTERM, `9` for SIGKILL). */
3207
3408
  async sendSignal(pid, signal) {
3208
3409
  const raw = await this.http.requestJson(
3209
3410
  "POST",
@@ -3213,15 +3414,18 @@ var init_sandbox = __esm({
3213
3414
  return fromSnakeKeys(raw);
3214
3415
  }
3215
3416
  // --- Process I/O ---
3417
+ /** Write bytes to a process's stdin. The process must have been started with `stdinMode: StdinMode.PIPE`. */
3216
3418
  async writeStdin(pid, data) {
3217
3419
  await this.http.requestBytes("POST", `/api/v1/processes/${pid}/stdin`, {
3218
3420
  body: data,
3219
3421
  contentType: "application/octet-stream"
3220
3422
  });
3221
3423
  }
3424
+ /** Close a process's stdin pipe, signalling EOF to the process. */
3222
3425
  async closeStdin(pid) {
3223
3426
  await this.http.requestJson("POST", `/api/v1/processes/${pid}/stdin/close`);
3224
3427
  }
3428
+ /** Return all captured stdout lines produced so far by a process. */
3225
3429
  async getStdout(pid) {
3226
3430
  const raw = await this.http.requestJson(
3227
3431
  "GET",
@@ -3229,6 +3433,7 @@ var init_sandbox = __esm({
3229
3433
  );
3230
3434
  return fromSnakeKeys(raw);
3231
3435
  }
3436
+ /** Return all captured stderr lines produced so far by a process. */
3232
3437
  async getStderr(pid) {
3233
3438
  const raw = await this.http.requestJson(
3234
3439
  "GET",
@@ -3236,6 +3441,7 @@ var init_sandbox = __esm({
3236
3441
  );
3237
3442
  return fromSnakeKeys(raw);
3238
3443
  }
3444
+ /** Return all captured stdout+stderr lines produced so far by a process. */
3239
3445
  async getOutput(pid) {
3240
3446
  const raw = await this.http.requestJson(
3241
3447
  "GET",
@@ -3244,6 +3450,7 @@ var init_sandbox = __esm({
3244
3450
  return fromSnakeKeys(raw);
3245
3451
  }
3246
3452
  // --- Streaming (SSE) ---
3453
+ /** Stream stdout events from a process until it exits. Yields one `OutputEvent` per line. */
3247
3454
  async *followStdout(pid, options) {
3248
3455
  const stream = await this.http.requestStream(
3249
3456
  "GET",
@@ -3257,6 +3464,7 @@ var init_sandbox = __esm({
3257
3464
  yield fromSnakeKeys(raw);
3258
3465
  }
3259
3466
  }
3467
+ /** Stream stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3260
3468
  async *followStderr(pid, options) {
3261
3469
  const stream = await this.http.requestStream(
3262
3470
  "GET",
@@ -3270,6 +3478,7 @@ var init_sandbox = __esm({
3270
3478
  yield fromSnakeKeys(raw);
3271
3479
  }
3272
3480
  }
3481
+ /** Stream combined stdout+stderr events from a process until it exits. Yields one `OutputEvent` per line. */
3273
3482
  async *followOutput(pid, options) {
3274
3483
  const stream = await this.http.requestStream(
3275
3484
  "GET",
@@ -3284,12 +3493,14 @@ var init_sandbox = __esm({
3284
3493
  }
3285
3494
  }
3286
3495
  // --- File operations ---
3496
+ /** Read a file from the sandbox and return its raw bytes. */
3287
3497
  async readFile(path2) {
3288
3498
  return this.http.requestBytes(
3289
3499
  "GET",
3290
3500
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3291
3501
  );
3292
3502
  }
3503
+ /** Write raw bytes to a file in the sandbox, creating it if it does not exist. */
3293
3504
  async writeFile(path2, content) {
3294
3505
  await this.http.requestBytes(
3295
3506
  "PUT",
@@ -3297,12 +3508,14 @@ var init_sandbox = __esm({
3297
3508
  { body: content, contentType: "application/octet-stream" }
3298
3509
  );
3299
3510
  }
3511
+ /** Delete a file from the sandbox. */
3300
3512
  async deleteFile(path2) {
3301
3513
  await this.http.requestJson(
3302
3514
  "DELETE",
3303
3515
  `/api/v1/files?path=${encodeURIComponent(path2)}`
3304
3516
  );
3305
3517
  }
3518
+ /** List the contents of a directory in the sandbox. */
3306
3519
  async listDirectory(path2) {
3307
3520
  const raw = await this.http.requestJson(
3308
3521
  "GET",
@@ -3311,6 +3524,7 @@ var init_sandbox = __esm({
3311
3524
  return fromSnakeKeys(raw);
3312
3525
  }
3313
3526
  // --- PTY ---
3527
+ /** Create an interactive PTY session. Returns a `sessionId` and `token` for WebSocket connection via `connectPty()`. */
3314
3528
  async createPtySession(options) {
3315
3529
  const payload = {
3316
3530
  command: options.command,
@@ -3327,6 +3541,7 @@ var init_sandbox = __esm({
3327
3541
  );
3328
3542
  return fromSnakeKeys(raw);
3329
3543
  }
3544
+ /** Create a PTY session and connect to it immediately. Cleans up the session if the WebSocket connection fails. */
3330
3545
  async createPty(options) {
3331
3546
  const { onData, onExit, ...createOptions } = options;
3332
3547
  const session = await this.createPtySession(createOptions);
@@ -3340,6 +3555,7 @@ var init_sandbox = __esm({
3340
3555
  throw error;
3341
3556
  }
3342
3557
  }
3558
+ /** Attach to an existing PTY session by ID and token and return a connected `Pty` handle. */
3343
3559
  async connectPty(sessionId, token, options) {
3344
3560
  const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
3345
3561
  const authToken = wsUrl.searchParams.get("token") ?? token;
@@ -3364,6 +3580,7 @@ var init_sandbox = __esm({
3364
3580
  await pty.connect();
3365
3581
  return pty;
3366
3582
  }
3583
+ /** Open a TCP tunnel to a port inside the sandbox and return the local listener. */
3367
3584
  async createTunnel(remotePort, options) {
3368
3585
  return TcpTunnel.listen({
3369
3586
  baseUrl: this.baseUrl,
@@ -3374,6 +3591,7 @@ var init_sandbox = __esm({
3374
3591
  connectTimeout: options?.connectTimeout
3375
3592
  });
3376
3593
  }
3594
+ /** Connect to a sandbox VNC session for programmatic desktop control. */
3377
3595
  async connectDesktop(options) {
3378
3596
  return Desktop.connect({
3379
3597
  baseUrl: this.baseUrl,
@@ -3396,6 +3614,7 @@ var init_sandbox = __esm({
3396
3614
  return `${wsBase}/api/v1/pty/${sessionId}/ws?token=${token}`;
3397
3615
  }
3398
3616
  // --- Health ---
3617
+ /** Check the sandbox daemon health. */
3399
3618
  async health() {
3400
3619
  const raw = await this.http.requestJson(
3401
3620
  "GET",
@@ -3403,6 +3622,7 @@ var init_sandbox = __esm({
3403
3622
  );
3404
3623
  return fromSnakeKeys(raw);
3405
3624
  }
3625
+ /** Get sandbox daemon info (version, uptime, process counts). */
3406
3626
  async info() {
3407
3627
  const raw = await this.http.requestJson(
3408
3628
  "GET",
@@ -3415,6 +3635,10 @@ var init_sandbox = __esm({
3415
3635
  });
3416
3636
 
3417
3637
  // src/client.ts
3638
+ var client_exports = {};
3639
+ __export(client_exports, {
3640
+ SandboxClient: () => SandboxClient
3641
+ });
3418
3642
  function sleep2(ms) {
3419
3643
  return new Promise((resolve) => setTimeout(resolve, ms));
3420
3644
  }
@@ -3451,7 +3675,13 @@ var init_client = __esm({
3451
3675
  projectId;
3452
3676
  namespace;
3453
3677
  local;
3454
- constructor(options) {
3678
+ /** @internal Pass `true` to suppress the deprecation warning when used by `Sandbox.create()` / `Sandbox.connect()`. */
3679
+ constructor(options, _internal = false) {
3680
+ if (!_internal) {
3681
+ console.warn(
3682
+ "[tensorlake] SandboxClient is deprecated; use Sandbox.create() / Sandbox.connect() instead."
3683
+ );
3684
+ }
3455
3685
  this.apiUrl = options?.apiUrl ?? API_URL;
3456
3686
  this.apiKey = options?.apiKey ?? API_KEY;
3457
3687
  this.organizationId = options?.organizationId;
@@ -3491,12 +3721,13 @@ var init_client = __esm({
3491
3721
  return lifecyclePath(subpath, this.local, this.namespace);
3492
3722
  }
3493
3723
  // --- Sandbox CRUD ---
3724
+ /** Create a new sandbox. Returns immediately; the sandbox may still be starting. Use `createAndConnect()` for a blocking, ready-to-use handle. */
3494
3725
  async create(options) {
3495
3726
  const body = {
3496
3727
  resources: {
3497
3728
  cpus: options?.cpus ?? 1,
3498
3729
  memory_mb: options?.memoryMb ?? 1024,
3499
- ephemeral_disk_mb: options?.ephemeralDiskMb ?? 1024
3730
+ ...options?.diskMb != null ? { disk_mb: options.diskMb } : {}
3500
3731
  }
3501
3732
  };
3502
3733
  if (options?.image != null) body.image = options.image;
@@ -3517,8 +3748,10 @@ var init_client = __esm({
3517
3748
  this.path("sandboxes"),
3518
3749
  { body }
3519
3750
  );
3520
- return fromSnakeKeys(raw, "sandboxId");
3751
+ const result = fromSnakeKeys(raw, "sandboxId");
3752
+ return Object.assign(result, { traceId: raw.traceId });
3521
3753
  }
3754
+ /** Get current state and metadata for a sandbox by ID. */
3522
3755
  async get(sandboxId) {
3523
3756
  const raw = await this.http.requestJson(
3524
3757
  "GET",
@@ -3526,15 +3759,18 @@ var init_client = __esm({
3526
3759
  );
3527
3760
  return fromSnakeKeys(raw, "sandboxId");
3528
3761
  }
3762
+ /** List all sandboxes in the namespace. */
3529
3763
  async list() {
3530
3764
  const raw = await this.http.requestJson(
3531
3765
  "GET",
3532
3766
  this.path("sandboxes")
3533
3767
  );
3534
- return (raw.sandboxes ?? []).map(
3768
+ const sandboxes = (raw.sandboxes ?? []).map(
3535
3769
  (s) => fromSnakeKeys(s, "sandboxId")
3536
3770
  );
3771
+ return Object.assign(sandboxes, { traceId: raw.traceId });
3537
3772
  }
3773
+ /** Update sandbox properties such as name, exposed ports, and proxy auth settings. */
3538
3774
  async update(sandboxId, options) {
3539
3775
  const body = {};
3540
3776
  if (options.name != null) body.name = options.name;
@@ -3554,6 +3790,7 @@ var init_client = __esm({
3554
3790
  );
3555
3791
  return fromSnakeKeys(raw, "sandboxId");
3556
3792
  }
3793
+ /** Get the current proxy port settings for a sandbox. */
3557
3794
  async getPortAccess(sandboxId) {
3558
3795
  const info = await this.get(sandboxId);
3559
3796
  return {
@@ -3562,6 +3799,7 @@ var init_client = __esm({
3562
3799
  sandboxUrl: info.sandboxUrl
3563
3800
  };
3564
3801
  }
3802
+ /** Add one or more user ports to the sandbox proxy allowlist. */
3565
3803
  async exposePorts(sandboxId, ports, options) {
3566
3804
  const requestedPorts = normalizeUserPorts(ports);
3567
3805
  const current = await this.getPortAccess(sandboxId);
@@ -3574,6 +3812,7 @@ var init_client = __esm({
3574
3812
  exposedPorts: desiredPorts
3575
3813
  });
3576
3814
  }
3815
+ /** Remove one or more user ports from the sandbox proxy allowlist. */
3577
3816
  async unexposePorts(sandboxId, ports) {
3578
3817
  const requestedPorts = normalizeUserPorts(ports);
3579
3818
  const current = await this.getPortAccess(sandboxId);
@@ -3584,32 +3823,98 @@ var init_client = __esm({
3584
3823
  exposedPorts: desiredPorts
3585
3824
  });
3586
3825
  }
3826
+ /** Terminate and delete a sandbox. */
3587
3827
  async delete(sandboxId) {
3588
3828
  await this.http.requestJson(
3589
3829
  "DELETE",
3590
3830
  this.path(`sandboxes/${sandboxId}`)
3591
3831
  );
3592
3832
  }
3593
- async suspend(sandboxId) {
3833
+ /**
3834
+ * Suspend a named sandbox, preserving its state for later resume.
3835
+ *
3836
+ * Only sandboxes created with a `name` can be suspended; ephemeral sandboxes
3837
+ * cannot. By default blocks until the sandbox is fully `Suspended`. Pass
3838
+ * `{ wait: false }` to return immediately after the request is sent
3839
+ * (fire-and-return); the server processes the suspend asynchronously.
3840
+ *
3841
+ * @param sandboxId - ID or name of the sandbox.
3842
+ * @param options.wait - If `true` (default), poll until `Suspended`. Pass `false` to fire-and-return.
3843
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3844
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3845
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Suspended` within `timeout`.
3846
+ */
3847
+ async suspend(sandboxId, options) {
3594
3848
  await this.http.requestResponse(
3595
3849
  "POST",
3596
3850
  this.path(`sandboxes/${sandboxId}/suspend`)
3597
3851
  );
3852
+ if (options?.wait === false) return;
3853
+ const timeout = options?.timeout ?? 300;
3854
+ const pollInterval = options?.pollInterval ?? 1;
3855
+ const deadline = Date.now() + timeout * 1e3;
3856
+ while (Date.now() < deadline) {
3857
+ const info = await this.get(sandboxId);
3858
+ if (info.status === "suspended" /* SUSPENDED */) return;
3859
+ if (info.status === "terminated" /* TERMINATED */) {
3860
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for suspend`);
3861
+ }
3862
+ await sleep2(pollInterval * 1e3);
3863
+ }
3864
+ throw new SandboxError(`Sandbox ${sandboxId} did not suspend within ${timeout}s`);
3598
3865
  }
3599
- async resume(sandboxId) {
3866
+ /**
3867
+ * Resume a suspended sandbox and bring it back to `Running`.
3868
+ *
3869
+ * By default blocks until the sandbox is `Running` and routable. Pass
3870
+ * `{ wait: false }` to return immediately after the request is sent
3871
+ * (fire-and-return); the server processes the resume asynchronously.
3872
+ *
3873
+ * @param sandboxId - ID or name of the sandbox.
3874
+ * @param options.wait - If `true` (default), poll until `Running`. Pass `false` to fire-and-return.
3875
+ * @param options.timeout - Max seconds to wait when `wait=true` (default 300).
3876
+ * @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
3877
+ * @throws {SandboxError} If `wait=true` and the sandbox does not reach `Running` within `timeout`.
3878
+ */
3879
+ async resume(sandboxId, options) {
3600
3880
  await this.http.requestResponse(
3601
3881
  "POST",
3602
3882
  this.path(`sandboxes/${sandboxId}/resume`)
3603
3883
  );
3884
+ if (options?.wait === false) return;
3885
+ const timeout = options?.timeout ?? 300;
3886
+ const pollInterval = options?.pollInterval ?? 1;
3887
+ const deadline = Date.now() + timeout * 1e3;
3888
+ while (Date.now() < deadline) {
3889
+ const info = await this.get(sandboxId);
3890
+ if (info.status === "running" /* RUNNING */) return;
3891
+ if (info.status === "terminated" /* TERMINATED */) {
3892
+ throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for resume`);
3893
+ }
3894
+ await sleep2(pollInterval * 1e3);
3895
+ }
3896
+ throw new SandboxError(`Sandbox ${sandboxId} did not resume within ${timeout}s`);
3604
3897
  }
3898
+ /** Claim a warm sandbox from a pool, creating one if no warm containers are available. */
3605
3899
  async claim(poolId) {
3606
3900
  const raw = await this.http.requestJson(
3607
3901
  "POST",
3608
3902
  this.path(`sandbox-pools/${poolId}/sandboxes`)
3609
3903
  );
3610
- return fromSnakeKeys(raw, "sandboxId");
3904
+ const result = fromSnakeKeys(raw, "sandboxId");
3905
+ return Object.assign(result, { traceId: raw.traceId });
3611
3906
  }
3612
3907
  // --- Snapshots ---
3908
+ /**
3909
+ * Request a snapshot of a running sandbox's filesystem.
3910
+ *
3911
+ * This call **returns immediately** with a `snapshotId` and `in_progress`
3912
+ * status — the snapshot is created asynchronously. Poll `getSnapshot()` until
3913
+ * `completed` or `failed`, or use `snapshotAndWait()` to block automatically.
3914
+ *
3915
+ * @param options.contentMode - `"filesystem_only"` for cold-boot snapshots (e.g. image builds).
3916
+ * Omit to use the server default (full VM snapshot).
3917
+ */
3613
3918
  async snapshot(sandboxId, options) {
3614
3919
  const requestOptions = options?.contentMode != null ? { body: { snapshot_content_mode: options.contentMode } } : void 0;
3615
3920
  const raw = await this.http.requestJson(
@@ -3619,6 +3924,7 @@ var init_client = __esm({
3619
3924
  );
3620
3925
  return fromSnakeKeys(raw, "snapshotId");
3621
3926
  }
3927
+ /** Get current status and metadata for a snapshot by ID. */
3622
3928
  async getSnapshot(snapshotId) {
3623
3929
  const raw = await this.http.requestJson(
3624
3930
  "GET",
@@ -3626,21 +3932,37 @@ var init_client = __esm({
3626
3932
  );
3627
3933
  return fromSnakeKeys(raw, "snapshotId");
3628
3934
  }
3935
+ /** List all snapshots in the namespace. */
3629
3936
  async listSnapshots() {
3630
3937
  const raw = await this.http.requestJson(
3631
3938
  "GET",
3632
3939
  this.path("snapshots")
3633
3940
  );
3634
- return (raw.snapshots ?? []).map(
3941
+ const snapshots = (raw.snapshots ?? []).map(
3635
3942
  (s) => fromSnakeKeys(s, "snapshotId")
3636
3943
  );
3944
+ return Object.assign(snapshots, { traceId: raw.traceId });
3637
3945
  }
3946
+ /** Delete a snapshot by ID. */
3638
3947
  async deleteSnapshot(snapshotId) {
3639
3948
  await this.http.requestJson(
3640
3949
  "DELETE",
3641
3950
  this.path(`snapshots/${snapshotId}`)
3642
3951
  );
3643
3952
  }
3953
+ /**
3954
+ * Create a snapshot and block until it is committed.
3955
+ *
3956
+ * Combines `snapshot()` with polling `getSnapshot()` until `completed`.
3957
+ * Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
3958
+ * without managing the client separately.
3959
+ *
3960
+ * @param sandboxId - ID of the running sandbox to snapshot.
3961
+ * @param options.timeout - Max seconds to wait (default 300).
3962
+ * @param options.pollInterval - Seconds between status polls (default 1).
3963
+ * @param options.contentMode - Content mode passed through to `snapshot()`.
3964
+ * @throws {SandboxError} If the snapshot fails or `timeout` elapses.
3965
+ */
3644
3966
  async snapshotAndWait(sandboxId, options) {
3645
3967
  const timeout = options?.timeout ?? 300;
3646
3968
  const pollInterval = options?.pollInterval ?? 1;
@@ -3663,6 +3985,7 @@ var init_client = __esm({
3663
3985
  );
3664
3986
  }
3665
3987
  // --- Pools ---
3988
+ /** Create a new sandbox pool with warm pre-booted containers. */
3666
3989
  async createPool(options) {
3667
3990
  const body = {
3668
3991
  image: options.image,
@@ -3684,6 +4007,7 @@ var init_client = __esm({
3684
4007
  );
3685
4008
  return fromSnakeKeys(raw, "poolId");
3686
4009
  }
4010
+ /** Get current state and metadata for a sandbox pool by ID. */
3687
4011
  async getPool(poolId) {
3688
4012
  const raw = await this.http.requestJson(
3689
4013
  "GET",
@@ -3691,15 +4015,18 @@ var init_client = __esm({
3691
4015
  );
3692
4016
  return fromSnakeKeys(raw, "poolId");
3693
4017
  }
4018
+ /** List all sandbox pools in the namespace. */
3694
4019
  async listPools() {
3695
4020
  const raw = await this.http.requestJson(
3696
4021
  "GET",
3697
4022
  this.path("sandbox-pools")
3698
4023
  );
3699
- return (raw.pools ?? []).map(
4024
+ const pools = (raw.pools ?? []).map(
3700
4025
  (p) => fromSnakeKeys(p, "poolId")
3701
4026
  );
4027
+ return Object.assign(pools, { traceId: raw.traceId });
3702
4028
  }
4029
+ /** Replace the configuration of an existing sandbox pool. */
3703
4030
  async updatePool(poolId, options) {
3704
4031
  const body = {
3705
4032
  image: options.image,
@@ -3721,6 +4048,7 @@ var init_client = __esm({
3721
4048
  );
3722
4049
  return fromSnakeKeys(raw, "poolId");
3723
4050
  }
4051
+ /** Delete a sandbox pool. Fails if the pool has active containers. */
3724
4052
  async deletePool(poolId) {
3725
4053
  await this.http.requestJson(
3726
4054
  "DELETE",
@@ -3728,6 +4056,7 @@ var init_client = __esm({
3728
4056
  );
3729
4057
  }
3730
4058
  // --- Connect ---
4059
+ /** Return a `Sandbox` handle for an existing running sandbox without verifying it exists. */
3731
4060
  connect(identifier, proxyUrl, routingHint) {
3732
4061
  const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
3733
4062
  return new Sandbox({
@@ -3739,26 +4068,35 @@ var init_client = __esm({
3739
4068
  routingHint
3740
4069
  });
3741
4070
  }
4071
+ /**
4072
+ * Create a sandbox, wait for it to reach `Running`, and return a connected handle.
4073
+ *
4074
+ * Blocks until the sandbox is ready or `startupTimeout` elapses. The returned
4075
+ * `Sandbox` auto-terminates when `terminate()` is called.
4076
+ *
4077
+ * @param options.startupTimeout - Max seconds to wait for `Running` status (default 60).
4078
+ * @throws {SandboxError} If the sandbox terminates during startup or the timeout elapses.
4079
+ */
3742
4080
  async createAndConnect(options) {
3743
4081
  const startupTimeout = options?.startupTimeout ?? 60;
3744
- let result;
3745
- if (options?.poolId != null) {
3746
- result = await this.claim(options.poolId);
3747
- } else {
3748
- result = await this.create(options);
3749
- }
3750
- if (result.status === "running" /* RUNNING */) {
3751
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, result.routingHint);
4082
+ const result = options?.poolId != null ? await this.claim(options.poolId) : await this.create(options);
4083
+ const requestedName = options?.poolId != null ? null : options?.name ?? null;
4084
+ const finishConnect = (routingHint, name) => {
4085
+ const sandbox = this.connect(result.sandboxId, options?.proxyUrl, routingHint);
3752
4086
  sandbox._setOwner(this);
4087
+ sandbox.traceId = result.traceId;
4088
+ sandbox._setLifecycleIdentifier(result.sandboxId);
4089
+ sandbox._setName(name ?? requestedName);
3753
4090
  return sandbox;
4091
+ };
4092
+ if (result.status === "running" /* RUNNING */) {
4093
+ return finishConnect(result.routingHint, result.name);
3754
4094
  }
3755
4095
  const deadline = Date.now() + startupTimeout * 1e3;
3756
4096
  while (Date.now() < deadline) {
3757
4097
  const info = await this.get(result.sandboxId);
3758
4098
  if (info.status === "running" /* RUNNING */) {
3759
- const sandbox = this.connect(result.sandboxId, options?.proxyUrl, info.routingHint);
3760
- sandbox._setOwner(this);
3761
- return sandbox;
4099
+ return finishConnect(info.routingHint, info.name);
3762
4100
  }
3763
4101
  if (info.status === "terminated" /* TERMINATED */) {
3764
4102
  throw new SandboxError(
@@ -4497,7 +4835,8 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4497
4835
  sandbox = await client.createAndConnect({
4498
4836
  ...plan.baseImage == null ? {} : { image: plan.baseImage },
4499
4837
  cpus: options.cpus ?? 2,
4500
- memoryMb: options.memoryMb ?? 4096
4838
+ memoryMb: options.memoryMb ?? 4096,
4839
+ ...options.diskMb != null ? { diskMb: options.diskMb } : {}
4501
4840
  });
4502
4841
  emit({
4503
4842
  type: "status",
@@ -4510,8 +4849,7 @@ async function createSandboxImage(source, options = {}, deps = {}) {
4510
4849
  });
4511
4850
  emit({
4512
4851
  type: "snapshot_created",
4513
- snapshot_id: snapshot.snapshotId,
4514
- snapshot_uri: snapshot.snapshotUri ?? null
4852
+ snapshot_id: snapshot.snapshotId
4515
4853
  });
4516
4854
  if (!snapshot.snapshotUri) {
4517
4855
  throw new Error(
@@ -4555,27 +4893,33 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
4555
4893
  name: { type: "string", short: "n" },
4556
4894
  cpus: { type: "string" },
4557
4895
  memory: { type: "string" },
4896
+ disk: { type: "string" },
4558
4897
  public: { type: "boolean", default: false }
4559
4898
  }
4560
4899
  });
4561
4900
  const dockerfilePath = parsed.positionals[0];
4562
4901
  if (!dockerfilePath) {
4563
- throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--public]");
4902
+ throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk GB] [--public]");
4564
4903
  }
4565
4904
  const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
4566
4905
  const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
4906
+ const diskGb = parsed.values.disk != null ? Number(parsed.values.disk) : void 0;
4567
4907
  if (cpus != null && !Number.isFinite(cpus)) {
4568
4908
  throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
4569
4909
  }
4570
4910
  if (memoryMb != null && !Number.isInteger(memoryMb)) {
4571
4911
  throw new Error(`Invalid --memory value: ${parsed.values.memory}`);
4572
4912
  }
4913
+ if (diskGb != null && !Number.isInteger(diskGb)) {
4914
+ throw new Error(`Invalid --disk value: ${parsed.values.disk}`);
4915
+ }
4573
4916
  await createSandboxImage(
4574
4917
  dockerfilePath,
4575
4918
  {
4576
4919
  registeredName: parsed.values.name,
4577
4920
  cpus,
4578
4921
  memoryMb,
4922
+ diskMb: diskGb != null ? diskGb * 1024 : void 0,
4579
4923
  isPublic: parsed.values.public
4580
4924
  },
4581
4925
  { emit: ndjsonStdoutEmit }