@kosa8/sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -33,7 +33,12 @@ __export(index_exports, {
33
33
  ACTOR_HEADER: () => ACTOR_HEADER,
34
34
  AGENT_TOKEN_HEADER: () => AGENT_TOKEN_HEADER,
35
35
  APPROVAL_HEADER: () => APPROVAL_HEADER,
36
+ CLOUD_KEY_PREFIX: () => CLOUD_KEY_PREFIX,
36
37
  CheckpointBook: () => CheckpointBook,
38
+ CloudFunctions: () => CloudFunctions,
39
+ CloudSandbox: () => CloudSandbox,
40
+ CloudSandboxes: () => CloudSandboxes,
41
+ DEFAULT_CLOUD_URL: () => DEFAULT_CLOUD_URL,
37
42
  FileCheckpointStore: () => FileCheckpointStore,
38
43
  Kosa8ApiError: () => Kosa8ApiError,
39
44
  Kosa8ApprovalRequiredError: () => Kosa8ApprovalRequiredError,
@@ -41,20 +46,26 @@ __export(index_exports, {
41
46
  Kosa8BadRequestError: () => Kosa8BadRequestError,
42
47
  Kosa8CheckpointError: () => Kosa8CheckpointError,
43
48
  Kosa8Client: () => Kosa8Client,
49
+ Kosa8Cloud: () => Kosa8Cloud,
44
50
  Kosa8ConflictError: () => Kosa8ConflictError,
45
51
  Kosa8ConnectionError: () => Kosa8ConnectionError,
46
52
  Kosa8Error: () => Kosa8Error,
47
53
  Kosa8NotFoundError: () => Kosa8NotFoundError,
54
+ Kosa8PaymentRequiredError: () => Kosa8PaymentRequiredError,
48
55
  Kosa8PolicyError: () => Kosa8PolicyError,
56
+ Kosa8RateLimitError: () => Kosa8RateLimitError,
49
57
  Kosa8StreamError: () => Kosa8StreamError,
58
+ Kosa8TimeoutError: () => Kosa8TimeoutError,
50
59
  Kosa8UnsupportedError: () => Kosa8UnsupportedError,
51
60
  MAX_CHECKPOINTS_PER_SANDBOX: () => MAX_CHECKPOINTS_PER_SANDBOX,
52
61
  MemoryCheckpointStore: () => MemoryCheckpointStore,
53
62
  SandboxHandle: () => SandboxHandle,
54
63
  Transport: () => Transport,
55
64
  defaultSocketPath: () => defaultSocketPath,
65
+ defaultWorkerFile: () => defaultWorkerFile,
56
66
  errorFromResponse: () => errorFromResponse,
57
67
  resolveEndpoint: () => resolveEndpoint,
68
+ tarContext: () => tarContext,
58
69
  validateSandboxName: () => validateSandboxName
59
70
  });
60
71
  module.exports = __toCommonJS(index_exports);
@@ -76,6 +87,8 @@ var Kosa8Error = class extends Error {
76
87
  path;
77
88
  /** The decoded response body, when there was one. */
78
89
  body;
90
+ /** kosa8 Cloud's stable error code (payment_required, quota_exceeded, ...). */
91
+ code;
79
92
  constructor(message, options = {}) {
80
93
  super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
81
94
  this.name = new.target.name;
@@ -85,10 +98,13 @@ var Kosa8Error = class extends Error {
85
98
  this.method = options.method;
86
99
  this.path = options.path;
87
100
  this.body = options.body;
101
+ this.code = options.code;
88
102
  }
89
103
  };
90
104
  var Kosa8ConnectionError = class extends Kosa8Error {
91
105
  };
106
+ var Kosa8TimeoutError = class extends Kosa8ConnectionError {
107
+ };
92
108
  var Kosa8ApiError = class extends Kosa8Error {
93
109
  };
94
110
  var Kosa8BadRequestError = class extends Kosa8ApiError {
@@ -119,6 +135,10 @@ var Kosa8StreamError = class extends Kosa8Error {
119
135
  this.stderr = out.stderr;
120
136
  }
121
137
  };
138
+ var Kosa8PaymentRequiredError = class extends Kosa8ApiError {
139
+ };
140
+ var Kosa8RateLimitError = class extends Kosa8ApiError {
141
+ };
122
142
  var Kosa8CheckpointError = class extends Kosa8Error {
123
143
  };
124
144
  function isRefusal(v) {
@@ -152,8 +172,12 @@ function errorFromResponse(status, body, ctx = {}) {
152
172
  return new Kosa8PolicyError(message, opts);
153
173
  case 404:
154
174
  return new Kosa8NotFoundError(message, opts);
175
+ case 402:
176
+ return new Kosa8PaymentRequiredError(message, opts);
155
177
  case 409:
156
178
  return new Kosa8ConflictError(message, opts);
179
+ case 429:
180
+ return new Kosa8RateLimitError(message, opts);
157
181
  case 501:
158
182
  return new Kosa8UnsupportedError(message, opts);
159
183
  default:
@@ -331,16 +355,136 @@ var CheckpointBook = class {
331
355
  }
332
356
  };
333
357
 
358
+ // src/tar.ts
359
+ var import_node_fs2 = require("fs");
360
+ var path2 = __toESM(require("path"), 1);
361
+ var BLOCK = 512;
362
+ function octal(buf, off, len, n) {
363
+ buf.write(n.toString(8).padStart(len - 1, "0") + "\0", off, len, "ascii");
364
+ }
365
+ function header(name, type, size, mode, mtime, link = "") {
366
+ const h = Buffer.alloc(BLOCK);
367
+ h.write(name, 0, 100, "utf8");
368
+ octal(h, 100, 8, mode & 4095);
369
+ octal(h, 108, 8, 0);
370
+ octal(h, 116, 8, 0);
371
+ octal(h, 124, 12, size);
372
+ octal(h, 136, 12, mtime);
373
+ h.fill(" ", 148, 156);
374
+ h.write(type, 156, 1, "ascii");
375
+ h.write(link, 157, 100, "utf8");
376
+ h.write("ustar\0", 257, 6, "ascii");
377
+ h.write("00", 263, 2, "ascii");
378
+ let sum = 0;
379
+ for (const b of h) sum += b;
380
+ h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
381
+ return h;
382
+ }
383
+ function paxRecord(key, value) {
384
+ const body = ` ${key}=${value}
385
+ `;
386
+ let n = Buffer.byteLength(body);
387
+ let len = n + String(n).length;
388
+ if (String(len).length !== String(n).length) len = n + String(len).length;
389
+ return `${len}${body}`;
390
+ }
391
+ function pad(n) {
392
+ const r = n % BLOCK;
393
+ return Buffer.alloc(r === 0 ? 0 : BLOCK - r);
394
+ }
395
+ function entry(out, name, type, mode, mtime, data, link = "") {
396
+ let pax = "";
397
+ if (Buffer.byteLength(name) > 100) pax += paxRecord("path", name);
398
+ if (Buffer.byteLength(link) > 100) pax += paxRecord("linkpath", link);
399
+ if (pax !== "") {
400
+ const p = Buffer.from(pax, "utf8");
401
+ out.push(header("PaxHeader", "x", p.length, 420, mtime), p, pad(p.length));
402
+ }
403
+ const size = data?.length ?? 0;
404
+ out.push(header(name.slice(0, 100), type, size, mode, mtime, link.slice(0, 100)));
405
+ if (data) out.push(data, pad(size));
406
+ }
407
+ async function readIgnore(dir) {
408
+ let text;
409
+ try {
410
+ text = await import_node_fs2.promises.readFile(path2.join(dir, ".dockerignore"), "utf8");
411
+ } catch {
412
+ return [];
413
+ }
414
+ const out = [];
415
+ for (const line of text.split(/\r?\n/)) {
416
+ const l = line.trim();
417
+ if (l === "" || line.startsWith("#")) continue;
418
+ out.push(path2.posix.normalize(l.replace(/\\/g, "/")).replace(/\/+$/, "") || ".");
419
+ }
420
+ return out;
421
+ }
422
+ async function tarContext(dir, dockerfile = "Dockerfile") {
423
+ try {
424
+ await import_node_fs2.promises.access(path2.join(dir, dockerfile));
425
+ } catch {
426
+ throw new Kosa8Error(`no ${dockerfile} in ${dir}`);
427
+ }
428
+ const ignore = await readIgnore(dir);
429
+ const skip = (rel) => ignore.some((p) => rel === p || rel.startsWith(p + "/"));
430
+ const out = [];
431
+ const walk = async (rel) => {
432
+ const ents = await import_node_fs2.promises.readdir(path2.join(dir, rel), { withFileTypes: true });
433
+ ents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
434
+ const subdirs = [];
435
+ for (const e of ents) {
436
+ const r = rel === "" ? e.name : `${rel}/${e.name}`;
437
+ if (skip(r)) continue;
438
+ const full = path2.join(dir, r);
439
+ const st = await import_node_fs2.promises.lstat(full);
440
+ const mtime = Math.floor(st.mtimeMs / 1e3);
441
+ if (st.isSymbolicLink()) {
442
+ entry(out, r, "2", st.mode, mtime, void 0, await import_node_fs2.promises.readlink(full));
443
+ } else if (st.isDirectory()) {
444
+ entry(out, r + "/", "5", st.mode, mtime);
445
+ subdirs.push(r);
446
+ } else if (st.isFile()) {
447
+ entry(out, r, "0", st.mode, mtime, await import_node_fs2.promises.readFile(full));
448
+ }
449
+ }
450
+ for (const s of subdirs) await walk(s);
451
+ };
452
+ await walk("");
453
+ out.push(Buffer.alloc(BLOCK * 2));
454
+ return Buffer.concat(out);
455
+ }
456
+
334
457
  // src/transport.ts
458
+ var fs2 = __toESM(require("fs"), 1);
335
459
  var http = __toESM(require("http"), 1);
336
460
  var https = __toESM(require("https"), 1);
337
461
  var os2 = __toESM(require("os"), 1);
338
- var path2 = __toESM(require("path"), 1);
462
+ var path3 = __toESM(require("path"), 1);
339
463
  var APPROVAL_HEADER = "X-Kosa8-Approval";
340
464
  var ACTOR_HEADER = "X-Kosa8-Actor";
341
465
  var AGENT_TOKEN_HEADER = "X-Kosa8-Agent-Token";
342
466
  function defaultSocketPath() {
343
- return path2.join(os2.homedir(), ".kosa8", "run", "kosa8.sock");
467
+ return path3.join(os2.homedir(), ".kosa8", "run", "kosa8.sock");
468
+ }
469
+ function defaultWorkerFile() {
470
+ return path3.join(os2.homedir(), ".kosa8", "worker.json");
471
+ }
472
+ function loadSavedWorker(file) {
473
+ let w;
474
+ try {
475
+ w = JSON.parse(fs2.readFileSync(file, "utf8"));
476
+ } catch {
477
+ return void 0;
478
+ }
479
+ if (!w || typeof w !== "object") return void 0;
480
+ const o = w;
481
+ const host = typeof o.host === "string" ? o.host.trim() : "";
482
+ if (host === "") return void 0;
483
+ return {
484
+ host,
485
+ token: typeof o.token === "string" && o.token !== "" ? o.token : void 0,
486
+ insecure: o.insecure === true
487
+ };
344
488
  }
345
489
  function isLoopback(hostname) {
346
490
  const h = hostname.replace(/^\[|\]$/g, "");
@@ -349,11 +493,20 @@ function isLoopback(hostname) {
349
493
  function resolveEndpoint(opts) {
350
494
  const env = opts.env ?? process.env;
351
495
  const fromEnv = (env.KOSA8_HOST ?? "").trim();
352
- const raw = (opts.host ?? fromEnv).trim().replace(/\/+$/, "");
353
- if (raw === "") {
354
- return { kind: "unix", socketPath: opts.socketPath ?? defaultSocketPath() };
496
+ const raw = (opts.host ?? fromEnv).trim();
497
+ const envInsecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
498
+ if (raw !== "") {
499
+ const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
500
+ return parseHost(raw, envInsecure, token);
501
+ }
502
+ if (opts.socketPath === void 0 && opts.workerFile !== null) {
503
+ const saved = loadSavedWorker(opts.workerFile ?? defaultWorkerFile());
504
+ if (saved) return parseHost(saved.host, envInsecure || saved.insecure, opts.token ?? saved.token);
355
505
  }
356
- let h = raw;
506
+ return { kind: "unix", socketPath: opts.socketPath ?? defaultSocketPath() };
507
+ }
508
+ function parseHost(raw, insecure, token) {
509
+ let h = raw.replace(/\/+$/, "");
357
510
  const i = h.indexOf("://");
358
511
  if (i > 0) h = h.slice(0, i).toLowerCase() + h.slice(i);
359
512
  if (h.startsWith("unix://")) {
@@ -369,13 +522,11 @@ function resolveEndpoint(opts) {
369
522
  h = "http://" + h;
370
523
  }
371
524
  const url = new URL(h);
372
- const insecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
373
525
  if (url.protocol === "http:" && !insecure && !isLoopback(url.hostname)) {
374
526
  throw new Kosa8Error(
375
527
  `refusing to talk to ${url.host} over plain http: the token and every request would cross the network unencrypted. Use https://${url.host} (a worker behind a TLS-terminating tunnel or proxy), or set insecure: true / KOSA8_INSECURE=1 if this link is already private and trusted`
376
528
  );
377
529
  }
378
- const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
379
530
  return { kind: "tcp", url, token: token || void 0 };
380
531
  }
381
532
  var Transport = class {
@@ -467,7 +618,7 @@ var Transport = class {
467
618
  }
468
619
  reject(
469
620
  new Kosa8ConnectionError(
470
- `kosa8 daemon unreachable at ${this.target}: ${err.message}` + (err.code === "ENOENT" || err.code === "ECONNREFUSED" ? " (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)" : ""),
621
+ `kosa8 daemon unreachable at ${this.target}: ${err.message}` + (err.code === "ENOENT" || err.code === "ECONNREFUSED" ? this.endpoint.kind === "unix" && process.platform === "win32" ? " (on Windows the engine runs in WSL2: run `kosa8 wsl setup`, or set KOSA8_HOST)" : " (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)" : ""),
471
622
  { method, path: p, cause: err }
472
623
  )
473
624
  );
@@ -580,6 +731,12 @@ function envList(env) {
580
731
  return Object.entries(env).map(([k, v]) => `${k}=${v}`);
581
732
  }
582
733
  var enc = encodeURIComponent;
734
+ function str(v) {
735
+ return typeof v === "string" ? v : "";
736
+ }
737
+ function num(v) {
738
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
739
+ }
583
740
  var Kosa8Client = class {
584
741
  transport;
585
742
  checkpoints;
@@ -624,6 +781,11 @@ var Kosa8Client = class {
624
781
  if (options.capture) body.capture = options.capture;
625
782
  if (options.labels) body.labels = options.labels;
626
783
  if (options.broker) body.broker = options.broker;
784
+ if (options.autoStopMin) body.auto_stop_min = options.autoStopMin;
785
+ if (options.autoDeleteMin) body.auto_delete_min = options.autoDeleteMin;
786
+ if (options.ttlMin) body.ttl_min = options.ttlMin;
787
+ if (options.ephemeral) body.ephemeral = true;
788
+ if (options.template) body.template = options.template;
627
789
  return this.transport.json("POST", "/kosa8/v1/sandboxes", {
628
790
  body,
629
791
  approval: options.approval,
@@ -920,6 +1082,315 @@ var Kosa8Client = class {
920
1082
  }
921
1083
  return { sandbox: restored.sandbox, checkpoint: target, dropped, restore_ms: restored.restore_ms };
922
1084
  }
1085
+ // --- toolbox: code -------------------------------------------------------------
1086
+ /**
1087
+ * POST /kosa8/v1/sandboxes/{id}/code: run a Python, JavaScript or
1088
+ * TypeScript snippet. It starts in `/workspace`. Charts a Python snippet
1089
+ * draws with matplotlib come back in `artifacts` as PNG bytes.
1090
+ */
1091
+ async runCode(id, code, opts = {}) {
1092
+ const body = { language: opts.language ?? "python", code };
1093
+ if (opts.image) body.image = opts.image;
1094
+ if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
1095
+ if (opts.timeoutS) body.timeout_s = opts.timeoutS;
1096
+ const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/code`, {
1097
+ body,
1098
+ approval: opts.approval,
1099
+ signal: opts.signal
1100
+ });
1101
+ const exitCode = num(d.exit);
1102
+ const timedOut = d.timed_out === true;
1103
+ const arts = Array.isArray(d.artifacts) ? d.artifacts : [];
1104
+ return {
1105
+ ok: exitCode === 0 && !timedOut,
1106
+ exitCode,
1107
+ stdout: str(d.stdout),
1108
+ stderr: str(d.stderr),
1109
+ language: str(d.language),
1110
+ image: str(d.image),
1111
+ artifacts: arts.map((a) => ({ type: str(a.type), name: str(a.name), data: Buffer.from(str(a.data), "base64") })),
1112
+ durationMs: num(d.duration_ms),
1113
+ timedOut,
1114
+ truncated: d.truncated === true
1115
+ };
1116
+ }
1117
+ // --- toolbox: files ------------------------------------------------------------
1118
+ /** GET …/files: a folder in the workspace (default `/workspace`), or one entry. */
1119
+ async listFiles(id, path4 = "/workspace", opts = {}) {
1120
+ const d = await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
1121
+ ...opts,
1122
+ query: { path: path4 }
1123
+ });
1124
+ return d?.files ?? [];
1125
+ }
1126
+ /** GET …/files/stat: one entry, not following a symlink. */
1127
+ statFile(id, path4, opts = {}) {
1128
+ return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files/stat`, { ...opts, query: { path: path4 } });
1129
+ }
1130
+ /** GET …/files/content: a file's bytes. A body cut short is a Kosa8StreamError. */
1131
+ async readFile(id, path4, opts = {}) {
1132
+ const p = `/kosa8/v1/sandboxes/${enc(id)}/files/content`;
1133
+ const res = await this.transport.raw("GET", p, { query: { path: path4 }, signal: opts.signal, approval: opts.approval });
1134
+ let data;
1135
+ try {
1136
+ data = await readAll(res.stream, opts.signal);
1137
+ } catch (err) {
1138
+ if (opts.signal?.aborted) throw err;
1139
+ throw new Kosa8StreamError(
1140
+ `${path4}: read cut short: ${err instanceof Error ? err.message : String(err)}`,
1141
+ { stdout: "", stderr: "" },
1142
+ { method: "GET", path: p, cause: err }
1143
+ );
1144
+ }
1145
+ if (res.status >= 400) {
1146
+ const text = data.toString("utf8");
1147
+ let body = text;
1148
+ try {
1149
+ body = JSON.parse(text);
1150
+ } catch {
1151
+ }
1152
+ throw errorFromResponse(res.status, body, { method: "GET", path: p });
1153
+ }
1154
+ const want = res.headers["content-length"];
1155
+ if (want !== void 0 && Number(want) !== data.length) {
1156
+ throw new Kosa8StreamError(`${path4}: got ${data.length} of ${want} bytes`, { stdout: "", stderr: "" }, { method: "GET", path: p });
1157
+ }
1158
+ return data;
1159
+ }
1160
+ /** A file's contents as text (UTF-8 by default). */
1161
+ async readText(id, path4, opts = {}) {
1162
+ return (await this.readFile(id, path4, opts)).toString(opts.encoding ?? "utf8");
1163
+ }
1164
+ /** PUT …/files/content: replace a file, making its folders. Answers the new entry. */
1165
+ writeFile(id, path4, data, opts = {}) {
1166
+ return this.transport.json("PUT", `/kosa8/v1/sandboxes/${enc(id)}/files/content`, {
1167
+ query: { path: path4, mode: opts.mode },
1168
+ rawBody: data,
1169
+ approval: opts.approval,
1170
+ signal: opts.signal
1171
+ });
1172
+ }
1173
+ /** DELETE …/files: a file, or with `recursive` a folder and everything in it. */
1174
+ deleteFile(id, path4, opts = {}) {
1175
+ return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
1176
+ query: { path: path4, recursive: opts.recursive },
1177
+ approval: opts.approval,
1178
+ signal: opts.signal
1179
+ });
1180
+ }
1181
+ /** POST …/files/mkdir: a folder, with its parents. */
1182
+ makeDir(id, path4, opts = {}) {
1183
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/mkdir`, { ...opts, body: { path: path4 } });
1184
+ }
1185
+ /** POST …/files/move: rename or move a file or folder. Answers the entry at `to`. */
1186
+ moveFile(id, from, to, opts = {}) {
1187
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/move`, { ...opts, body: { from, to } });
1188
+ }
1189
+ // --- toolbox: git ----------------------------------------------------------------
1190
+ /**
1191
+ * POST …/git: one git operation in the workspace. A git failure resolves
1192
+ * with `ok: false`; only a refusal (bad op, no sandbox, policy) throws.
1193
+ */
1194
+ async git(id, op, opts = {}) {
1195
+ const { approval, signal, authorName, authorEmail, ...fields } = opts;
1196
+ const body = { op };
1197
+ for (const [k, v] of Object.entries(fields)) if (v !== void 0) body[k] = v;
1198
+ if (authorName !== void 0) body.author_name = authorName;
1199
+ if (authorEmail !== void 0) body.author_email = authorEmail;
1200
+ const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/git`, {
1201
+ body,
1202
+ approval,
1203
+ signal
1204
+ });
1205
+ const out = {
1206
+ op: str(d.op) || op,
1207
+ ok: d.ok === true,
1208
+ exitCode: num(d.exit),
1209
+ stdout: str(d.stdout),
1210
+ stderr: str(d.stderr),
1211
+ path: str(d.path)
1212
+ };
1213
+ if (d.status) out.status = d.status;
1214
+ if (d.log) out.log = d.log;
1215
+ if (d.branches) out.branches = d.branches;
1216
+ return out;
1217
+ }
1218
+ /** Clone `url` into the workspace (into `path`, else a folder named after the repo). */
1219
+ gitClone(id, url, opts = {}) {
1220
+ return this.git(id, "clone", { ...opts, url });
1221
+ }
1222
+ /** `git status` of the repository at `path`, parsed into `status`. */
1223
+ gitStatus(id, path4, opts = {}) {
1224
+ return this.git(id, "status", { ...opts, path: path4 });
1225
+ }
1226
+ /** Stage everything (unless `addAll: false`) and commit. A failed add is returned as is. */
1227
+ async gitCommit(id, path4, message, opts = {}) {
1228
+ const { addAll = true, ...rest } = opts;
1229
+ if (addAll) {
1230
+ const r = await this.git(id, "add", { path: path4, image: rest.image, approval: rest.approval, signal: rest.signal });
1231
+ if (!r.ok) return r;
1232
+ }
1233
+ return this.git(id, "commit", { ...rest, path: path4, message });
1234
+ }
1235
+ // --- toolbox: shell sessions -------------------------------------------------------
1236
+ /** POST …/sessions: a shell that keeps `cd` and `export` between commands. */
1237
+ createSession(id, name, opts = {}) {
1238
+ const body = {};
1239
+ if (name) body.name = name;
1240
+ if (opts.image) body.image = opts.image;
1241
+ if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
1242
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, {
1243
+ body,
1244
+ approval: opts.approval,
1245
+ signal: opts.signal
1246
+ });
1247
+ }
1248
+ /** GET …/sessions */
1249
+ async listSessions(id, opts = {}) {
1250
+ return await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, opts) ?? [];
1251
+ }
1252
+ /** POST …/sessions/{name}/exec: run one command line in the session. */
1253
+ async sessionExec(id, session, command, opts = {}) {
1254
+ const body = { command };
1255
+ if (opts.timeoutS) body.timeout_s = opts.timeoutS;
1256
+ const d = await this.transport.json(
1257
+ "POST",
1258
+ `/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}/exec`,
1259
+ { body, approval: opts.approval, signal: opts.signal }
1260
+ );
1261
+ const exitCode = num(d.exit);
1262
+ const timedOut = d.timed_out === true;
1263
+ return {
1264
+ ok: exitCode === 0 && !timedOut,
1265
+ exitCode,
1266
+ stdout: str(d.stdout),
1267
+ stderr: str(d.stderr),
1268
+ cwd: str(d.cwd),
1269
+ durationMs: num(d.duration_ms),
1270
+ timedOut,
1271
+ truncated: d.truncated === true
1272
+ };
1273
+ }
1274
+ /** Run a command in a session (default `main`), making the session on first use. */
1275
+ async shell(id, command, opts = {}) {
1276
+ const { session = "main", ...rest } = opts;
1277
+ try {
1278
+ return await this.sessionExec(id, session, command, rest);
1279
+ } catch (err) {
1280
+ if (!(err instanceof Kosa8NotFoundError) || !err.message.includes("no such session")) throw err;
1281
+ }
1282
+ await this.createSession(id, session, { approval: rest.approval, signal: rest.signal });
1283
+ return this.sessionExec(id, session, command, rest);
1284
+ }
1285
+ /** DELETE …/sessions/{name} */
1286
+ deleteSession(id, session, opts = {}) {
1287
+ return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}`, opts);
1288
+ }
1289
+ // --- toolbox: terminals ----------------------------------------------------------
1290
+ /**
1291
+ * POST …/pty: a container with a terminal; resolves with its id. Connect
1292
+ * to `/kosa8/v1/sandboxes/{id}/containers/{cid}/tty` with any WebSocket
1293
+ * client: binary messages are the terminal, text messages JSON control.
1294
+ */
1295
+ async startPty(id, opts = {}) {
1296
+ const body = {};
1297
+ if (opts.image) body.image = opts.image;
1298
+ if (opts.command && opts.command.length > 0) body.command = opts.command;
1299
+ if (opts.cols && opts.rows) {
1300
+ body.cols = opts.cols;
1301
+ body.rows = opts.rows;
1302
+ }
1303
+ if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
1304
+ const out = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/pty`, {
1305
+ body,
1306
+ approval: opts.approval,
1307
+ signal: opts.signal
1308
+ });
1309
+ return out.id;
1310
+ }
1311
+ /** POST …/containers/{cid}/resize: set a terminal's size. */
1312
+ resizePty(id, container, cols, rows, opts = {}) {
1313
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/containers/${enc(container)}/resize`, {
1314
+ ...opts,
1315
+ body: { cols, rows }
1316
+ });
1317
+ }
1318
+ // --- toolbox: previews -----------------------------------------------------------
1319
+ /** POST …/previews: a signed link, which expires, to a web server in the sandbox. */
1320
+ createPreview(id, port, opts = {}) {
1321
+ const body = { port };
1322
+ if (opts.session) body.session = opts.session;
1323
+ if (opts.container) body.container = opts.container;
1324
+ if (opts.expiresInS) body.expires_in_s = opts.expiresInS;
1325
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/previews`, {
1326
+ body,
1327
+ approval: opts.approval,
1328
+ signal: opts.signal
1329
+ });
1330
+ }
1331
+ /** POST /kosa8/v1/previews/rotate: end every preview link at once. */
1332
+ revokePreviews(opts = {}) {
1333
+ return this.transport.json("POST", "/kosa8/v1/previews/rotate", opts);
1334
+ }
1335
+ // --- toolbox: lifecycle ----------------------------------------------------------
1336
+ /**
1337
+ * POST …/lifecycle: change auto-stop, auto-delete, TTL or ephemeral.
1338
+ * Options left out keep their value; 0 (or `false`) turns one off.
1339
+ */
1340
+ setLifecycle(id, opts = {}) {
1341
+ const body = {};
1342
+ if (opts.autoStopMin !== void 0) body.auto_stop_min = opts.autoStopMin;
1343
+ if (opts.autoDeleteMin !== void 0) body.auto_delete_min = opts.autoDeleteMin;
1344
+ if (opts.ttlMin !== void 0) body.ttl_min = opts.ttlMin;
1345
+ if (opts.ephemeral !== void 0) body.ephemeral = opts.ephemeral;
1346
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/lifecycle`, {
1347
+ body,
1348
+ approval: opts.approval,
1349
+ signal: opts.signal
1350
+ });
1351
+ }
1352
+ // --- toolbox: templates ----------------------------------------------------------
1353
+ /** GET /kosa8/v1/templates */
1354
+ async listTemplates(opts = {}) {
1355
+ return await this.transport.json("GET", "/kosa8/v1/templates", opts) ?? [];
1356
+ }
1357
+ /** DELETE /kosa8/v1/templates/{name}. Sandboxes made from it keep their copy. */
1358
+ deleteTemplate(name, opts = {}) {
1359
+ return this.transport.json("DELETE", `/kosa8/v1/templates/${enc(name)}`, opts);
1360
+ }
1361
+ /**
1362
+ * POST /kosa8/v1/templates: build `contextDir`'s Dockerfile in a throwaway
1363
+ * sandbox and keep the image as a template, for `createSandbox({ template })`.
1364
+ * The context is packed honouring `.dockerignore`. Resolves with the final
1365
+ * frame (`{ done, ok, image }`); a failed build throws Kosa8StreamError.
1366
+ */
1367
+ async buildTemplate(name, contextDir = ".", opts = {}) {
1368
+ const dockerfile = opts.dockerfile ?? "Dockerfile";
1369
+ const q = new URLSearchParams();
1370
+ q.append("name", name);
1371
+ q.append("dockerfile", dockerfile);
1372
+ for (const [k, v] of Object.entries(opts.buildArgs ?? {})) q.append("buildarg", `${k}=${v}`);
1373
+ const egress = egressString(opts.egress);
1374
+ if (egress) q.append("egress", egress);
1375
+ if (opts.replace) q.append("replace", "1");
1376
+ const ctx = await tarContext(contextDir, dockerfile);
1377
+ let out = "";
1378
+ for await (const f of this.transport.frames("POST", `/kosa8/v1/templates?${q.toString()}`, {
1379
+ rawBody: ctx,
1380
+ approval: opts.approval,
1381
+ signal: opts.signal
1382
+ })) {
1383
+ if (f.done) {
1384
+ if (f.error) throw new Kosa8StreamError(f.error, { stdout: out, stderr: "" }, { daemonMessage: f.error });
1385
+ return f;
1386
+ }
1387
+ if (f.data) {
1388
+ out += f.data.toString("utf8");
1389
+ opts.onOutput?.(f.data, f.stream ?? "stdout");
1390
+ }
1391
+ }
1392
+ throw new Kosa8StreamError("template build stream ended without a final frame", { stdout: out, stderr: "" });
1393
+ }
923
1394
  // --- helpers -------------------------------------------------------------------
924
1395
  /** A handle bound to one sandbox. */
925
1396
  sandbox(id) {
@@ -1019,13 +1490,275 @@ var SandboxHandle = class {
1019
1490
  rewind(opts) {
1020
1491
  return this.client.rewind(this.id, opts);
1021
1492
  }
1493
+ runCode(code, opts) {
1494
+ return this.client.runCode(this.id, code, opts);
1495
+ }
1496
+ listFiles(path4, opts) {
1497
+ return this.client.listFiles(this.id, path4, opts);
1498
+ }
1499
+ statFile(path4, opts) {
1500
+ return this.client.statFile(this.id, path4, opts);
1501
+ }
1502
+ readFile(path4, opts) {
1503
+ return this.client.readFile(this.id, path4, opts);
1504
+ }
1505
+ readText(path4, opts) {
1506
+ return this.client.readText(this.id, path4, opts);
1507
+ }
1508
+ writeFile(path4, data, opts) {
1509
+ return this.client.writeFile(this.id, path4, data, opts);
1510
+ }
1511
+ deleteFile(path4, opts) {
1512
+ return this.client.deleteFile(this.id, path4, opts);
1513
+ }
1514
+ makeDir(path4, opts) {
1515
+ return this.client.makeDir(this.id, path4, opts);
1516
+ }
1517
+ moveFile(from, to, opts) {
1518
+ return this.client.moveFile(this.id, from, to, opts);
1519
+ }
1520
+ git(op, opts) {
1521
+ return this.client.git(this.id, op, opts);
1522
+ }
1523
+ gitClone(url, opts) {
1524
+ return this.client.gitClone(this.id, url, opts);
1525
+ }
1526
+ gitStatus(path4, opts) {
1527
+ return this.client.gitStatus(this.id, path4, opts);
1528
+ }
1529
+ gitCommit(path4, message, opts) {
1530
+ return this.client.gitCommit(this.id, path4, message, opts);
1531
+ }
1532
+ createSession(name, opts) {
1533
+ return this.client.createSession(this.id, name, opts);
1534
+ }
1535
+ sessions(opts) {
1536
+ return this.client.listSessions(this.id, opts);
1537
+ }
1538
+ sessionExec(session, command, opts) {
1539
+ return this.client.sessionExec(this.id, session, command, opts);
1540
+ }
1541
+ shell(command, opts) {
1542
+ return this.client.shell(this.id, command, opts);
1543
+ }
1544
+ deleteSession(session, opts) {
1545
+ return this.client.deleteSession(this.id, session, opts);
1546
+ }
1547
+ startPty(opts) {
1548
+ return this.client.startPty(this.id, opts);
1549
+ }
1550
+ resizePty(container, cols, rows, opts) {
1551
+ return this.client.resizePty(this.id, container, cols, rows, opts);
1552
+ }
1553
+ createPreview(port, opts) {
1554
+ return this.client.createPreview(this.id, port, opts);
1555
+ }
1556
+ setLifecycle(opts) {
1557
+ return this.client.setLifecycle(this.id, opts);
1558
+ }
1559
+ };
1560
+
1561
+ // src/cloud.ts
1562
+ var DEFAULT_CLOUD_URL = "https://api.kosa8.cloud";
1563
+ var CLOUD_KEY_PREFIX = "k8c_live_";
1564
+ function trimSlash(s) {
1565
+ return s.replace(/\/+$/, "");
1566
+ }
1567
+ function checkBase(url) {
1568
+ let u;
1569
+ try {
1570
+ u = new URL(url);
1571
+ } catch {
1572
+ throw new Kosa8Error(`kosa8 Cloud address is not a URL: ${url}`);
1573
+ }
1574
+ const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
1575
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
1576
+ throw new Kosa8Error(`kosa8 Cloud address must use https: ${url}`);
1577
+ }
1578
+ return trimSlash(u.toString());
1579
+ }
1580
+ var Kosa8Cloud = class {
1581
+ baseUrl;
1582
+ sandboxes;
1583
+ functions;
1584
+ apiKey;
1585
+ fetchImpl;
1586
+ timeoutMs;
1587
+ constructor(options = {}) {
1588
+ const env = options.env ?? (typeof process !== "undefined" ? process.env : {});
1589
+ const key = (options.apiKey ?? env.KOSA8_API_KEY ?? "").trim();
1590
+ if (!key) throw new Kosa8Error("no kosa8 Cloud API key: pass apiKey or set KOSA8_API_KEY");
1591
+ if (!key.startsWith(CLOUD_KEY_PREFIX)) throw new Kosa8Error(`a kosa8 Cloud API key starts with ${CLOUD_KEY_PREFIX}`);
1592
+ this.apiKey = key;
1593
+ this.baseUrl = checkBase(options.baseUrl ?? env.KOSA8_CLOUD_URL ?? DEFAULT_CLOUD_URL);
1594
+ const f = options.fetch ?? globalThis.fetch;
1595
+ if (typeof f !== "function") throw new Kosa8Error("no fetch available; pass options.fetch");
1596
+ this.fetchImpl = f;
1597
+ this.timeoutMs = options.timeoutMs ?? 12e4;
1598
+ this.sandboxes = new CloudSandboxes(this);
1599
+ this.functions = new CloudFunctions(this);
1600
+ }
1601
+ /** The org, the key in use and, for a member's key, the member. */
1602
+ me() {
1603
+ return this.request("GET", "/v1/me");
1604
+ }
1605
+ /** This month's usage, bill estimate and whether work is paused for payment. */
1606
+ usage() {
1607
+ return this.request("GET", "/v1/usage");
1608
+ }
1609
+ /** @internal */
1610
+ async request(method, path4, body) {
1611
+ const ctl = new AbortController();
1612
+ const timer = setTimeout(() => ctl.abort(), this.timeoutMs);
1613
+ let resp;
1614
+ try {
1615
+ resp = await this.fetchImpl(`${this.baseUrl}${path4}`, {
1616
+ method,
1617
+ headers: {
1618
+ Authorization: `Bearer ${this.apiKey}`,
1619
+ Accept: "application/json",
1620
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
1621
+ },
1622
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
1623
+ signal: ctl.signal
1624
+ });
1625
+ } catch (e) {
1626
+ if (ctl.signal.aborted) {
1627
+ throw new Kosa8TimeoutError(`kosa8 Cloud did not answer within ${this.timeoutMs} ms`, { method, path: path4, cause: e });
1628
+ }
1629
+ throw new Kosa8ConnectionError(`could not reach kosa8 Cloud at ${this.baseUrl}: ${String(e?.message ?? e)}`, { method, path: path4, cause: e });
1630
+ } finally {
1631
+ clearTimeout(timer);
1632
+ }
1633
+ const text = await resp.text();
1634
+ let data = text;
1635
+ try {
1636
+ data = text === "" ? null : JSON.parse(text);
1637
+ } catch {
1638
+ }
1639
+ if (!resp.ok) throw cloudError(resp.status, data, { method, path: path4 });
1640
+ return data;
1641
+ }
1642
+ };
1643
+ function cloudError(status, body, ctx) {
1644
+ let code;
1645
+ let msgBody = body;
1646
+ if (typeof body === "object" && body !== null) {
1647
+ const b = body;
1648
+ if (typeof b.error === "string") code = b.error;
1649
+ msgBody = { error: typeof b.message === "string" && b.message ? b.message : b.error };
1650
+ }
1651
+ return errorFromResponse(status, msgBody, { ...ctx, code });
1652
+ }
1653
+ var CloudSandboxes = class {
1654
+ constructor(cloud) {
1655
+ this.cloud = cloud;
1656
+ }
1657
+ /** Start a sandbox; resolves once it is running. */
1658
+ async create(opts = {}) {
1659
+ const body = {};
1660
+ if (opts.template !== void 0) body.template = opts.template;
1661
+ if (opts.region !== void 0) body.region = opts.region;
1662
+ if (opts.sleepAfterS !== void 0) body.sleep_after_s = opts.sleepAfterS;
1663
+ const r = await this.cloud.request("POST", "/v1/sandboxes", body);
1664
+ return new CloudSandbox(this.cloud, r.sandbox);
1665
+ }
1666
+ /** The org's sandboxes; with all, deleted ones too. */
1667
+ async list(opts = {}) {
1668
+ const r = await this.cloud.request("GET", `/v1/sandboxes${opts.all ? "?all=1" : ""}`);
1669
+ return r.sandboxes.map((s) => new CloudSandbox(this.cloud, s));
1670
+ }
1671
+ async get(id) {
1672
+ const r = await this.cloud.request("GET", `/v1/sandboxes/${encodeURIComponent(id)}`);
1673
+ return new CloudSandbox(this.cloud, r.sandbox);
1674
+ }
1675
+ };
1676
+ var CloudSandbox = class {
1677
+ constructor(cloud, info) {
1678
+ this.cloud = cloud;
1679
+ this.info = info;
1680
+ }
1681
+ get id() {
1682
+ return this.info.id;
1683
+ }
1684
+ path(suffix = "") {
1685
+ return `/v1/sandboxes/${encodeURIComponent(this.info.id)}${suffix}`;
1686
+ }
1687
+ /**
1688
+ * Run a command and wait for it. A string runs through `sh -c`. A
1689
+ * sleeping sandbox wakes for it.
1690
+ */
1691
+ async exec(cmd, opts = {}) {
1692
+ const argv = typeof cmd === "string" ? ["sh", "-c", cmd] : cmd;
1693
+ const body = { cmd: argv };
1694
+ if (opts.image !== void 0) body.image = opts.image;
1695
+ if (opts.env) body.env = Object.entries(opts.env).map(([k, v]) => `${k}=${v}`);
1696
+ const r = await this.cloud.request(
1697
+ "POST",
1698
+ this.path("/exec"),
1699
+ body
1700
+ );
1701
+ const out = { exitCode: r.exit, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
1702
+ if (typeof r.woke_ms === "number") out.wokeMs = r.woke_ms;
1703
+ return out;
1704
+ }
1705
+ /** Snapshot it and free its VM; it then costs only snapshot storage. */
1706
+ async sleep() {
1707
+ this.info = (await this.cloud.request("POST", this.path("/sleep"))).sandbox;
1708
+ return this;
1709
+ }
1710
+ /** Restore a sleeping sandbox, files and all. */
1711
+ async wake() {
1712
+ this.info = (await this.cloud.request("POST", this.path("/wake"))).sandbox;
1713
+ return this;
1714
+ }
1715
+ async refresh() {
1716
+ this.info = (await this.cloud.request("GET", this.path())).sandbox;
1717
+ return this;
1718
+ }
1719
+ async delete() {
1720
+ await this.cloud.request("DELETE", this.path());
1721
+ }
1722
+ };
1723
+ var CloudFunctions = class {
1724
+ constructor(cloud) {
1725
+ this.cloud = cloud;
1726
+ }
1727
+ /** Deploy (create or replace) a function; every deploy bumps its version. */
1728
+ deploy(name, spec) {
1729
+ return this.cloud.request("PUT", `/v1/functions/${encodeURIComponent(name)}`, spec);
1730
+ }
1731
+ async list() {
1732
+ return (await this.cloud.request("GET", "/v1/functions")).functions;
1733
+ }
1734
+ get(name) {
1735
+ return this.cloud.request("GET", `/v1/functions/${encodeURIComponent(name)}`);
1736
+ }
1737
+ async delete(name) {
1738
+ await this.cloud.request("DELETE", `/v1/functions/${encodeURIComponent(name)}`);
1739
+ }
1740
+ /** Call it once. A handler that raises resolves with ok: false, not a throw. */
1741
+ invoke(name, event = null) {
1742
+ return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/invoke`, { event });
1743
+ }
1744
+ /** One call per event, results in input order. */
1745
+ map(name, events, opts = {}) {
1746
+ const body = { events };
1747
+ if (opts.concurrency !== void 0) body.concurrency = opts.concurrency;
1748
+ return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/map`, body);
1749
+ }
1022
1750
  };
1023
1751
  // Annotate the CommonJS export names for ESM import in node:
1024
1752
  0 && (module.exports = {
1025
1753
  ACTOR_HEADER,
1026
1754
  AGENT_TOKEN_HEADER,
1027
1755
  APPROVAL_HEADER,
1756
+ CLOUD_KEY_PREFIX,
1028
1757
  CheckpointBook,
1758
+ CloudFunctions,
1759
+ CloudSandbox,
1760
+ CloudSandboxes,
1761
+ DEFAULT_CLOUD_URL,
1029
1762
  FileCheckpointStore,
1030
1763
  Kosa8ApiError,
1031
1764
  Kosa8ApprovalRequiredError,
@@ -1033,20 +1766,26 @@ var SandboxHandle = class {
1033
1766
  Kosa8BadRequestError,
1034
1767
  Kosa8CheckpointError,
1035
1768
  Kosa8Client,
1769
+ Kosa8Cloud,
1036
1770
  Kosa8ConflictError,
1037
1771
  Kosa8ConnectionError,
1038
1772
  Kosa8Error,
1039
1773
  Kosa8NotFoundError,
1774
+ Kosa8PaymentRequiredError,
1040
1775
  Kosa8PolicyError,
1776
+ Kosa8RateLimitError,
1041
1777
  Kosa8StreamError,
1778
+ Kosa8TimeoutError,
1042
1779
  Kosa8UnsupportedError,
1043
1780
  MAX_CHECKPOINTS_PER_SANDBOX,
1044
1781
  MemoryCheckpointStore,
1045
1782
  SandboxHandle,
1046
1783
  Transport,
1047
1784
  defaultSocketPath,
1785
+ defaultWorkerFile,
1048
1786
  errorFromResponse,
1049
1787
  resolveEndpoint,
1788
+ tarContext,
1050
1789
  validateSandboxName
1051
1790
  });
1052
1791
  //# sourceMappingURL=index.cjs.map