@lizard-build/sdk 0.1.49 → 0.1.52

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
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  LizardError: () => LizardError,
37
37
  MetricsAPI: () => MetricsAPI,
38
38
  NotFoundError: () => NotFoundError,
39
+ PaymentRequiredError: () => PaymentRequiredError,
39
40
  PlatformClient: () => PlatformClient,
40
41
  ProjectsAPI: () => ProjectsAPI,
41
42
  RegionsAPI: () => RegionsAPI,
@@ -111,18 +112,44 @@ var TimeoutError = class extends LizardError {
111
112
  this.name = "TimeoutError";
112
113
  }
113
114
  };
115
+ var PaymentRequiredError = class extends LizardError {
116
+ constructor(message = "Payment required") {
117
+ super(message);
118
+ this.name = "PaymentRequiredError";
119
+ }
120
+ /** The one page to open next for this {@link paymentStatus}. */
121
+ get url() {
122
+ const s = this.paymentStatus;
123
+ if (this.invoiceUrl) return this.invoiceUrl;
124
+ if ((s === "trial_available" || s === "subscription_required") && this.subscribeUrl) return this.subscribeUrl;
125
+ if (s && PREPAID_STATUSES.has(s) && this.topupUrl) return this.topupUrl;
126
+ return this.billingUrl ?? this.subscribeUrl ?? this.topupUrl;
127
+ }
128
+ };
129
+ var PREPAID_STATUSES = /* @__PURE__ */ new Set(["grace", "frozen", "card_required", "credits_required"]);
130
+ var ERROR_CODE = /^[A-Z][A-Z0-9_]*$/;
114
131
  async function handleApiError(res) {
115
- let message;
116
- let code;
132
+ let body = {};
117
133
  try {
118
- const body = await res.json();
119
- message = body.error ?? res.statusText;
120
- if (typeof body.code === "string") code = body.code;
134
+ const parsed = await res.json();
135
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) body = parsed;
121
136
  } catch {
122
- message = res.statusText;
123
137
  }
138
+ const text = (key) => typeof body[key] === "string" && body[key] ? body[key] : void 0;
139
+ const error = text("error");
140
+ const errorIsCode = !!error && ERROR_CODE.test(error);
141
+ const message = (errorIsCode ? text("message") ?? error : error ?? text("message")) ?? res.statusText;
142
+ const code = text("code") ?? (errorIsCode ? error : void 0);
124
143
  let err;
125
- if (res.status === 401 || res.status === 403) err = new AuthenticationError(message);
144
+ if (res.status === 402) {
145
+ const paid = new PaymentRequiredError(message);
146
+ paid.paymentStatus = text("status");
147
+ paid.subscribeUrl = text("subscribeUrl");
148
+ paid.billingUrl = text("billingUrl");
149
+ paid.topupUrl = text("topupUrl");
150
+ paid.invoiceUrl = text("invoiceUrl");
151
+ err = paid;
152
+ } else if (res.status === 401 || res.status === 403) err = new AuthenticationError(message);
126
153
  else if (res.status === 404) err = new NotFoundError(message);
127
154
  else if (res.status === 409) err = new ConflictError(message);
128
155
  else if (res.status === 408 || res.status === 504) err = new TimeoutError(message);
@@ -548,7 +575,9 @@ var Process = class {
548
575
  envs: opts?.envs,
549
576
  user: opts?.user,
550
577
  workdir: opts?.workdir,
551
- timeoutMs: opts?.timeoutMs
578
+ timeoutMs: opts?.timeoutMs,
579
+ // The pid event is opt-in: older CLIs print any unknown data line as output.
580
+ pidEvent: opts?.onPid ? true : void 0
552
581
  }),
553
582
  signal: AbortSignal.timeout(opts?.timeoutMs ?? 6e4)
554
583
  });
@@ -556,11 +585,6 @@ var Process = class {
556
585
  if (!streaming) return res.json();
557
586
  return this.consumeStream(res, opts);
558
587
  }
559
- /**
560
- * Read an SSE exec stream, handing each line to the caller as it arrives while
561
- * still accumulating the full result. Events are `{stream, line}` for output and
562
- * `{exitCode}` at the end.
563
- */
564
588
  /**
565
589
  * List the processes this sandbox is currently running.
566
590
  *
@@ -584,8 +608,8 @@ var Process = class {
584
608
  /**
585
609
  * Signal a running process. Defaults to `SIGTERM`.
586
610
  *
587
- * The signal goes to the process group, so a shell's children die with it —
588
- * killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
611
+ * The signal goes to the whole process tree, so a shell's children die with
612
+ * it — killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
589
613
  *
590
614
  * The pid comes from {@link list}, or from the `pid` event at the start of a
591
615
  * streaming `exec`.
@@ -604,6 +628,11 @@ var Process = class {
604
628
  });
605
629
  if (!res.ok) await handleApiError(res);
606
630
  }
631
+ /**
632
+ * Read an SSE exec stream, handing each line to the caller as it arrives while
633
+ * still accumulating the full result. Events are `{pid}` first (when asked for
634
+ * with `pidEvent`), `{stream, line}` for output and `{exitCode}` at the end.
635
+ */
607
636
  async consumeStream(res, opts) {
608
637
  const reader = res.body?.getReader();
609
638
  if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
@@ -654,33 +683,37 @@ var Fs = class {
654
683
  /**
655
684
  * Write a file into the sandbox filesystem.
656
685
  *
657
- * Creates parent directories automatically if they don't exist.
658
- * Byte input must contain valid UTF-8; arbitrary binary uploads are unsupported.
686
+ * Creates parent directories automatically if they don't exist. A string is
687
+ * written as UTF-8; bytes (`Uint8Array`, `Buffer` or `ArrayBuffer`) are written
688
+ * exactly as given, so binary files round-trip with {@link readBytes}.
689
+ *
690
+ * With `opts.user` the file is written as (and owned by) that user.
659
691
  *
660
692
  * @example
661
693
  * ```ts
662
694
  * await sandbox.fs.write('/app/index.js', 'console.log("hello")')
663
695
  * ```
664
696
  *
665
- * @example Write UTF-8 bytes:
697
+ * @example Write binary data:
666
698
  * ```ts
667
- * await sandbox.fs.write('/app/notes.txt', new TextEncoder().encode('hello'))
699
+ * await sandbox.fs.write('/app/logo.png', await readFile('logo.png'))
668
700
  * ```
669
701
  */
670
702
  async write(path, data, opts) {
671
- const content = typeof data === "string" ? data : new TextDecoder("utf-8", { fatal: true }).decode(data);
703
+ const body = typeof data === "string" ? { path, content: data, user: opts?.user } : { path, content: bytesToBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data), encoding: "base64", user: opts?.user };
672
704
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
673
705
  method: "POST",
674
706
  headers: this.config.headers,
675
- body: JSON.stringify({ path, content, user: opts?.user })
707
+ body: JSON.stringify(body)
676
708
  });
677
709
  if (!res.ok) await handleApiError(res);
678
710
  }
679
711
  /**
680
712
  * Read a file from the sandbox filesystem.
681
713
  *
682
- * @returns The file contents as a UTF-8 string.
714
+ * @returns The file contents as a UTF-8 string. Use {@link readBytes} for binary files.
683
715
  *
716
+ * Throws `NotFoundError` if the path does not exist.
684
717
  * @example
685
718
  * ```ts
686
719
  * const content = await sandbox.fs.read('/app/index.js')
@@ -748,9 +781,7 @@ var Fs = class {
748
781
  * console.log(info.size, info.type)
749
782
  * ```
750
783
  *
751
- * Throws `NotFoundError` if the path does not exist. Sandboxes created before
752
- * this shipped run a guest agent without it and throw `LizardError` (501) —
753
- * recreate the sandbox to use it.
784
+ * Throws `NotFoundError` if the path does not exist.
754
785
  */
755
786
  async stat(path, opts) {
756
787
  const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/stat`);
@@ -767,9 +798,6 @@ var Fs = class {
767
798
  * ```ts
768
799
  * await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
769
800
  * ```
770
- *
771
- * Sandboxes created before this shipped run a guest agent without it and throw
772
- * `LizardError` (501) — recreate the sandbox to use it.
773
801
  */
774
802
  async move(from, to) {
775
803
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/move`, {
@@ -798,8 +826,7 @@ var Fs = class {
798
826
  * }, 1000)
799
827
  * ```
800
828
  *
801
- * Sandboxes created before this shipped run a guest agent without it and throw
802
- * `LizardError` (501) — recreate the sandbox to use it.
829
+ * Not available on Firecracker sandboxes yet: the call throws `LizardError` (501).
803
830
  */
804
831
  async watch(path, opts) {
805
832
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`, {
@@ -845,6 +872,12 @@ var Watcher = class {
845
872
  if (!res.ok) await handleApiError(res);
846
873
  }
847
874
  };
875
+ function bytesToBase64(bytes) {
876
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
877
+ let s = "";
878
+ for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
879
+ return btoa(s);
880
+ }
848
881
 
849
882
  // src/sandbox/desktop.ts
850
883
  var BUTTONS = { left: "1", middle: "2", right: "3" };
@@ -1145,11 +1178,17 @@ var Sandbox = class extends SandboxClient {
1145
1178
  async kill(opts) {
1146
1179
  return SandboxClient.killSandbox(this.sandboxId, this.resolveOpts(opts));
1147
1180
  }
1148
- /** Queue a CRIU checkpoint and stop compute. Use waitForStatus('paused') before resuming. */
1181
+ /**
1182
+ * Pause the sandbox: its memory, running processes and files are kept and
1183
+ * compute stops. Wait with `waitForStatus('paused')` before resuming.
1184
+ */
1149
1185
  async pause(opts) {
1150
1186
  return SandboxClient.pauseSandbox(this.sandboxId, this.resolveOpts(opts));
1151
1187
  }
1152
- /** Queue restoration of saved memory and files. Use waitForStatus('running') before executing. */
1188
+ /**
1189
+ * Resume a paused sandbox exactly where it stopped, processes included. Wait
1190
+ * with `waitForStatus('running')` before running commands.
1191
+ */
1153
1192
  async resume(opts) {
1154
1193
  return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
1155
1194
  }
@@ -1182,33 +1221,82 @@ var Sandbox = class extends SandboxClient {
1182
1221
  return SandboxClient.setTimeoutSandbox(this.sandboxId, timeoutMs, this.resolveOpts(opts));
1183
1222
  }
1184
1223
  /**
1185
- * Register a public HTTPS route and return its hostname without a scheme.
1224
+ * Publish a port on a public HTTPS hostname and return the hostname without a scheme.
1186
1225
  *
1187
- * Useful for accessing HTTP servers started inside the sandbox from your
1188
- * agent or tests without additional tunneling.
1226
+ * The port is private: requests need {@link accessToken} (set by this call) as
1227
+ * the `X-Lizard-Access-Token` header, or `?lizard_token=<token>` in a browser.
1228
+ * {@link exposePort} returns the token and a ready-made browser URL together.
1189
1229
  *
1190
1230
  * @example
1191
1231
  * ```ts
1192
1232
  * // Start an HTTP server on port 3000 before exposing it.
1193
1233
  * const hostname = await sandbox.getHost(3000)
1194
- * console.log(`https://${hostname}`)
1234
+ * const res = await fetch(`https://${hostname}/`, {
1235
+ * headers: { 'X-Lizard-Access-Token': sandbox.accessToken! },
1236
+ * })
1195
1237
  * ```
1196
1238
  */
1197
1239
  async getHost(port, opts) {
1198
- const { hostname } = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
1199
- return hostname;
1240
+ return (await this.exposePort(port, opts)).hostname;
1200
1241
  }
1201
- /** Fork is unsupported on Kubernetes; the backend returns HTTP 501. */
1202
- fork(opts = {}) {
1203
- return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/fork`, { count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 });
1242
+ /**
1243
+ * Publish a port and return everything needed to reach it: the hostname, a
1244
+ * browser `url` that carries the token, and the `accessToken` for API clients.
1245
+ *
1246
+ * @example
1247
+ * ```ts
1248
+ * const { url, hostname, accessToken } = await sandbox.exposePort(3000)
1249
+ * console.log(url) // open in a browser
1250
+ * await fetch(`https://${hostname}/api`, { headers: { 'X-Lizard-Access-Token': accessToken } })
1251
+ * ```
1252
+ */
1253
+ async exposePort(port, opts) {
1254
+ const exposed = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
1255
+ if (exposed.accessToken) this.accessToken = exposed.accessToken;
1256
+ return exposed;
1257
+ }
1258
+ /**
1259
+ * Clone this running sandbox `count` times (1-10). Each fork is a copy of the
1260
+ * microVM at this instant -- memory, running processes and files -- and is
1261
+ * billed like its source. The source keeps running.
1262
+ *
1263
+ * Returns one entry per requested fork, in order: `{ sandbox, info }`, or
1264
+ * `{ error }` for a copy that could not be made (e.g. the account's sandbox
1265
+ * limit). A sandbox with a volume attached cannot be forked (`ConflictError`, 409).
1266
+ *
1267
+ * @param opts.timeoutMs Lifetime of each fork; defaults to the source's timeout.
1268
+ *
1269
+ * @example
1270
+ * ```ts
1271
+ * const forks = await sandbox.fork({ count: 2 })
1272
+ * for (const f of forks) if (f.sandbox) await f.sandbox.process.exec('echo hi')
1273
+ * ```
1274
+ */
1275
+ async fork(opts = {}) {
1276
+ const result = await new PlatformClient(this.resolveOpts()).post(
1277
+ `/api/sandboxes/${this.sandboxId}/fork`,
1278
+ { count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 }
1279
+ );
1280
+ const Ctor = this.constructor;
1281
+ return (Array.isArray(result) ? result : []).map((entry) => {
1282
+ const id = entry.sandbox?.sandboxId ?? entry.sandbox?.id;
1283
+ if (!entry.sandbox || !id) return { error: entry.error ?? "fork failed" };
1284
+ return { sandbox: new Ctor({ ...this.resolveOpts(), sandboxId: id }), info: { ...entry.sandbox, sandboxId: id } };
1285
+ });
1204
1286
  }
1205
- /** Capture memory and workspace files. Disconnect active clients before capturing. */
1287
+ /**
1288
+ * Save this sandbox -- memory, running processes and files -- as a private
1289
+ * snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
1290
+ * start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
1291
+ * keeps running. `poolSize` applies to container sandboxes only.
1292
+ */
1206
1293
  snapshot(name = `Snapshot ${this.sandboxId}`, opts = {}) {
1207
1294
  return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5 });
1208
1295
  }
1209
1296
  unexpose(port) {
1210
1297
  return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
1211
1298
  }
1299
+ /** Stream the sandbox's logs. Not available on Firecracker sandboxes yet (`LizardError`, 501). */
1212
1300
  logs(opts = {}) {
1213
1301
  return new PlatformClient(this.resolveOpts()).events(query(`/api/sandboxes/${this.sandboxId}/logs`, { tail: opts.tail }), { signal: opts.signal });
1214
1302
  }
@@ -1218,7 +1306,7 @@ var Sandbox = class extends SandboxClient {
1218
1306
  static getSnapshot(snapshotId, opts) {
1219
1307
  return new PlatformClient(opts ?? {}).get(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`);
1220
1308
  }
1221
- /** Start a sandbox from an available warm copy in the snapshot's project and region. */
1309
+ /** Start a new sandbox from a snapshot, in the snapshot's project and region. */
1222
1310
  static async restore(snapshotId, opts) {
1223
1311
  const client = new PlatformClient(opts ?? {});
1224
1312
  const snapshot = await this.getSnapshot(snapshotId, opts);
@@ -1233,7 +1321,7 @@ var Sandbox = class extends SandboxClient {
1233
1321
  static setSnapshotWarmPool(snapshotId, poolSize, opts) {
1234
1322
  return new PlatformClient(opts ?? {}).patch(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`, { poolSize });
1235
1323
  }
1236
- /** Release idle warm copies; retain saved state and existing claimed sandboxes. */
1324
+ /** Release idle warm copies (container snapshots); retain saved state and existing claimed sandboxes. */
1237
1325
  static pauseSnapshot(snapshotId, opts) {
1238
1326
  return new PlatformClient(opts ?? {}).post(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}/pause`, {});
1239
1327
  }
@@ -1611,88 +1699,54 @@ var RegionsAPI = class {
1611
1699
  }
1612
1700
  };
1613
1701
 
1614
- // src/cli.ts
1615
- var import_node_child_process = require("child_process");
1616
- var LizardCLI = class {
1617
- constructor(opts = {}) {
1618
- this.opts = opts;
1619
- }
1620
- run(args, opts = {}) {
1621
- return new Promise((resolve, reject) => {
1622
- const env = { ...process.env };
1623
- if (this.opts.apiKey) {
1624
- env.LIZARD_API_KEY = this.opts.apiKey;
1625
- env.LIZARD_TOKEN = this.opts.apiKey;
1626
- }
1627
- if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
1628
- const child = (0, import_node_child_process.spawn)(this.opts.executable ?? "lizard", ["--json", ...args], {
1629
- cwd: this.opts.cwd,
1630
- env,
1631
- shell: false,
1632
- stdio: "pipe",
1633
- signal: opts.signal,
1634
- timeout: this.opts.timeoutMs ?? 6e5
1635
- });
1636
- child.stdout.setEncoding("utf8");
1637
- child.stderr.setEncoding("utf8");
1638
- let stdout = "", stderr = "", bytes = 0;
1639
- const collect = (stream, data) => {
1640
- bytes += Buffer.byteLength(data);
1641
- if (bytes > 16 * 1024 * 1024) {
1642
- child.kill();
1643
- reject(new Error("CLI output exceeds 16 MiB"));
1644
- return;
1645
- }
1646
- if (stream === "stdout") stdout += data;
1647
- else stderr += data;
1648
- };
1649
- child.stdout.on("data", (data) => collect("stdout", data));
1650
- child.stderr.on("data", (data) => collect("stderr", data));
1651
- child.on("error", reject);
1652
- child.on("close", (code, signal) => {
1653
- if (signal) {
1654
- reject(new Error(`CLI ended with signal ${signal}`));
1655
- return;
1656
- }
1657
- let events = [];
1658
- try {
1659
- events = [JSON.parse(stdout)];
1660
- } catch {
1661
- events = stdout.split("\n").filter(Boolean).flatMap((line) => {
1662
- try {
1663
- return [JSON.parse(line)];
1664
- } catch {
1665
- return [];
1666
- }
1667
- });
1668
- }
1669
- resolve({ code: code ?? 1, stdout, stderr, events });
1670
- });
1671
- child.stdin.on("error", (error) => {
1672
- if (error.code !== "EPIPE") reject(error);
1673
- });
1674
- child.stdin.end(opts.stdin);
1675
- });
1676
- }
1677
- };
1678
-
1679
1702
  // src/platform/billing.ts
1680
1703
  var BillingAPI = class {
1681
1704
  constructor(client) {
1682
1705
  this.client = client;
1683
1706
  }
1684
- /** Current balance, status, burn rate, and runway. */
1707
+ /**
1708
+ * The plan: trial, this month's credits, overage, next charge, cancel date. With
1709
+ * `workspaceId`, the plan of that workspace's owner (`isOwner: false`, no card or
1710
+ * invoice fields).
1711
+ */
1712
+ subscription(opts) {
1713
+ return this.client.get(query("/api/billing/subscription", { workspaceId: opts?.workspaceId }));
1714
+ }
1715
+ /**
1716
+ * Opens Stripe Checkout for Pro: the trial when the account can have one, otherwise
1717
+ * Pro at once ($19 today). Returns the page for the user to finish; a second call
1718
+ * while it is open returns the same session.
1719
+ */
1720
+ startCheckout(opts = {}) {
1721
+ return this.client.post("/api/billing/subscription/checkout", { returnUrl: opts.returnUrl });
1722
+ }
1723
+ /** Ends the trial now: charges $19 and starts the first paid month with $19 of credits. */
1724
+ startProNow() {
1725
+ return this.client.post("/api/billing/subscription/start-now", {});
1726
+ }
1727
+ /** Cancels Pro at the end of the current month or trial. Cancelling in the trial costs nothing. */
1728
+ cancel() {
1729
+ return this.client.post("/api/billing/subscription/cancel", {});
1730
+ }
1731
+ /** Undoes {@link cancel}: Pro renews as usual. */
1732
+ resume() {
1733
+ return this.client.post("/api/billing/subscription/resume", {});
1734
+ }
1735
+ /** Redeems a promo code: a longer trial with more trial credits. Works only before the first payment. */
1736
+ redeemPromo(code) {
1737
+ return this.client.post("/api/billing/promo/redeem", { code });
1738
+ }
1739
+ /** Prepaid credits balance, status, burn rate and runway (old `payg` accounts and enterprise). */
1685
1740
  balance() {
1686
1741
  return this.client.get("/api/billing/balance");
1687
1742
  }
1688
- /** A page of balance transactions, newest first. */
1743
+ /** A page of balance transactions, newest first (old `payg` accounts and enterprise). */
1689
1744
  transactions(opts) {
1690
- const qs = new URLSearchParams();
1691
- if (opts?.limit !== void 0) qs.set("limit", String(opts.limit));
1692
- if (opts?.cursor) qs.set("cursor", opts.cursor);
1693
- if (opts?.includeUsage) qs.set("includeUsage", "1");
1694
- const q = qs.toString();
1695
- return this.client.get(`/api/billing/transactions${q ? `?${q}` : ""}`);
1745
+ return this.client.get(query("/api/billing/transactions", {
1746
+ limit: opts?.limit,
1747
+ cursor: opts?.cursor || void 0,
1748
+ includeUsage: opts?.includeUsage ? "1" : void 0
1749
+ }));
1696
1750
  }
1697
1751
  /** Cost summary for the current billing period, broken down by resource. */
1698
1752
  summary() {
@@ -1702,46 +1756,13 @@ var BillingAPI = class {
1702
1756
  live() {
1703
1757
  return this.client.get("/api/billing/live");
1704
1758
  }
1759
+ /** Saved cards. Cards are added in Checkout or on the Billing page. */
1705
1760
  paymentMethods() {
1706
1761
  return this.client.get("/api/billing/payment-methods");
1707
1762
  }
1708
- setupPaymentMethod(returnUrl) {
1709
- return this.client.post("/api/billing/payment-methods/setup", { returnUrl });
1710
- }
1711
1763
  removePaymentMethod(id) {
1712
1764
  return this.client.delete(`/api/billing/payment-methods/${encodeURIComponent(id)}`);
1713
1765
  }
1714
- purchase(creditCents, opts = {}) {
1715
- if (!Number.isSafeInteger(creditCents) || creditCents <= 0) throw new Error("creditCents must be a positive integer");
1716
- return this.client.post("/api/billing/purchase", { creditCents, paymentMethod: opts.paymentMethod ?? "card", returnUrl: opts.returnUrl });
1717
- }
1718
- autoTopup() {
1719
- return this.client.get("/api/billing/auto-topup");
1720
- }
1721
- setAutoTopup(opts) {
1722
- return this.client.put("/api/billing/auto-topup", opts);
1723
- }
1724
- runAutoTopup() {
1725
- return this.client.post("/api/billing/auto-topup/run", {});
1726
- }
1727
- redeemPromo(code) {
1728
- return this.client.post("/api/billing/promo/redeem", { code });
1729
- }
1730
- x402Quote(creditCents) {
1731
- return this.client.post("/api/billing/purchase/x402/quote", { creditCents });
1732
- }
1733
- paymentStatus(attemptId) {
1734
- return this.client.get(`/api/billing/purchase/x402/${encodeURIComponent(attemptId)}`);
1735
- }
1736
- /** Pay with the CLI's durable x402 journal. Requires CLI >= 4.0.8. Amounts are cents. */
1737
- async payX402(creditCents, opts) {
1738
- if (![creditCents, opts.maxTotalCents].every((n) => Number.isSafeInteger(n) && n > 0)) throw new Error("Payment amounts must be positive integer cents");
1739
- const args = ["credits", "topup", (creditCents / 100).toFixed(2), "--method", "x402", "--max-total", (opts.maxTotalCents / 100).toFixed(2), "--yes"];
1740
- if (opts.requestId) args.push("--request-id", opts.requestId);
1741
- const result = await new LizardCLI({ apiKey: this.client.config.apiKey, apiUrl: this.client.config.apiUrl, executable: opts.executable }).run(args);
1742
- if (result.code !== 0) throw new Error(`x402 CLI exited with code ${result.code}; inspect the payment journal or query paymentStatus before retrying`);
1743
- return result.events[0];
1744
- }
1745
1766
  };
1746
1767
 
1747
1768
  // src/volume/index.ts
@@ -1772,7 +1793,7 @@ var Volume = class _Volume {
1772
1793
  * "the scratch disk for this task" no longer has to store an id between runs.
1773
1794
  *
1774
1795
  * An existing volume is returned as-is — `sizeGb` applies only to a fresh create,
1775
- * so an existing volume keeps its size. Call {@link Volume.resize} to change it.
1796
+ * so an existing volume keeps its size.
1776
1797
  */
1777
1798
  static async getOrCreate(projectId, name, opts) {
1778
1799
  const config = new ConnectionConfig(opts);
@@ -1815,11 +1836,15 @@ var Volume = class _Volume {
1815
1836
  /**
1816
1837
  * Resize a volume, by name or by id, to `sizeGb`.
1817
1838
  *
1818
- * The change is in place and online: the size is a quota, so no data is copied,
1819
- * it completes in well under a second, and a sandbox that has the volume mounted
1820
- * keeps running and sees the new size immediately. Both growing and shrinking are
1821
- * allowed, but a shrink must leave at least 10% of the new size free — otherwise
1822
- * it throws `ConflictError` with `code: 'volume_too_full_to_shrink'`.
1839
+ * **Firecracker volumes cannot be resized yet**: volumes created for the default
1840
+ * Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
1841
+ * Create the volume at the size you need, or copy the data to a new, larger one.
1842
+ *
1843
+ * On a container volume the change is in place and online: the size is a quota,
1844
+ * so no data is copied and a mounted sandbox sees the new size immediately. Both
1845
+ * growing and shrinking are allowed, but a shrink must leave at least 10% of the
1846
+ * new size free — otherwise it throws `ConflictError` with
1847
+ * `code: 'volume_too_full_to_shrink'`.
1823
1848
  *
1824
1849
  * Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
1825
1850
  * `volume_resize_in_progress`, `volume_not_provisioned`,
@@ -1845,7 +1870,7 @@ var Volume = class _Volume {
1845
1870
  if (!res.ok) await handleApiError(res);
1846
1871
  return res.json();
1847
1872
  }
1848
- /** Resize this volume in place — see the static {@link Volume.resize}. */
1873
+ /** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
1849
1874
  async resize(projectId, sizeGb) {
1850
1875
  return _Volume.resize(projectId, this.volumeId, sizeGb, {
1851
1876
  apiKey: this.config.apiKey,
@@ -1968,7 +1993,7 @@ var Execution = class {
1968
1993
  this.stderr = "";
1969
1994
  /** Rich output items (images, JSON, HTML, plain values). */
1970
1995
  this.results = [];
1971
- /** Monotonically increasing counter for this context. */
1996
+ /** Monotonically increasing counter for this context (the Python kernel's `In[n]`). */
1972
1997
  this.executionCount = 0;
1973
1998
  }
1974
1999
  get success() {
@@ -1979,25 +2004,317 @@ var Execution = class {
1979
2004
  }
1980
2005
  };
1981
2006
 
2007
+ // src/code-interpreter/runner.ts
2008
+ var RUNNER_SOURCE = String.raw`# Lizard code runner: executes code for CodeSandbox.run_code / runCode inside the sandbox.
2009
+ # Python runs in a persistent Jupyter kernel per context when the template has one
2010
+ # (the interpreter template does); everything else runs as a fresh process.
2011
+ # Events are JSON lines on stdout; an event over 24000 chars is sent as "part" lines.
2012
+ import base64, json, os, re, shutil, signal, subprocess, sys, threading, time
2013
+ ROOT = "/tmp/.lizard-code"
2014
+ LANGS = {"python": "python3", "javascript": "node", "bash": "bash"}
2015
+ ALIASES = {"py": "python", "python3": "python", "js": "javascript", "node": "javascript", "sh": "bash"}
2016
+ ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
2017
+ LOCK = threading.Lock()
2018
+
2019
+
2020
+ def emit(ev):
2021
+ s = json.dumps(ev)
2022
+ with LOCK:
2023
+ if len(s) <= 24000:
2024
+ print(s, flush=True)
2025
+ return
2026
+ n = (len(s) + 23999) // 24000
2027
+ for i in range(n):
2028
+ print(json.dumps({"type": "part", "data": s[i * 24000:(i + 1) * 24000], "end": i == n - 1}), flush=True)
2029
+
2030
+
2031
+ def fail(msg):
2032
+ emit({"type": "fatal", "message": msg})
2033
+ sys.exit(3)
2034
+
2035
+
2036
+ def meta_path(ctx):
2037
+ return os.path.join(ROOT, ctx, "meta.json")
2038
+
2039
+
2040
+ def load(ctx):
2041
+ try:
2042
+ with open(meta_path(ctx)) as f:
2043
+ return json.load(f)
2044
+ except OSError:
2045
+ return None
2046
+
2047
+
2048
+ def save(m):
2049
+ os.makedirs(os.path.join(ROOT, m["id"]), exist_ok=True)
2050
+ with open(meta_path(m["id"]), "w") as f:
2051
+ json.dump(m, f)
2052
+
2053
+
2054
+ def alive(pid):
2055
+ try:
2056
+ os.kill(pid, 0)
2057
+ except OSError:
2058
+ return False
2059
+ try:
2060
+ with open("/proc/%d/stat" % pid) as f:
2061
+ return f.read().split(") ")[-1][:1] != "Z"
2062
+ except OSError:
2063
+ return False
2064
+
2065
+
2066
+ def kernel_pid(m):
2067
+ try:
2068
+ with open(os.path.join(ROOT, m["id"], "kernel.pid")) as f:
2069
+ return int(f.read().strip())
2070
+ except (OSError, ValueError):
2071
+ return 0
2072
+
2073
+
2074
+ def stop_kernel(m):
2075
+ pid = kernel_pid(m)
2076
+ if pid and alive(pid):
2077
+ try:
2078
+ os.killpg(pid, signal.SIGKILL)
2079
+ except OSError:
2080
+ pass
2081
+ for name in ("kernel.pid", "kernel.json"):
2082
+ try:
2083
+ os.remove(os.path.join(ROOT, m["id"], name))
2084
+ except OSError:
2085
+ pass
2086
+
2087
+
2088
+ def start_kernel(m):
2089
+ d = os.path.join(ROOT, m["id"])
2090
+ conn = os.path.join(d, "kernel.json")
2091
+ stop_kernel(m)
2092
+ os.makedirs(m["cwd"], exist_ok=True)
2093
+ log = open(os.path.join(d, "kernel.log"), "ab")
2094
+ p = subprocess.Popen([sys.executable, "-m", "ipykernel_launcher", "-f", conn], cwd=m["cwd"],
2095
+ stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True,
2096
+ env=dict(os.environ, PYTHONUNBUFFERED="1"))
2097
+ with open(os.path.join(d, "kernel.pid"), "w") as f:
2098
+ f.write(str(p.pid))
2099
+ end = time.time() + 30
2100
+ while time.time() < end:
2101
+ if os.path.exists(conn) and os.path.getsize(conn) > 0:
2102
+ return
2103
+ if p.poll() is not None:
2104
+ break
2105
+ time.sleep(0.05)
2106
+ with open(os.path.join(d, "kernel.log"), "rb") as f:
2107
+ tail = f.read()[-2000:].decode("utf-8", "replace")
2108
+ fail("python kernel failed to start: " + (tail or "no output"))
2109
+
2110
+
2111
+ def has_kernel():
2112
+ try:
2113
+ import jupyter_client, ipykernel # noqa: F401
2114
+ return True
2115
+ except ImportError:
2116
+ return False
2117
+
2118
+
2119
+ def client(m):
2120
+ from jupyter_client import BlockingKernelClient
2121
+ if not (kernel_pid(m) and alive(kernel_pid(m))):
2122
+ start_kernel(m)
2123
+ kc = BlockingKernelClient()
2124
+ kc.load_connection_file(os.path.join(ROOT, m["id"], "kernel.json"))
2125
+ kc.start_channels()
2126
+ kc.wait_for_ready(timeout=30)
2127
+ return kc
2128
+
2129
+
2130
+ def shell_reply(kc, msg_id, timeout):
2131
+ end = time.time() + timeout
2132
+ while time.time() < end:
2133
+ try:
2134
+ r = kc.get_shell_msg(timeout=max(0.01, end - time.time()))
2135
+ except Exception:
2136
+ return None
2137
+ if r["parent_header"].get("msg_id") == msg_id:
2138
+ return r
2139
+ return None
2140
+
2141
+
2142
+ def silent(kc, code):
2143
+ shell_reply(kc, kc.execute(code, silent=True, store_history=False), 10)
2144
+
2145
+
2146
+ BEST = ("image/png", "image/jpeg", "image/svg+xml", "text/html", "text/markdown", "application/json", "text/latex", "text/plain")
2147
+
2148
+
2149
+ def run_python(m, code, envs, timeout):
2150
+ kc = client(m)
2151
+ if envs:
2152
+ silent(kc, "import os as _lz_os\n_lz_saved = {k: _lz_os.environ.get(k) for k in %r}\n_lz_os.environ.update(%r)" % (list(envs), envs))
2153
+ msg_id = kc.execute(code, store_history=True, allow_stdin=False)
2154
+ end = time.time() + timeout
2155
+ timed_out = False
2156
+ while True:
2157
+ left = end - time.time()
2158
+ if left <= 0:
2159
+ timed_out = True
2160
+ break
2161
+ try:
2162
+ msg = kc.get_iopub_msg(timeout=left)
2163
+ except Exception:
2164
+ continue
2165
+ if msg["parent_header"].get("msg_id") != msg_id:
2166
+ continue
2167
+ t, c = msg["msg_type"], msg["content"]
2168
+ if t == "stream":
2169
+ emit({"type": "stderr" if c.get("name") == "stderr" else "stdout", "data": c.get("text", "")})
2170
+ elif t in ("execute_result", "display_data"):
2171
+ data = c.get("data", {})
2172
+ mime = next((k for k in BEST if k in data), next(iter(data), None))
2173
+ if mime:
2174
+ v = data[mime]
2175
+ emit({"type": "result", "mime": mime, "data": v if isinstance(v, str) else json.dumps(v)})
2176
+ elif t == "error":
2177
+ emit({"type": "error", "name": c.get("ename", "Error"), "message": c.get("evalue", ""),
2178
+ "traceback": ANSI.sub("", "\n".join(c.get("traceback", [])))})
2179
+ elif t == "status" and c.get("execution_state") == "idle":
2180
+ break
2181
+ count = 0
2182
+ if timed_out:
2183
+ pid = kernel_pid(m)
2184
+ if pid:
2185
+ try:
2186
+ os.kill(pid, signal.SIGINT)
2187
+ except OSError:
2188
+ pass
2189
+ emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs; the kernel was interrupted" % timeout, "traceback": ""})
2190
+ else:
2191
+ r = shell_reply(kc, msg_id, 10)
2192
+ count = (r or {}).get("content", {}).get("execution_count") or 0
2193
+ if envs and not timed_out:
2194
+ silent(kc, "for _lz_k, _lz_v in _lz_saved.items():\n _lz_os.environ.pop(_lz_k, None) if _lz_v is None else _lz_os.environ.__setitem__(_lz_k, _lz_v)")
2195
+ kc.stop_channels()
2196
+ emit({"type": "done", "execution_count": count})
2197
+
2198
+
2199
+ def run_process(m, code, envs, timeout):
2200
+ interp = LANGS[m["language"]]
2201
+ if shutil.which(interp) is None:
2202
+ emit({"type": "error", "name": "LanguageUnavailable", "message": "%s is not installed in this sandbox" % interp, "traceback": ""})
2203
+ emit({"type": "done", "execution_count": 0})
2204
+ return
2205
+ d = os.path.join(ROOT, m["id"])
2206
+ src = os.path.join(d, "cell" + {"node": ".js", "bash": ".sh"}.get(interp, ".py"))
2207
+ with open(src, "w") as f:
2208
+ f.write(code)
2209
+ os.makedirs(m["cwd"], exist_ok=True)
2210
+ p = subprocess.Popen([interp, src], cwd=m["cwd"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
2211
+ stderr=subprocess.PIPE, env=dict(os.environ, **(envs or {})), start_new_session=True)
2212
+ err_tail = []
2213
+
2214
+ def pump(stream, kind):
2215
+ for line in iter(stream.readline, b""):
2216
+ text = line.decode("utf-8", "replace")
2217
+ if kind == "stderr":
2218
+ err_tail.append(text)
2219
+ del err_tail[:-50]
2220
+ emit({"type": kind, "data": text})
2221
+
2222
+ ts = [threading.Thread(target=pump, args=(p.stdout, "stdout")), threading.Thread(target=pump, args=(p.stderr, "stderr"))]
2223
+ for t in ts:
2224
+ t.start()
2225
+ try:
2226
+ rc = p.wait(timeout=timeout)
2227
+ except subprocess.TimeoutExpired:
2228
+ os.killpg(p.pid, signal.SIGKILL)
2229
+ rc = None
2230
+ for t in ts:
2231
+ t.join(5)
2232
+ m["count"] = m.get("count", 0) + 1
2233
+ save(m)
2234
+ if rc is None:
2235
+ emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs" % timeout, "traceback": ""})
2236
+ elif rc != 0:
2237
+ emit({"type": "error", "name": "ExitError", "message": "exited with code %d" % rc, "traceback": "".join(err_tail)})
2238
+ emit({"type": "done", "execution_count": m["count"]})
2239
+
2240
+
2241
+ def ctx_for(a):
2242
+ lang = (a.get("language") or "python").lower()
2243
+ lang = ALIASES.get(lang, lang)
2244
+ if lang not in LANGS:
2245
+ fail("unsupported language %r; use python, javascript or bash" % lang)
2246
+ return lang
2247
+
2248
+
2249
+ def main():
2250
+ a = json.loads(base64.b64decode(sys.argv[1]))
2251
+ act = a["action"]
2252
+ os.makedirs(ROOT, exist_ok=True)
2253
+ if act == "create" or (act == "run" and not a.get("context")):
2254
+ lang = ctx_for(a)
2255
+ cid = a.get("context") or ("default-" + lang)
2256
+ m = load(cid) if act == "run" else None
2257
+ if m is None:
2258
+ m = {"id": cid if act == "run" else a["id"], "language": lang, "cwd": a.get("cwd") or "/home/user"}
2259
+ save(m)
2260
+ if lang == "python" and act == "create" and has_kernel():
2261
+ client(m).stop_channels()
2262
+ if act == "create":
2263
+ emit({"type": "context", "id": m["id"], "language": m["language"], "cwd": m["cwd"]})
2264
+ return
2265
+ elif act == "list":
2266
+ out = []
2267
+ for name in sorted(os.listdir(ROOT)):
2268
+ m = load(name)
2269
+ if m and not name.startswith("default-"):
2270
+ out.append({"id": m["id"], "language": m["language"], "cwd": m["cwd"]})
2271
+ emit({"type": "contexts", "data": out})
2272
+ return
2273
+ else:
2274
+ m = load(a["context"])
2275
+ if m is None:
2276
+ fail("context %r not found" % a["context"])
2277
+ if act == "delete":
2278
+ stop_kernel(m)
2279
+ shutil.rmtree(os.path.join(ROOT, m["id"]), ignore_errors=True)
2280
+ emit({"type": "ok"})
2281
+ return
2282
+ if act == "restart":
2283
+ if m["language"] == "python" and has_kernel():
2284
+ start_kernel(m)
2285
+ m["count"] = 0
2286
+ save(m)
2287
+ emit({"type": "ok"})
2288
+ return
2289
+ code = a.get("code")
2290
+ if code is None:
2291
+ with open(a["codeFile"]) as f:
2292
+ code = f.read()
2293
+ os.remove(a["codeFile"])
2294
+ if m["language"] == "python" and has_kernel():
2295
+ run_python(m, code, a.get("envs") or {}, a.get("timeout") or 60)
2296
+ else:
2297
+ run_process(m, code, a.get("envs") or {}, a.get("timeout") or 60)
2298
+
2299
+
2300
+ main()
2301
+ `;
2302
+
1982
2303
  // src/code-interpreter/index.ts
1983
- var CODE_INTERPRETER_PORT = 8080;
2304
+ var DEFAULT_RUN_TIMEOUT_MS = 6e4;
2305
+ var RUNNER_OVERHEAD_MS = 45e3;
2306
+ var MAX_INLINE_ARGS = 1e5;
1984
2307
  var CodeSandbox = class extends Sandbox {
1985
- get serverUrl() {
1986
- return this.getHost(CODE_INTERPRETER_PORT).then((h) => `https://${h}`);
1987
- }
1988
2308
  /**
1989
- * Execute code in a persistent kernel.
2309
+ * Execute code and wait for it to finish.
1990
2310
  *
1991
- * Variables, imports, and function definitions from previous calls are
1992
- * available in subsequent calls within the same context.
2311
+ * Python keeps its state per context (the default context, per language,
2312
+ * when none is given). Code errors appear in `execution.error`; request
2313
+ * failures throw.
1993
2314
  *
1994
2315
  * @param code Source code to run.
1995
2316
  * @param opts Language, context, env vars, timeout, and streaming callbacks.
1996
2317
  *
1997
- * @returns Execution result with stdout, stderr, results, and any error.
1998
- *
1999
- * Code errors appear in `execution.error`; request failures throw.
2000
- *
2001
2318
  * @example
2002
2319
  * ```ts
2003
2320
  * const result = await sandbox.runCode(`
@@ -2011,63 +2328,42 @@ var CodeSandbox = class extends Sandbox {
2011
2328
  if (opts?.context && opts?.language) {
2012
2329
  throw new Error("Provide context or language, not both");
2013
2330
  }
2014
- const body = { code, env_vars: opts?.envs ?? {} };
2015
- if (opts?.context) body.context_id = opts.context.id;
2016
- else if (opts?.language) body.language = opts.language;
2017
- const controller = new AbortController();
2018
- const timer = opts?.timeoutMs ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
2019
- try {
2020
- const res = await fetch(`${await this.serverUrl}/execute`, {
2021
- method: "POST",
2022
- headers: { "Content-Type": "application/json" },
2023
- body: JSON.stringify(body),
2024
- signal: controller.signal
2025
- });
2026
- if (!res.ok) {
2027
- throw new Error(`Code interpreter returned ${res.status}: ${await res.text()}`);
2028
- }
2029
- if (!res.body) throw new Error("Empty response body");
2030
- const execution = new Execution();
2031
- for await (const line of readLines(res.body)) {
2032
- let item;
2033
- try {
2034
- item = JSON.parse(line);
2035
- } catch {
2036
- continue;
2037
- }
2038
- if (item.type === "stdout") {
2039
- execution.stdout += item.data;
2040
- opts?.onStdout?.(item.data);
2041
- } else if (item.type === "stderr") {
2042
- execution.stderr += item.data;
2043
- opts?.onStderr?.(item.data);
2044
- } else if (item.type === "result") {
2045
- const out = item;
2046
- execution.results.push(out);
2047
- opts?.onResult?.(out);
2048
- } else if (item.type === "error") {
2049
- const err = new ExecutionError(
2050
- item.name,
2051
- item.message,
2052
- item.traceback
2053
- );
2054
- execution.error = err;
2055
- opts?.onError?.(err);
2056
- } else if (item.type === "done") {
2057
- execution.executionCount = item.execution_count ?? 0;
2058
- break;
2059
- }
2331
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
2332
+ const args = {
2333
+ action: "run",
2334
+ code,
2335
+ envs: opts?.envs ?? {},
2336
+ timeout: timeoutMs / 1e3
2337
+ };
2338
+ if (opts?.context) args.context = opts.context.id;
2339
+ else args.language = opts?.language ?? "python";
2340
+ const execution = new Execution();
2341
+ await this.runner(args, timeoutMs, (item) => {
2342
+ if (item.type === "stdout") {
2343
+ execution.stdout += item.data;
2344
+ opts?.onStdout?.(item.data);
2345
+ } else if (item.type === "stderr") {
2346
+ execution.stderr += item.data;
2347
+ opts?.onStderr?.(item.data);
2348
+ } else if (item.type === "result") {
2349
+ const out = { type: "result", mime: item.mime, data: item.data };
2350
+ execution.results.push(out);
2351
+ opts?.onResult?.(out);
2352
+ } else if (item.type === "error") {
2353
+ const err = new ExecutionError(item.name, item.message, item.traceback);
2354
+ execution.error = err;
2355
+ opts?.onError?.(err);
2356
+ } else if (item.type === "done") {
2357
+ execution.executionCount = item.execution_count ?? 0;
2060
2358
  }
2061
- return execution;
2062
- } finally {
2063
- clearTimeout(timer);
2064
- }
2359
+ });
2360
+ return execution;
2065
2361
  }
2066
2362
  /**
2067
2363
  * Create a new isolated execution context.
2068
2364
  *
2069
- * Each context maintains its own variable namespace and process state.
2070
- * Useful for running multiple independent sessions in the same sandbox.
2365
+ * A Python context is its own kernel: its own variables, and `cwd` as its
2366
+ * working directory. Other languages get the working directory only.
2071
2367
  *
2072
2368
  * @example
2073
2369
  * ```ts
@@ -2077,40 +2373,80 @@ var CodeSandbox = class extends Sandbox {
2077
2373
  * ```
2078
2374
  */
2079
2375
  async createContext(opts) {
2080
- const res = await fetch(`${await this.serverUrl}/contexts`, {
2081
- method: "POST",
2082
- headers: { "Content-Type": "application/json" },
2083
- body: JSON.stringify({
2084
- language: opts?.language ?? "python",
2085
- cwd: opts?.cwd ?? "/home/user"
2086
- })
2087
- });
2088
- if (!res.ok) throw new Error(`Failed to create context: ${await res.text()}`);
2089
- return res.json();
2376
+ const id = `ctx-${randomId()}`;
2377
+ let ctx;
2378
+ await this.runner(
2379
+ { action: "create", id, language: opts?.language ?? "python", cwd: opts?.cwd ?? "/home/user" },
2380
+ DEFAULT_RUN_TIMEOUT_MS,
2381
+ (ev) => {
2382
+ if (ev.type === "context") ctx = { id: ev.id, language: ev.language, cwd: ev.cwd };
2383
+ }
2384
+ );
2385
+ if (!ctx) throw new LizardError("Failed to create context");
2386
+ return ctx;
2090
2387
  }
2091
- /**
2092
- * List all active execution contexts in this sandbox.
2093
- */
2388
+ /** List the contexts created with {@link createContext} in this sandbox. */
2094
2389
  async listContexts() {
2095
- const res = await fetch(`${await this.serverUrl}/contexts`);
2096
- if (!res.ok) throw new Error(`Failed to list contexts: ${await res.text()}`);
2097
- return res.json();
2390
+ let out = [];
2391
+ await this.runner({ action: "list" }, DEFAULT_RUN_TIMEOUT_MS, (ev) => {
2392
+ if (ev.type === "contexts") out = ev.data;
2393
+ });
2394
+ return out;
2098
2395
  }
2099
- /**
2100
- * Delete an execution context and free its resources.
2101
- */
2396
+ /** Delete an execution context, stopping its kernel. */
2102
2397
  async deleteContext(context) {
2103
2398
  const id = typeof context === "string" ? context : context.id;
2104
- const res = await fetch(`${await this.serverUrl}/contexts/${id}`, { method: "DELETE" });
2105
- if (!res.ok) throw new Error(`Failed to delete context: ${await res.text()}`);
2399
+ await this.runner({ action: "delete", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
2400
+ });
2106
2401
  }
2107
- /**
2108
- * Restart a context, clearing all variables and state.
2109
- */
2402
+ /** Restart a context, clearing all variables and state. */
2110
2403
  async restartContext(context) {
2111
2404
  const id = typeof context === "string" ? context : context.id;
2112
- const res = await fetch(`${await this.serverUrl}/contexts/${id}/restart`, { method: "POST" });
2113
- if (!res.ok) throw new Error(`Failed to restart context: ${await res.text()}`);
2405
+ await this.runner({ action: "restart", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
2406
+ });
2407
+ }
2408
+ /** Run the in-sandbox runner with `args` and hand each of its events to `onEvent`. */
2409
+ async runner(args, timeoutMs, onEvent) {
2410
+ let encoded = toBase64(JSON.stringify(args));
2411
+ if (encoded.length > MAX_INLINE_ARGS && typeof args.code === "string") {
2412
+ const codeFile = `/tmp/.lizard-code/in-${randomId()}`;
2413
+ await this.fs.write(codeFile, args.code);
2414
+ const { code: _code, ...rest } = args;
2415
+ encoded = toBase64(JSON.stringify({ ...rest, codeFile }));
2416
+ }
2417
+ let part = "";
2418
+ let fatal;
2419
+ let sawEvent = false;
2420
+ const stderr = [];
2421
+ const result = await this.process.exec(["python3", "-c", RUNNER_SOURCE, encoded], {
2422
+ timeoutMs: timeoutMs + RUNNER_OVERHEAD_MS,
2423
+ onStdout: (line) => {
2424
+ let ev;
2425
+ try {
2426
+ ev = JSON.parse(line);
2427
+ if (ev.type === "part") {
2428
+ part += ev.data;
2429
+ if (!ev.end) return;
2430
+ ev = JSON.parse(part);
2431
+ part = "";
2432
+ }
2433
+ } catch {
2434
+ part = "";
2435
+ return;
2436
+ }
2437
+ sawEvent = true;
2438
+ if (ev.type === "fatal") fatal = ev.message;
2439
+ else onEvent(ev);
2440
+ },
2441
+ onStderr: (line) => {
2442
+ stderr.push(line);
2443
+ }
2444
+ });
2445
+ if (fatal) throw new LizardError(fatal);
2446
+ if (result.exitCode !== 0 || !sawEvent) {
2447
+ const detail = stderr.slice(-20).join("\n").trim() || `exit code ${result.exitCode}`;
2448
+ throw new LizardError(`Code runner failed (python3 is required in the sandbox): ${detail}`);
2449
+ }
2114
2450
  }
2115
2451
  static async create(templateOrOpts, opts) {
2116
2452
  return super.create(templateOrOpts, opts);
@@ -2119,27 +2455,83 @@ var CodeSandbox = class extends Sandbox {
2119
2455
  return super.connect(sandboxId, opts);
2120
2456
  }
2121
2457
  };
2122
- CodeSandbox.defaultTemplate = "code-interpreter-v1";
2123
- async function* readLines(body) {
2124
- const reader = body.getReader();
2125
- const decoder = new TextDecoder();
2126
- let buf = "";
2127
- try {
2128
- while (true) {
2129
- const { done, value } = await reader.read();
2130
- if (done) break;
2131
- buf += decoder.decode(value, { stream: true });
2132
- const lines = buf.split("\n");
2133
- buf = lines.pop() ?? "";
2134
- for (const line of lines) {
2135
- if (line.trim()) yield line;
2458
+ CodeSandbox.defaultTemplate = "interpreter";
2459
+ function randomId() {
2460
+ const c = globalThis.crypto;
2461
+ return c?.randomUUID ? c.randomUUID().replace(/-/g, "").slice(0, 16) : Math.random().toString(16).slice(2, 18);
2462
+ }
2463
+ function toBase64(text) {
2464
+ if (typeof Buffer !== "undefined") return Buffer.from(text, "utf8").toString("base64");
2465
+ const bytes = new TextEncoder().encode(text);
2466
+ let s = "";
2467
+ for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
2468
+ return btoa(s);
2469
+ }
2470
+
2471
+ // src/cli.ts
2472
+ var import_node_child_process = require("child_process");
2473
+ var LizardCLI = class {
2474
+ constructor(opts = {}) {
2475
+ this.opts = opts;
2476
+ }
2477
+ run(args, opts = {}) {
2478
+ return new Promise((resolve, reject) => {
2479
+ const env = { ...process.env };
2480
+ if (this.opts.apiKey) {
2481
+ env.LIZARD_API_KEY = this.opts.apiKey;
2482
+ env.LIZARD_TOKEN = this.opts.apiKey;
2136
2483
  }
2137
- }
2138
- if (buf.trim()) yield buf;
2139
- } finally {
2140
- reader.releaseLock();
2484
+ if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
2485
+ const child = (0, import_node_child_process.spawn)(this.opts.executable ?? "lizard", ["--json", ...args], {
2486
+ cwd: this.opts.cwd,
2487
+ env,
2488
+ shell: false,
2489
+ stdio: "pipe",
2490
+ signal: opts.signal,
2491
+ timeout: this.opts.timeoutMs ?? 6e5
2492
+ });
2493
+ child.stdout.setEncoding("utf8");
2494
+ child.stderr.setEncoding("utf8");
2495
+ let stdout = "", stderr = "", bytes = 0;
2496
+ const collect = (stream, data) => {
2497
+ bytes += Buffer.byteLength(data);
2498
+ if (bytes > 16 * 1024 * 1024) {
2499
+ child.kill();
2500
+ reject(new Error("CLI output exceeds 16 MiB"));
2501
+ return;
2502
+ }
2503
+ if (stream === "stdout") stdout += data;
2504
+ else stderr += data;
2505
+ };
2506
+ child.stdout.on("data", (data) => collect("stdout", data));
2507
+ child.stderr.on("data", (data) => collect("stderr", data));
2508
+ child.on("error", reject);
2509
+ child.on("close", (code, signal) => {
2510
+ if (signal) {
2511
+ reject(new Error(`CLI ended with signal ${signal}`));
2512
+ return;
2513
+ }
2514
+ let events = [];
2515
+ try {
2516
+ events = [JSON.parse(stdout)];
2517
+ } catch {
2518
+ events = stdout.split("\n").filter(Boolean).flatMap((line) => {
2519
+ try {
2520
+ return [JSON.parse(line)];
2521
+ } catch {
2522
+ return [];
2523
+ }
2524
+ });
2525
+ }
2526
+ resolve({ code: code ?? 1, stdout, stderr, events });
2527
+ });
2528
+ child.stdin.on("error", (error) => {
2529
+ if (error.code !== "EPIPE") reject(error);
2530
+ });
2531
+ child.stdin.end(opts.stdin);
2532
+ });
2141
2533
  }
2142
- }
2534
+ };
2143
2535
  // Annotate the CommonJS export names for ESM import in node:
2144
2536
  0 && (module.exports = {
2145
2537
  AddonsAPI,
@@ -2158,6 +2550,7 @@ async function* readLines(body) {
2158
2550
  LizardError,
2159
2551
  MetricsAPI,
2160
2552
  NotFoundError,
2553
+ PaymentRequiredError,
2161
2554
  PlatformClient,
2162
2555
  ProjectsAPI,
2163
2556
  RegionsAPI,