@kosa8/sdk 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ var Kosa8Error = class extends Error {
15
15
  path;
16
16
  /** The decoded response body, when there was one. */
17
17
  body;
18
+ /** kosa8 Cloud's stable error code (payment_required, quota_exceeded, ...). */
19
+ code;
18
20
  constructor(message, options = {}) {
19
21
  super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
20
22
  this.name = new.target.name;
@@ -24,10 +26,13 @@ var Kosa8Error = class extends Error {
24
26
  this.method = options.method;
25
27
  this.path = options.path;
26
28
  this.body = options.body;
29
+ this.code = options.code;
27
30
  }
28
31
  };
29
32
  var Kosa8ConnectionError = class extends Kosa8Error {
30
33
  };
34
+ var Kosa8TimeoutError = class extends Kosa8ConnectionError {
35
+ };
31
36
  var Kosa8ApiError = class extends Kosa8Error {
32
37
  };
33
38
  var Kosa8BadRequestError = class extends Kosa8ApiError {
@@ -58,6 +63,10 @@ var Kosa8StreamError = class extends Kosa8Error {
58
63
  this.stderr = out.stderr;
59
64
  }
60
65
  };
66
+ var Kosa8PaymentRequiredError = class extends Kosa8ApiError {
67
+ };
68
+ var Kosa8RateLimitError = class extends Kosa8ApiError {
69
+ };
61
70
  var Kosa8CheckpointError = class extends Kosa8Error {
62
71
  };
63
72
  function isRefusal(v) {
@@ -91,8 +100,12 @@ function errorFromResponse(status, body, ctx = {}) {
91
100
  return new Kosa8PolicyError(message, opts);
92
101
  case 404:
93
102
  return new Kosa8NotFoundError(message, opts);
103
+ case 402:
104
+ return new Kosa8PaymentRequiredError(message, opts);
94
105
  case 409:
95
106
  return new Kosa8ConflictError(message, opts);
107
+ case 429:
108
+ return new Kosa8RateLimitError(message, opts);
96
109
  case 501:
97
110
  return new Kosa8UnsupportedError(message, opts);
98
111
  default:
@@ -270,16 +283,136 @@ var CheckpointBook = class {
270
283
  }
271
284
  };
272
285
 
286
+ // src/tar.ts
287
+ import { promises as fsp } from "fs";
288
+ import * as path2 from "path";
289
+ var BLOCK = 512;
290
+ function octal(buf, off, len, n) {
291
+ buf.write(n.toString(8).padStart(len - 1, "0") + "\0", off, len, "ascii");
292
+ }
293
+ function header(name, type, size, mode, mtime, link = "") {
294
+ const h = Buffer.alloc(BLOCK);
295
+ h.write(name, 0, 100, "utf8");
296
+ octal(h, 100, 8, mode & 4095);
297
+ octal(h, 108, 8, 0);
298
+ octal(h, 116, 8, 0);
299
+ octal(h, 124, 12, size);
300
+ octal(h, 136, 12, mtime);
301
+ h.fill(" ", 148, 156);
302
+ h.write(type, 156, 1, "ascii");
303
+ h.write(link, 157, 100, "utf8");
304
+ h.write("ustar\0", 257, 6, "ascii");
305
+ h.write("00", 263, 2, "ascii");
306
+ let sum = 0;
307
+ for (const b of h) sum += b;
308
+ h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
309
+ return h;
310
+ }
311
+ function paxRecord(key, value) {
312
+ const body = ` ${key}=${value}
313
+ `;
314
+ let n = Buffer.byteLength(body);
315
+ let len = n + String(n).length;
316
+ if (String(len).length !== String(n).length) len = n + String(len).length;
317
+ return `${len}${body}`;
318
+ }
319
+ function pad(n) {
320
+ const r = n % BLOCK;
321
+ return Buffer.alloc(r === 0 ? 0 : BLOCK - r);
322
+ }
323
+ function entry(out, name, type, mode, mtime, data, link = "") {
324
+ let pax = "";
325
+ if (Buffer.byteLength(name) > 100) pax += paxRecord("path", name);
326
+ if (Buffer.byteLength(link) > 100) pax += paxRecord("linkpath", link);
327
+ if (pax !== "") {
328
+ const p = Buffer.from(pax, "utf8");
329
+ out.push(header("PaxHeader", "x", p.length, 420, mtime), p, pad(p.length));
330
+ }
331
+ const size = data?.length ?? 0;
332
+ out.push(header(name.slice(0, 100), type, size, mode, mtime, link.slice(0, 100)));
333
+ if (data) out.push(data, pad(size));
334
+ }
335
+ async function readIgnore(dir) {
336
+ let text;
337
+ try {
338
+ text = await fsp.readFile(path2.join(dir, ".dockerignore"), "utf8");
339
+ } catch {
340
+ return [];
341
+ }
342
+ const out = [];
343
+ for (const line of text.split(/\r?\n/)) {
344
+ const l = line.trim();
345
+ if (l === "" || line.startsWith("#")) continue;
346
+ out.push(path2.posix.normalize(l.replace(/\\/g, "/")).replace(/\/+$/, "") || ".");
347
+ }
348
+ return out;
349
+ }
350
+ async function tarContext(dir, dockerfile = "Dockerfile") {
351
+ try {
352
+ await fsp.access(path2.join(dir, dockerfile));
353
+ } catch {
354
+ throw new Kosa8Error(`no ${dockerfile} in ${dir}`);
355
+ }
356
+ const ignore = await readIgnore(dir);
357
+ const skip = (rel) => ignore.some((p) => rel === p || rel.startsWith(p + "/"));
358
+ const out = [];
359
+ const walk = async (rel) => {
360
+ const ents = await fsp.readdir(path2.join(dir, rel), { withFileTypes: true });
361
+ ents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
362
+ const subdirs = [];
363
+ for (const e of ents) {
364
+ const r = rel === "" ? e.name : `${rel}/${e.name}`;
365
+ if (skip(r)) continue;
366
+ const full = path2.join(dir, r);
367
+ const st = await fsp.lstat(full);
368
+ const mtime = Math.floor(st.mtimeMs / 1e3);
369
+ if (st.isSymbolicLink()) {
370
+ entry(out, r, "2", st.mode, mtime, void 0, await fsp.readlink(full));
371
+ } else if (st.isDirectory()) {
372
+ entry(out, r + "/", "5", st.mode, mtime);
373
+ subdirs.push(r);
374
+ } else if (st.isFile()) {
375
+ entry(out, r, "0", st.mode, mtime, await fsp.readFile(full));
376
+ }
377
+ }
378
+ for (const s of subdirs) await walk(s);
379
+ };
380
+ await walk("");
381
+ out.push(Buffer.alloc(BLOCK * 2));
382
+ return Buffer.concat(out);
383
+ }
384
+
273
385
  // src/transport.ts
386
+ import * as fs2 from "fs";
274
387
  import * as http from "http";
275
388
  import * as https from "https";
276
389
  import * as os2 from "os";
277
- import * as path2 from "path";
390
+ import * as path3 from "path";
278
391
  var APPROVAL_HEADER = "X-Kosa8-Approval";
279
392
  var ACTOR_HEADER = "X-Kosa8-Actor";
280
393
  var AGENT_TOKEN_HEADER = "X-Kosa8-Agent-Token";
281
394
  function defaultSocketPath() {
282
- return path2.join(os2.homedir(), ".kosa8", "run", "kosa8.sock");
395
+ return path3.join(os2.homedir(), ".kosa8", "run", "kosa8.sock");
396
+ }
397
+ function defaultWorkerFile() {
398
+ return path3.join(os2.homedir(), ".kosa8", "worker.json");
399
+ }
400
+ function loadSavedWorker(file) {
401
+ let w;
402
+ try {
403
+ w = JSON.parse(fs2.readFileSync(file, "utf8"));
404
+ } catch {
405
+ return void 0;
406
+ }
407
+ if (!w || typeof w !== "object") return void 0;
408
+ const o = w;
409
+ const host = typeof o.host === "string" ? o.host.trim() : "";
410
+ if (host === "") return void 0;
411
+ return {
412
+ host,
413
+ token: typeof o.token === "string" && o.token !== "" ? o.token : void 0,
414
+ insecure: o.insecure === true
415
+ };
283
416
  }
284
417
  function isLoopback(hostname) {
285
418
  const h = hostname.replace(/^\[|\]$/g, "");
@@ -288,11 +421,20 @@ function isLoopback(hostname) {
288
421
  function resolveEndpoint(opts) {
289
422
  const env = opts.env ?? process.env;
290
423
  const fromEnv = (env.KOSA8_HOST ?? "").trim();
291
- const raw = (opts.host ?? fromEnv).trim().replace(/\/+$/, "");
292
- if (raw === "") {
293
- return { kind: "unix", socketPath: opts.socketPath ?? defaultSocketPath() };
424
+ const raw = (opts.host ?? fromEnv).trim();
425
+ const envInsecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
426
+ if (raw !== "") {
427
+ const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
428
+ return parseHost(raw, envInsecure, token);
429
+ }
430
+ if (opts.socketPath === void 0 && opts.workerFile !== null) {
431
+ const saved = loadSavedWorker(opts.workerFile ?? defaultWorkerFile());
432
+ if (saved) return parseHost(saved.host, envInsecure || saved.insecure, opts.token ?? saved.token);
294
433
  }
295
- let h = raw;
434
+ return { kind: "unix", socketPath: opts.socketPath ?? defaultSocketPath() };
435
+ }
436
+ function parseHost(raw, insecure, token) {
437
+ let h = raw.replace(/\/+$/, "");
296
438
  const i = h.indexOf("://");
297
439
  if (i > 0) h = h.slice(0, i).toLowerCase() + h.slice(i);
298
440
  if (h.startsWith("unix://")) {
@@ -308,13 +450,11 @@ function resolveEndpoint(opts) {
308
450
  h = "http://" + h;
309
451
  }
310
452
  const url = new URL(h);
311
- const insecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
312
453
  if (url.protocol === "http:" && !insecure && !isLoopback(url.hostname)) {
313
454
  throw new Kosa8Error(
314
455
  `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`
315
456
  );
316
457
  }
317
- const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
318
458
  return { kind: "tcp", url, token: token || void 0 };
319
459
  }
320
460
  var Transport = class {
@@ -406,7 +546,7 @@ var Transport = class {
406
546
  }
407
547
  reject(
408
548
  new Kosa8ConnectionError(
409
- `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`)" : ""),
549
+ `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`)" : ""),
410
550
  { method, path: p, cause: err }
411
551
  )
412
552
  );
@@ -519,6 +659,12 @@ function envList(env) {
519
659
  return Object.entries(env).map(([k, v]) => `${k}=${v}`);
520
660
  }
521
661
  var enc = encodeURIComponent;
662
+ function str(v) {
663
+ return typeof v === "string" ? v : "";
664
+ }
665
+ function num(v) {
666
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
667
+ }
522
668
  var Kosa8Client = class {
523
669
  transport;
524
670
  checkpoints;
@@ -563,6 +709,11 @@ var Kosa8Client = class {
563
709
  if (options.capture) body.capture = options.capture;
564
710
  if (options.labels) body.labels = options.labels;
565
711
  if (options.broker) body.broker = options.broker;
712
+ if (options.autoStopMin) body.auto_stop_min = options.autoStopMin;
713
+ if (options.autoDeleteMin) body.auto_delete_min = options.autoDeleteMin;
714
+ if (options.ttlMin) body.ttl_min = options.ttlMin;
715
+ if (options.ephemeral) body.ephemeral = true;
716
+ if (options.template) body.template = options.template;
566
717
  return this.transport.json("POST", "/kosa8/v1/sandboxes", {
567
718
  body,
568
719
  approval: options.approval,
@@ -859,6 +1010,315 @@ var Kosa8Client = class {
859
1010
  }
860
1011
  return { sandbox: restored.sandbox, checkpoint: target, dropped, restore_ms: restored.restore_ms };
861
1012
  }
1013
+ // --- toolbox: code -------------------------------------------------------------
1014
+ /**
1015
+ * POST /kosa8/v1/sandboxes/{id}/code: run a Python, JavaScript or
1016
+ * TypeScript snippet. It starts in `/workspace`. Charts a Python snippet
1017
+ * draws with matplotlib come back in `artifacts` as PNG bytes.
1018
+ */
1019
+ async runCode(id, code, opts = {}) {
1020
+ const body = { language: opts.language ?? "python", code };
1021
+ if (opts.image) body.image = opts.image;
1022
+ if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
1023
+ if (opts.timeoutS) body.timeout_s = opts.timeoutS;
1024
+ const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/code`, {
1025
+ body,
1026
+ approval: opts.approval,
1027
+ signal: opts.signal
1028
+ });
1029
+ const exitCode = num(d.exit);
1030
+ const timedOut = d.timed_out === true;
1031
+ const arts = Array.isArray(d.artifacts) ? d.artifacts : [];
1032
+ return {
1033
+ ok: exitCode === 0 && !timedOut,
1034
+ exitCode,
1035
+ stdout: str(d.stdout),
1036
+ stderr: str(d.stderr),
1037
+ language: str(d.language),
1038
+ image: str(d.image),
1039
+ artifacts: arts.map((a) => ({ type: str(a.type), name: str(a.name), data: Buffer.from(str(a.data), "base64") })),
1040
+ durationMs: num(d.duration_ms),
1041
+ timedOut,
1042
+ truncated: d.truncated === true
1043
+ };
1044
+ }
1045
+ // --- toolbox: files ------------------------------------------------------------
1046
+ /** GET …/files: a folder in the workspace (default `/workspace`), or one entry. */
1047
+ async listFiles(id, path4 = "/workspace", opts = {}) {
1048
+ const d = await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
1049
+ ...opts,
1050
+ query: { path: path4 }
1051
+ });
1052
+ return d?.files ?? [];
1053
+ }
1054
+ /** GET …/files/stat: one entry, not following a symlink. */
1055
+ statFile(id, path4, opts = {}) {
1056
+ return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files/stat`, { ...opts, query: { path: path4 } });
1057
+ }
1058
+ /** GET …/files/content: a file's bytes. A body cut short is a Kosa8StreamError. */
1059
+ async readFile(id, path4, opts = {}) {
1060
+ const p = `/kosa8/v1/sandboxes/${enc(id)}/files/content`;
1061
+ const res = await this.transport.raw("GET", p, { query: { path: path4 }, signal: opts.signal, approval: opts.approval });
1062
+ let data;
1063
+ try {
1064
+ data = await readAll(res.stream, opts.signal);
1065
+ } catch (err) {
1066
+ if (opts.signal?.aborted) throw err;
1067
+ throw new Kosa8StreamError(
1068
+ `${path4}: read cut short: ${err instanceof Error ? err.message : String(err)}`,
1069
+ { stdout: "", stderr: "" },
1070
+ { method: "GET", path: p, cause: err }
1071
+ );
1072
+ }
1073
+ if (res.status >= 400) {
1074
+ const text = data.toString("utf8");
1075
+ let body = text;
1076
+ try {
1077
+ body = JSON.parse(text);
1078
+ } catch {
1079
+ }
1080
+ throw errorFromResponse(res.status, body, { method: "GET", path: p });
1081
+ }
1082
+ const want = res.headers["content-length"];
1083
+ if (want !== void 0 && Number(want) !== data.length) {
1084
+ throw new Kosa8StreamError(`${path4}: got ${data.length} of ${want} bytes`, { stdout: "", stderr: "" }, { method: "GET", path: p });
1085
+ }
1086
+ return data;
1087
+ }
1088
+ /** A file's contents as text (UTF-8 by default). */
1089
+ async readText(id, path4, opts = {}) {
1090
+ return (await this.readFile(id, path4, opts)).toString(opts.encoding ?? "utf8");
1091
+ }
1092
+ /** PUT …/files/content: replace a file, making its folders. Answers the new entry. */
1093
+ writeFile(id, path4, data, opts = {}) {
1094
+ return this.transport.json("PUT", `/kosa8/v1/sandboxes/${enc(id)}/files/content`, {
1095
+ query: { path: path4, mode: opts.mode },
1096
+ rawBody: data,
1097
+ approval: opts.approval,
1098
+ signal: opts.signal
1099
+ });
1100
+ }
1101
+ /** DELETE …/files: a file, or with `recursive` a folder and everything in it. */
1102
+ deleteFile(id, path4, opts = {}) {
1103
+ return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
1104
+ query: { path: path4, recursive: opts.recursive },
1105
+ approval: opts.approval,
1106
+ signal: opts.signal
1107
+ });
1108
+ }
1109
+ /** POST …/files/mkdir: a folder, with its parents. */
1110
+ makeDir(id, path4, opts = {}) {
1111
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/mkdir`, { ...opts, body: { path: path4 } });
1112
+ }
1113
+ /** POST …/files/move: rename or move a file or folder. Answers the entry at `to`. */
1114
+ moveFile(id, from, to, opts = {}) {
1115
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/move`, { ...opts, body: { from, to } });
1116
+ }
1117
+ // --- toolbox: git ----------------------------------------------------------------
1118
+ /**
1119
+ * POST …/git: one git operation in the workspace. A git failure resolves
1120
+ * with `ok: false`; only a refusal (bad op, no sandbox, policy) throws.
1121
+ */
1122
+ async git(id, op, opts = {}) {
1123
+ const { approval, signal, authorName, authorEmail, ...fields } = opts;
1124
+ const body = { op };
1125
+ for (const [k, v] of Object.entries(fields)) if (v !== void 0) body[k] = v;
1126
+ if (authorName !== void 0) body.author_name = authorName;
1127
+ if (authorEmail !== void 0) body.author_email = authorEmail;
1128
+ const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/git`, {
1129
+ body,
1130
+ approval,
1131
+ signal
1132
+ });
1133
+ const out = {
1134
+ op: str(d.op) || op,
1135
+ ok: d.ok === true,
1136
+ exitCode: num(d.exit),
1137
+ stdout: str(d.stdout),
1138
+ stderr: str(d.stderr),
1139
+ path: str(d.path)
1140
+ };
1141
+ if (d.status) out.status = d.status;
1142
+ if (d.log) out.log = d.log;
1143
+ if (d.branches) out.branches = d.branches;
1144
+ return out;
1145
+ }
1146
+ /** Clone `url` into the workspace (into `path`, else a folder named after the repo). */
1147
+ gitClone(id, url, opts = {}) {
1148
+ return this.git(id, "clone", { ...opts, url });
1149
+ }
1150
+ /** `git status` of the repository at `path`, parsed into `status`. */
1151
+ gitStatus(id, path4, opts = {}) {
1152
+ return this.git(id, "status", { ...opts, path: path4 });
1153
+ }
1154
+ /** Stage everything (unless `addAll: false`) and commit. A failed add is returned as is. */
1155
+ async gitCommit(id, path4, message, opts = {}) {
1156
+ const { addAll = true, ...rest } = opts;
1157
+ if (addAll) {
1158
+ const r = await this.git(id, "add", { path: path4, image: rest.image, approval: rest.approval, signal: rest.signal });
1159
+ if (!r.ok) return r;
1160
+ }
1161
+ return this.git(id, "commit", { ...rest, path: path4, message });
1162
+ }
1163
+ // --- toolbox: shell sessions -------------------------------------------------------
1164
+ /** POST …/sessions: a shell that keeps `cd` and `export` between commands. */
1165
+ createSession(id, name, opts = {}) {
1166
+ const body = {};
1167
+ if (name) body.name = name;
1168
+ if (opts.image) body.image = opts.image;
1169
+ if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
1170
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, {
1171
+ body,
1172
+ approval: opts.approval,
1173
+ signal: opts.signal
1174
+ });
1175
+ }
1176
+ /** GET …/sessions */
1177
+ async listSessions(id, opts = {}) {
1178
+ return await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, opts) ?? [];
1179
+ }
1180
+ /** POST …/sessions/{name}/exec: run one command line in the session. */
1181
+ async sessionExec(id, session, command, opts = {}) {
1182
+ const body = { command };
1183
+ if (opts.timeoutS) body.timeout_s = opts.timeoutS;
1184
+ const d = await this.transport.json(
1185
+ "POST",
1186
+ `/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}/exec`,
1187
+ { body, approval: opts.approval, signal: opts.signal }
1188
+ );
1189
+ const exitCode = num(d.exit);
1190
+ const timedOut = d.timed_out === true;
1191
+ return {
1192
+ ok: exitCode === 0 && !timedOut,
1193
+ exitCode,
1194
+ stdout: str(d.stdout),
1195
+ stderr: str(d.stderr),
1196
+ cwd: str(d.cwd),
1197
+ durationMs: num(d.duration_ms),
1198
+ timedOut,
1199
+ truncated: d.truncated === true
1200
+ };
1201
+ }
1202
+ /** Run a command in a session (default `main`), making the session on first use. */
1203
+ async shell(id, command, opts = {}) {
1204
+ const { session = "main", ...rest } = opts;
1205
+ try {
1206
+ return await this.sessionExec(id, session, command, rest);
1207
+ } catch (err) {
1208
+ if (!(err instanceof Kosa8NotFoundError) || !err.message.includes("no such session")) throw err;
1209
+ }
1210
+ await this.createSession(id, session, { approval: rest.approval, signal: rest.signal });
1211
+ return this.sessionExec(id, session, command, rest);
1212
+ }
1213
+ /** DELETE …/sessions/{name} */
1214
+ deleteSession(id, session, opts = {}) {
1215
+ return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}`, opts);
1216
+ }
1217
+ // --- toolbox: terminals ----------------------------------------------------------
1218
+ /**
1219
+ * POST …/pty: a container with a terminal; resolves with its id. Connect
1220
+ * to `/kosa8/v1/sandboxes/{id}/containers/{cid}/tty` with any WebSocket
1221
+ * client: binary messages are the terminal, text messages JSON control.
1222
+ */
1223
+ async startPty(id, opts = {}) {
1224
+ const body = {};
1225
+ if (opts.image) body.image = opts.image;
1226
+ if (opts.command && opts.command.length > 0) body.command = opts.command;
1227
+ if (opts.cols && opts.rows) {
1228
+ body.cols = opts.cols;
1229
+ body.rows = opts.rows;
1230
+ }
1231
+ if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
1232
+ const out = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/pty`, {
1233
+ body,
1234
+ approval: opts.approval,
1235
+ signal: opts.signal
1236
+ });
1237
+ return out.id;
1238
+ }
1239
+ /** POST …/containers/{cid}/resize: set a terminal's size. */
1240
+ resizePty(id, container, cols, rows, opts = {}) {
1241
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/containers/${enc(container)}/resize`, {
1242
+ ...opts,
1243
+ body: { cols, rows }
1244
+ });
1245
+ }
1246
+ // --- toolbox: previews -----------------------------------------------------------
1247
+ /** POST …/previews: a signed link, which expires, to a web server in the sandbox. */
1248
+ createPreview(id, port, opts = {}) {
1249
+ const body = { port };
1250
+ if (opts.session) body.session = opts.session;
1251
+ if (opts.container) body.container = opts.container;
1252
+ if (opts.expiresInS) body.expires_in_s = opts.expiresInS;
1253
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/previews`, {
1254
+ body,
1255
+ approval: opts.approval,
1256
+ signal: opts.signal
1257
+ });
1258
+ }
1259
+ /** POST /kosa8/v1/previews/rotate: end every preview link at once. */
1260
+ revokePreviews(opts = {}) {
1261
+ return this.transport.json("POST", "/kosa8/v1/previews/rotate", opts);
1262
+ }
1263
+ // --- toolbox: lifecycle ----------------------------------------------------------
1264
+ /**
1265
+ * POST …/lifecycle: change auto-stop, auto-delete, TTL or ephemeral.
1266
+ * Options left out keep their value; 0 (or `false`) turns one off.
1267
+ */
1268
+ setLifecycle(id, opts = {}) {
1269
+ const body = {};
1270
+ if (opts.autoStopMin !== void 0) body.auto_stop_min = opts.autoStopMin;
1271
+ if (opts.autoDeleteMin !== void 0) body.auto_delete_min = opts.autoDeleteMin;
1272
+ if (opts.ttlMin !== void 0) body.ttl_min = opts.ttlMin;
1273
+ if (opts.ephemeral !== void 0) body.ephemeral = opts.ephemeral;
1274
+ return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/lifecycle`, {
1275
+ body,
1276
+ approval: opts.approval,
1277
+ signal: opts.signal
1278
+ });
1279
+ }
1280
+ // --- toolbox: templates ----------------------------------------------------------
1281
+ /** GET /kosa8/v1/templates */
1282
+ async listTemplates(opts = {}) {
1283
+ return await this.transport.json("GET", "/kosa8/v1/templates", opts) ?? [];
1284
+ }
1285
+ /** DELETE /kosa8/v1/templates/{name}. Sandboxes made from it keep their copy. */
1286
+ deleteTemplate(name, opts = {}) {
1287
+ return this.transport.json("DELETE", `/kosa8/v1/templates/${enc(name)}`, opts);
1288
+ }
1289
+ /**
1290
+ * POST /kosa8/v1/templates: build `contextDir`'s Dockerfile in a throwaway
1291
+ * sandbox and keep the image as a template, for `createSandbox({ template })`.
1292
+ * The context is packed honouring `.dockerignore`. Resolves with the final
1293
+ * frame (`{ done, ok, image }`); a failed build throws Kosa8StreamError.
1294
+ */
1295
+ async buildTemplate(name, contextDir = ".", opts = {}) {
1296
+ const dockerfile = opts.dockerfile ?? "Dockerfile";
1297
+ const q = new URLSearchParams();
1298
+ q.append("name", name);
1299
+ q.append("dockerfile", dockerfile);
1300
+ for (const [k, v] of Object.entries(opts.buildArgs ?? {})) q.append("buildarg", `${k}=${v}`);
1301
+ const egress = egressString(opts.egress);
1302
+ if (egress) q.append("egress", egress);
1303
+ if (opts.replace) q.append("replace", "1");
1304
+ const ctx = await tarContext(contextDir, dockerfile);
1305
+ let out = "";
1306
+ for await (const f of this.transport.frames("POST", `/kosa8/v1/templates?${q.toString()}`, {
1307
+ rawBody: ctx,
1308
+ approval: opts.approval,
1309
+ signal: opts.signal
1310
+ })) {
1311
+ if (f.done) {
1312
+ if (f.error) throw new Kosa8StreamError(f.error, { stdout: out, stderr: "" }, { daemonMessage: f.error });
1313
+ return f;
1314
+ }
1315
+ if (f.data) {
1316
+ out += f.data.toString("utf8");
1317
+ opts.onOutput?.(f.data, f.stream ?? "stdout");
1318
+ }
1319
+ }
1320
+ throw new Kosa8StreamError("template build stream ended without a final frame", { stdout: out, stderr: "" });
1321
+ }
862
1322
  // --- helpers -------------------------------------------------------------------
863
1323
  /** A handle bound to one sandbox. */
864
1324
  sandbox(id) {
@@ -958,12 +1418,274 @@ var SandboxHandle = class {
958
1418
  rewind(opts) {
959
1419
  return this.client.rewind(this.id, opts);
960
1420
  }
1421
+ runCode(code, opts) {
1422
+ return this.client.runCode(this.id, code, opts);
1423
+ }
1424
+ listFiles(path4, opts) {
1425
+ return this.client.listFiles(this.id, path4, opts);
1426
+ }
1427
+ statFile(path4, opts) {
1428
+ return this.client.statFile(this.id, path4, opts);
1429
+ }
1430
+ readFile(path4, opts) {
1431
+ return this.client.readFile(this.id, path4, opts);
1432
+ }
1433
+ readText(path4, opts) {
1434
+ return this.client.readText(this.id, path4, opts);
1435
+ }
1436
+ writeFile(path4, data, opts) {
1437
+ return this.client.writeFile(this.id, path4, data, opts);
1438
+ }
1439
+ deleteFile(path4, opts) {
1440
+ return this.client.deleteFile(this.id, path4, opts);
1441
+ }
1442
+ makeDir(path4, opts) {
1443
+ return this.client.makeDir(this.id, path4, opts);
1444
+ }
1445
+ moveFile(from, to, opts) {
1446
+ return this.client.moveFile(this.id, from, to, opts);
1447
+ }
1448
+ git(op, opts) {
1449
+ return this.client.git(this.id, op, opts);
1450
+ }
1451
+ gitClone(url, opts) {
1452
+ return this.client.gitClone(this.id, url, opts);
1453
+ }
1454
+ gitStatus(path4, opts) {
1455
+ return this.client.gitStatus(this.id, path4, opts);
1456
+ }
1457
+ gitCommit(path4, message, opts) {
1458
+ return this.client.gitCommit(this.id, path4, message, opts);
1459
+ }
1460
+ createSession(name, opts) {
1461
+ return this.client.createSession(this.id, name, opts);
1462
+ }
1463
+ sessions(opts) {
1464
+ return this.client.listSessions(this.id, opts);
1465
+ }
1466
+ sessionExec(session, command, opts) {
1467
+ return this.client.sessionExec(this.id, session, command, opts);
1468
+ }
1469
+ shell(command, opts) {
1470
+ return this.client.shell(this.id, command, opts);
1471
+ }
1472
+ deleteSession(session, opts) {
1473
+ return this.client.deleteSession(this.id, session, opts);
1474
+ }
1475
+ startPty(opts) {
1476
+ return this.client.startPty(this.id, opts);
1477
+ }
1478
+ resizePty(container, cols, rows, opts) {
1479
+ return this.client.resizePty(this.id, container, cols, rows, opts);
1480
+ }
1481
+ createPreview(port, opts) {
1482
+ return this.client.createPreview(this.id, port, opts);
1483
+ }
1484
+ setLifecycle(opts) {
1485
+ return this.client.setLifecycle(this.id, opts);
1486
+ }
1487
+ };
1488
+
1489
+ // src/cloud.ts
1490
+ var DEFAULT_CLOUD_URL = "https://api.kosa8.com";
1491
+ var CLOUD_KEY_PREFIX = "k8c_live_";
1492
+ function trimSlash(s) {
1493
+ return s.replace(/\/+$/, "");
1494
+ }
1495
+ function checkBase(url) {
1496
+ let u;
1497
+ try {
1498
+ u = new URL(url);
1499
+ } catch {
1500
+ throw new Kosa8Error(`kosa8 Cloud address is not a URL: ${url}`);
1501
+ }
1502
+ const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
1503
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
1504
+ throw new Kosa8Error(`kosa8 Cloud address must use https: ${url}`);
1505
+ }
1506
+ return trimSlash(u.toString());
1507
+ }
1508
+ var Kosa8Cloud = class {
1509
+ baseUrl;
1510
+ sandboxes;
1511
+ functions;
1512
+ apiKey;
1513
+ fetchImpl;
1514
+ timeoutMs;
1515
+ constructor(options = {}) {
1516
+ const env = options.env ?? (typeof process !== "undefined" ? process.env : {});
1517
+ const key = (options.apiKey ?? env.KOSA8_API_KEY ?? "").trim();
1518
+ if (!key) throw new Kosa8Error("no kosa8 Cloud API key: pass apiKey or set KOSA8_API_KEY");
1519
+ if (!key.startsWith(CLOUD_KEY_PREFIX)) throw new Kosa8Error(`a kosa8 Cloud API key starts with ${CLOUD_KEY_PREFIX}`);
1520
+ this.apiKey = key;
1521
+ this.baseUrl = checkBase(options.baseUrl ?? env.KOSA8_CLOUD_URL ?? DEFAULT_CLOUD_URL);
1522
+ const f = options.fetch ?? globalThis.fetch;
1523
+ if (typeof f !== "function") throw new Kosa8Error("no fetch available; pass options.fetch");
1524
+ this.fetchImpl = f;
1525
+ this.timeoutMs = options.timeoutMs ?? 12e4;
1526
+ this.sandboxes = new CloudSandboxes(this);
1527
+ this.functions = new CloudFunctions(this);
1528
+ }
1529
+ /** The org, the key in use and, for a member's key, the member. */
1530
+ me() {
1531
+ return this.request("GET", "/v1/me");
1532
+ }
1533
+ /** This month's usage, bill estimate and whether work is paused for payment. */
1534
+ usage() {
1535
+ return this.request("GET", "/v1/usage");
1536
+ }
1537
+ /** @internal */
1538
+ async request(method, path4, body) {
1539
+ const ctl = new AbortController();
1540
+ const timer = setTimeout(() => ctl.abort(), this.timeoutMs);
1541
+ let resp;
1542
+ try {
1543
+ resp = await this.fetchImpl(`${this.baseUrl}${path4}`, {
1544
+ method,
1545
+ headers: {
1546
+ Authorization: `Bearer ${this.apiKey}`,
1547
+ Accept: "application/json",
1548
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
1549
+ },
1550
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
1551
+ signal: ctl.signal
1552
+ });
1553
+ } catch (e) {
1554
+ if (ctl.signal.aborted) {
1555
+ throw new Kosa8TimeoutError(`kosa8 Cloud did not answer within ${this.timeoutMs} ms`, { method, path: path4, cause: e });
1556
+ }
1557
+ throw new Kosa8ConnectionError(`could not reach kosa8 Cloud at ${this.baseUrl}: ${String(e?.message ?? e)}`, { method, path: path4, cause: e });
1558
+ } finally {
1559
+ clearTimeout(timer);
1560
+ }
1561
+ const text = await resp.text();
1562
+ let data = text;
1563
+ try {
1564
+ data = text === "" ? null : JSON.parse(text);
1565
+ } catch {
1566
+ }
1567
+ if (!resp.ok) throw cloudError(resp.status, data, { method, path: path4 });
1568
+ return data;
1569
+ }
1570
+ };
1571
+ function cloudError(status, body, ctx) {
1572
+ let code;
1573
+ let msgBody = body;
1574
+ if (typeof body === "object" && body !== null) {
1575
+ const b = body;
1576
+ if (typeof b.error === "string") code = b.error;
1577
+ msgBody = { error: typeof b.message === "string" && b.message ? b.message : b.error };
1578
+ }
1579
+ return errorFromResponse(status, msgBody, { ...ctx, code });
1580
+ }
1581
+ var CloudSandboxes = class {
1582
+ constructor(cloud) {
1583
+ this.cloud = cloud;
1584
+ }
1585
+ /** Start a sandbox; resolves once it is running. */
1586
+ async create(opts = {}) {
1587
+ const body = {};
1588
+ if (opts.template !== void 0) body.template = opts.template;
1589
+ if (opts.region !== void 0) body.region = opts.region;
1590
+ if (opts.sleepAfterS !== void 0) body.sleep_after_s = opts.sleepAfterS;
1591
+ const r = await this.cloud.request("POST", "/v1/sandboxes", body);
1592
+ return new CloudSandbox(this.cloud, r.sandbox);
1593
+ }
1594
+ /** The org's sandboxes; with all, deleted ones too. */
1595
+ async list(opts = {}) {
1596
+ const r = await this.cloud.request("GET", `/v1/sandboxes${opts.all ? "?all=1" : ""}`);
1597
+ return r.sandboxes.map((s) => new CloudSandbox(this.cloud, s));
1598
+ }
1599
+ async get(id) {
1600
+ const r = await this.cloud.request("GET", `/v1/sandboxes/${encodeURIComponent(id)}`);
1601
+ return new CloudSandbox(this.cloud, r.sandbox);
1602
+ }
1603
+ };
1604
+ var CloudSandbox = class {
1605
+ constructor(cloud, info) {
1606
+ this.cloud = cloud;
1607
+ this.info = info;
1608
+ }
1609
+ get id() {
1610
+ return this.info.id;
1611
+ }
1612
+ path(suffix = "") {
1613
+ return `/v1/sandboxes/${encodeURIComponent(this.info.id)}${suffix}`;
1614
+ }
1615
+ /**
1616
+ * Run a command and wait for it. A string runs through `sh -c`. A
1617
+ * sleeping sandbox wakes for it.
1618
+ */
1619
+ async exec(cmd, opts = {}) {
1620
+ const argv = typeof cmd === "string" ? ["sh", "-c", cmd] : cmd;
1621
+ const body = { cmd: argv };
1622
+ if (opts.image !== void 0) body.image = opts.image;
1623
+ if (opts.env) body.env = Object.entries(opts.env).map(([k, v]) => `${k}=${v}`);
1624
+ const r = await this.cloud.request(
1625
+ "POST",
1626
+ this.path("/exec"),
1627
+ body
1628
+ );
1629
+ const out = { exitCode: r.exit, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
1630
+ if (typeof r.woke_ms === "number") out.wokeMs = r.woke_ms;
1631
+ return out;
1632
+ }
1633
+ /** Snapshot it and free its VM; it then costs only snapshot storage. */
1634
+ async sleep() {
1635
+ this.info = (await this.cloud.request("POST", this.path("/sleep"))).sandbox;
1636
+ return this;
1637
+ }
1638
+ /** Restore a sleeping sandbox, files and all. */
1639
+ async wake() {
1640
+ this.info = (await this.cloud.request("POST", this.path("/wake"))).sandbox;
1641
+ return this;
1642
+ }
1643
+ async refresh() {
1644
+ this.info = (await this.cloud.request("GET", this.path())).sandbox;
1645
+ return this;
1646
+ }
1647
+ async delete() {
1648
+ await this.cloud.request("DELETE", this.path());
1649
+ }
1650
+ };
1651
+ var CloudFunctions = class {
1652
+ constructor(cloud) {
1653
+ this.cloud = cloud;
1654
+ }
1655
+ /** Deploy (create or replace) a function; every deploy bumps its version. */
1656
+ deploy(name, spec) {
1657
+ return this.cloud.request("PUT", `/v1/functions/${encodeURIComponent(name)}`, spec);
1658
+ }
1659
+ async list() {
1660
+ return (await this.cloud.request("GET", "/v1/functions")).functions;
1661
+ }
1662
+ get(name) {
1663
+ return this.cloud.request("GET", `/v1/functions/${encodeURIComponent(name)}`);
1664
+ }
1665
+ async delete(name) {
1666
+ await this.cloud.request("DELETE", `/v1/functions/${encodeURIComponent(name)}`);
1667
+ }
1668
+ /** Call it once. A handler that raises resolves with ok: false, not a throw. */
1669
+ invoke(name, event = null) {
1670
+ return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/invoke`, { event });
1671
+ }
1672
+ /** One call per event, results in input order. */
1673
+ map(name, events, opts = {}) {
1674
+ const body = { events };
1675
+ if (opts.concurrency !== void 0) body.concurrency = opts.concurrency;
1676
+ return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/map`, body);
1677
+ }
961
1678
  };
962
1679
  export {
963
1680
  ACTOR_HEADER,
964
1681
  AGENT_TOKEN_HEADER,
965
1682
  APPROVAL_HEADER,
1683
+ CLOUD_KEY_PREFIX,
966
1684
  CheckpointBook,
1685
+ CloudFunctions,
1686
+ CloudSandbox,
1687
+ CloudSandboxes,
1688
+ DEFAULT_CLOUD_URL,
967
1689
  FileCheckpointStore,
968
1690
  Kosa8ApiError,
969
1691
  Kosa8ApprovalRequiredError,
@@ -971,20 +1693,26 @@ export {
971
1693
  Kosa8BadRequestError,
972
1694
  Kosa8CheckpointError,
973
1695
  Kosa8Client,
1696
+ Kosa8Cloud,
974
1697
  Kosa8ConflictError,
975
1698
  Kosa8ConnectionError,
976
1699
  Kosa8Error,
977
1700
  Kosa8NotFoundError,
1701
+ Kosa8PaymentRequiredError,
978
1702
  Kosa8PolicyError,
1703
+ Kosa8RateLimitError,
979
1704
  Kosa8StreamError,
1705
+ Kosa8TimeoutError,
980
1706
  Kosa8UnsupportedError,
981
1707
  MAX_CHECKPOINTS_PER_SANDBOX,
982
1708
  MemoryCheckpointStore,
983
1709
  SandboxHandle,
984
1710
  Transport,
985
1711
  defaultSocketPath,
1712
+ defaultWorkerFile,
986
1713
  errorFromResponse,
987
1714
  resolveEndpoint,
1715
+ tarContext,
988
1716
  validateSandboxName
989
1717
  };
990
1718
  //# sourceMappingURL=index.js.map