@miosa/sdk 3.0.0 → 3.0.2

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
@@ -168,7 +168,7 @@ var TokenRefreshFailedError = class extends MiosaError {
168
168
  };
169
169
 
170
170
  // src/version.ts
171
- var SDK_VERSION = "3.0.0";
171
+ var SDK_VERSION = "2.0.7";
172
172
  var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
173
173
 
174
174
  // src/http.ts
@@ -198,7 +198,6 @@ async function ensureHttp2Agent() {
198
198
  }
199
199
  }
200
200
  ensureHttp2Agent();
201
- var SSE_ACCEPT = "text/event-stream, application/json;q=0.9";
202
201
  var DEFAULT_TIMEOUT = 3e4;
203
202
  var DEFAULT_MAX_RETRIES = 3;
204
203
  var RETRY_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
@@ -365,13 +364,16 @@ var HttpClient = class {
365
364
  return this.request(path, { method: "POST", formData });
366
365
  }
367
366
  /**
368
- * Open a Server-Sent Events stream. Returns an AsyncIterableIterator of
369
- * parsed event data objects. The caller is responsible for breaking the loop.
367
+ * Open a Server-Sent Events stream and yield each frame's parsed `data:`
368
+ * payload together with its SSE `event:` name (when present). Most callers
369
+ * want {@link stream}; use this when the event name carries meaning — e.g. the
370
+ * sandbox exec stream tags frames as `stdout` / `stderr` / `exit`. The caller
371
+ * is responsible for breaking the loop.
370
372
  */
371
- async *stream(path, options = {}) {
373
+ async *streamFrames(path, options = {}) {
372
374
  const method = options.method ?? "GET";
373
375
  let headers = this.baseHeaders({
374
- Accept: SSE_ACCEPT,
376
+ Accept: "text/event-stream",
375
377
  ...options.headers
376
378
  });
377
379
  let body5 = null;
@@ -409,14 +411,13 @@ var HttpClient = class {
409
411
  response.headers.get("x-request-id") ?? void 0
410
412
  );
411
413
  }
412
- if (response.status === 204) return;
413
414
  if (!response.body) {
414
415
  throw new MiosaError("SSE stream has no body", 0, "NO_STREAM_BODY");
415
416
  }
416
417
  const reader = response.body.getReader();
417
418
  const decoder = new TextDecoder();
418
419
  let buffer = "";
419
- let eventName;
420
+ let event = null;
420
421
  try {
421
422
  while (true) {
422
423
  const { done, value } = await reader.read();
@@ -424,23 +425,22 @@ var HttpClient = class {
424
425
  buffer += decoder.decode(value, { stream: true });
425
426
  const lines = buffer.split("\n");
426
427
  buffer = lines.pop() ?? "";
427
- for (const line of lines) {
428
- if (line.startsWith("event:")) {
429
- eventName = line.slice(6).trim();
428
+ for (const rawLine of lines) {
429
+ const line = rawLine.replace(/\r$/, "");
430
+ if (line === "") {
431
+ event = null;
430
432
  continue;
431
433
  }
432
- if (line === "") {
433
- eventName = void 0;
434
+ if (line.startsWith(":")) continue;
435
+ if (line.startsWith("event:")) {
436
+ event = line.slice(6).trim();
434
437
  continue;
435
438
  }
436
439
  if (line.startsWith("data:")) {
437
440
  const raw = line.slice(5).trim();
438
- const name = eventName;
439
- eventName = void 0;
440
441
  if (raw === "[DONE]" || raw === "") continue;
441
442
  try {
442
- const parsed = JSON.parse(raw);
443
- yield name !== void 0 && parsed !== null && typeof parsed === "object" && !("type" in parsed) ? { type: name, ...parsed } : parsed;
443
+ yield { event, data: JSON.parse(raw) };
444
444
  } catch {
445
445
  }
446
446
  }
@@ -451,6 +451,15 @@ var HttpClient = class {
451
451
  reader.releaseLock();
452
452
  }
453
453
  }
454
+ /**
455
+ * Open a Server-Sent Events stream. Returns an AsyncIterableIterator of
456
+ * parsed event data objects. The caller is responsible for breaking the loop.
457
+ */
458
+ async *stream(path, options = {}) {
459
+ for await (const frame of this.streamFrames(path, options)) {
460
+ yield frame.data;
461
+ }
462
+ }
454
463
  };
455
464
 
456
465
  // src/resources/admin.ts
@@ -7788,130 +7797,41 @@ var Jobs = class {
7788
7797
  return `/opencomputers/hosts/${hostId}`;
7789
7798
  }
7790
7799
  /**
7791
- * Run a command on the host and wait for the buffered result.
7792
- *
7793
- * Sends `stream: false` so the server buffers output and answers with JSON.
7794
- * `stream` defaults to *true* server-side, which answers with a chunked SSE
7795
- * body instead - use {@link runStream} for that.
7796
- *
7797
- * Note this holds the connection open for the life of the command
7798
- * (`timeout_ms`, 30 s default), because the server does not answer until
7799
- * the command finishes.
7800
+ * Dispatch a command to run on the remote host.
7800
7801
  */
7801
7802
  async run(hostId, params) {
7802
- return this.http.post(`${this.base(hostId)}/exec`, {
7803
- ...params,
7804
- stream: false
7805
- });
7803
+ return this.http.post(`${this.base(hostId)}/exec`, params);
7806
7804
  }
7807
7805
  /**
7808
- * Run a command on the host and stream its output as it happens.
7809
- *
7810
- * Same endpoint as {@link run} with `stream: true`, which is the server's
7811
- * default mode. Terminates with one `exec_result` or one `exec_error`.
7806
+ * List all jobs for a host.
7812
7807
  */
7813
- runStream(hostId, params) {
7814
- return translateExecStream(
7815
- this.http.stream(`${this.base(hostId)}/exec`, {
7816
- method: "POST",
7817
- body: { ...params, stream: true }
7818
- })
7819
- );
7820
- }
7821
- /**
7822
- * List recent job activity for a host.
7823
- *
7824
- * Hits `/jobs`, not `/exec`: there is no `GET /opencomputers/hosts/{id}/exec`
7825
- * route on the server, so the old path 404'd. The rows are audit-log events
7826
- * about jobs (`job.dispatched` / `job.done` / `job.failed`), newest first,
7827
- * not job records - see {@link JobAuditEvent}.
7828
- *
7829
- * @param limit Server caps this at 100 and defaults to 20.
7830
- */
7831
- async list(hostId, limit) {
7832
- return this.http.get(
7833
- `${this.base(hostId)}/jobs`,
7834
- limit === void 0 ? void 0 : { limit }
7835
- );
7808
+ async list(hostId) {
7809
+ return this.http.get(`${this.base(hostId)}/exec`);
7836
7810
  }
7837
7811
  /**
7838
7812
  * Fetch the current state of a job.
7839
- *
7840
- * The server wraps the record in a `job` envelope; this unwraps it.
7841
7813
  */
7842
7814
  async get(hostId, jobId) {
7843
- const response = await this.http.get(
7844
- `${this.base(hostId)}/exec/${jobId}`
7845
- );
7846
- if (response && typeof response === "object" && "job" in response) {
7847
- return response.job;
7848
- }
7849
- return response;
7815
+ return this.http.get(`${this.base(hostId)}/exec/${jobId}`);
7850
7816
  }
7851
7817
  /**
7852
- * Re-attach to a job that is already running and stream what is left.
7818
+ * Stream live output from a running job.
7853
7819
  *
7854
- * This is the reconnect endpoint. If the job has already reached a terminal
7855
- * state the server answers `204 No Content` and this yields nothing, rather
7856
- * than replaying output - the server never stored it.
7820
+ * Yields `JobEvent` objects with `type` of `stdout`, `stderr`, `exit`, or
7821
+ * `done`. Break the loop when you receive `done` or `exit`.
7857
7822
  */
7858
7823
  stream(hostId, jobId) {
7859
- return translateExecStream(
7860
- this.http.stream(`${this.base(hostId)}/exec/${jobId}/stream`)
7824
+ return this.http.stream(
7825
+ `${this.base(hostId)}/exec/${jobId}/stream`
7861
7826
  );
7862
7827
  }
7863
7828
  /**
7864
7829
  * Cancel a running or queued job.
7865
- *
7866
- * Answers `204` on success and `409` when the job is already terminal.
7867
7830
  */
7868
7831
  async cancel(hostId, jobId) {
7869
7832
  return this.http.delete(`${this.base(hostId)}/exec/${jobId}`);
7870
7833
  }
7871
7834
  };
7872
- var EXEC_EVENT_NAMES = /* @__PURE__ */ new Set(["exec_chunk", "exec_result", "exec_error"]);
7873
- function asString(value) {
7874
- return typeof value === "string" ? value : null;
7875
- }
7876
- function asNumber(value) {
7877
- return typeof value === "number" ? value : null;
7878
- }
7879
- function toJobEvent(raw) {
7880
- if (raw === null || typeof raw !== "object") return null;
7881
- const record = raw;
7882
- const name = asString(record["type"]);
7883
- if (name === null || !EXEC_EVENT_NAMES.has(name)) return null;
7884
- if (name === "exec_chunk") {
7885
- return {
7886
- type: "exec_chunk",
7887
- // The server defaults this to :stdout when the host omits it; anything
7888
- // other than "stderr" is stdout by the same rule.
7889
- stream: record["stream"] === "stderr" ? "stderr" : "stdout",
7890
- data: asString(record["data"])
7891
- };
7892
- }
7893
- if (name === "exec_result") {
7894
- return {
7895
- type: "exec_result",
7896
- exit_code: asNumber(record["exit_code"]),
7897
- elapsed_ms: asNumber(record["elapsed_ms"])
7898
- };
7899
- }
7900
- const jobId = asString(record["job_id"]);
7901
- return {
7902
- type: "exec_error",
7903
- reason: asString(record["reason"]),
7904
- // Only the timeout path carries job_id. exactOptionalPropertyTypes means
7905
- // the key has to be omitted rather than set to undefined.
7906
- ...jobId === null ? {} : { job_id: jobId }
7907
- };
7908
- }
7909
- async function* translateExecStream(source) {
7910
- for await (const raw of source) {
7911
- const event = toJobEvent(raw);
7912
- if (event !== null) yield event;
7913
- }
7914
- }
7915
7835
 
7916
7836
  // src/resources/open-computers/secrets.ts
7917
7837
  var Secrets = class {
@@ -8522,32 +8442,6 @@ var RuntimeCapabilitiesResource = class {
8522
8442
  }
8523
8443
  };
8524
8444
 
8525
- // src/resources/sandbox-host-routes.ts
8526
- var SANDBOXES_HOST_BASE_URL = "https://sandboxes.miosa.ai/api/v1";
8527
- var SANDBOXES_HOST = "sandboxes.miosa.ai";
8528
- function isSandboxesHost(baseUrl) {
8529
- try {
8530
- return new URL(baseUrl).host === SANDBOXES_HOST;
8531
- } catch {
8532
- return false;
8533
- }
8534
- }
8535
- async function* streamSandboxesHostRoute(http, path, alternative) {
8536
- try {
8537
- yield* http.stream(path);
8538
- } catch (err) {
8539
- if (err instanceof MiosaError && err.status === 404 && !isSandboxesHost(http.baseUrl)) {
8540
- throw new NotFoundError(
8541
- `HTTP 404 for GET ${path}. This route is mounted only on the sandboxes host router (${SANDBOXES_HOST}); it is not part of the platform router that serves this client's baseUrl (${http.baseUrl}). Either construct the client with baseUrl: "${SANDBOXES_HOST_BASE_URL}", or ${alternative}. A wrong sandbox id also produces a 404.`,
8542
- err.code,
8543
- err.details,
8544
- err.requestId
8545
- );
8546
- }
8547
- throw err;
8548
- }
8549
- }
8550
-
8551
8445
  // src/resources/sandboxes.ts
8552
8446
  function encodeContent(content) {
8553
8447
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
@@ -8558,12 +8452,12 @@ function encodeContent(content) {
8558
8452
  return btoa(bin);
8559
8453
  }
8560
8454
  var SANDBOX_TEMPLATE = "miosa-sandbox";
8561
- var SANDBOX_SHAPE_CONTRACTS = {
8562
- xs: { cpuCount: 1, memoryMb: 2048, diskSizeMb: 10240 },
8563
- small: { cpuCount: 2, memoryMb: 4096, diskSizeMb: 10240 },
8564
- medium: { cpuCount: 4, memoryMb: 8192, diskSizeMb: 20480 },
8565
- large: { cpuCount: 8, memoryMb: 16384, diskSizeMb: 40960 },
8566
- xl: { cpuCount: 16, memoryMb: 32768, diskSizeMb: 81920 }
8455
+ var SANDBOX_TIER_BY_CPU = {
8456
+ 1: { size: "xs", memoryMb: 2048 },
8457
+ 2: { size: "small", memoryMb: 4096 },
8458
+ 4: { size: "medium", memoryMb: 8192 },
8459
+ 8: { size: "large", memoryMb: 16384 },
8460
+ 16: { size: "xl", memoryMb: 32768 }
8567
8461
  };
8568
8462
  var AGENT_WORKSPACE_TIMEOUT_SEC = 86400;
8569
8463
  var AGENT_WORKSPACE_IDLE_TIMEOUT_SEC = 1800;
@@ -8572,12 +8466,6 @@ var AGENT_WORKSPACE_KEEP_LAST_SNAPSHOTS = 1;
8572
8466
  function isLegacyForkParams(opts) {
8573
8467
  return "name" in opts || "metadata" in opts;
8574
8468
  }
8575
- async function* dropWatchHandshake(source) {
8576
- for await (const event of source) {
8577
- if (event.type === "connected") continue;
8578
- yield event;
8579
- }
8580
- }
8581
8469
  function unwrap48(payload) {
8582
8470
  if (payload !== null && typeof payload === "object" && "data" in payload && payload.data !== void 0) {
8583
8471
  return payload.data;
@@ -8622,31 +8510,9 @@ function createBody(params = {}) {
8622
8510
  if (legacyPersistencePolicy) metadata.miosa_persistent = persistent;
8623
8511
  const cpuCount = params.cpuCount ?? params.cpu_count;
8624
8512
  const memoryMb = params.memoryMb ?? params.memory_mb;
8625
- const diskMb = params.diskMb ?? params.disk_mb ?? params.diskSizeMb ?? params.disk_size_mb;
8626
- const suppliedResources = [cpuCount, memoryMb, diskMb].filter(
8627
- (value) => value !== void 0
8628
- ).length;
8629
- if (suppliedResources !== 0 && suppliedResources !== 3) {
8630
- throw new TypeError(
8631
- "Raw sandbox resources require cpuCount, memoryMb, and diskSizeMb together. Prefer size."
8632
- );
8633
- }
8634
- let resolvedSize = params.size;
8635
- if (suppliedResources === 3) {
8636
- const matchingSize = Object.entries(SANDBOX_SHAPE_CONTRACTS).find(
8637
- ([, contract]) => contract.cpuCount === cpuCount && contract.memoryMb === memoryMb && contract.diskSizeMb === diskMb
8638
- )?.[0];
8639
- if (!matchingSize) {
8640
- throw new TypeError(
8641
- "Raw sandbox resources must exactly match a named size contract."
8642
- );
8643
- }
8644
- if (resolvedSize && resolvedSize !== matchingSize) {
8645
- throw new TypeError(
8646
- `Raw sandbox resources match ${matchingSize}, not requested size ${resolvedSize}.`
8647
- );
8648
- }
8649
- resolvedSize = matchingSize;
8513
+ const resolvedSize = params.size;
8514
+ if (cpuCount !== void 0 || memoryMb !== void 0) {
8515
+ assertPublishedShape(cpuCount, memoryMb);
8650
8516
  }
8651
8517
  if (snapshotExpirationSec !== void 0) {
8652
8518
  metadata.snapshot_expiration_sec = snapshotExpirationSec;
@@ -8692,6 +8558,19 @@ function createBody(params = {}) {
8692
8558
  external_project_id: params.externalProjectId ?? params.external_project_id
8693
8559
  });
8694
8560
  }
8561
+ function assertPublishedShape(cpuCount, memoryMb) {
8562
+ const tier = cpuCount === void 0 ? void 0 : SANDBOX_TIER_BY_CPU[cpuCount];
8563
+ if (tier && tier.memoryMb === memoryMb) return;
8564
+ const supplied = `${cpuCount ?? "?"} vCPU / ${memoryMb ?? "?"} MiB`;
8565
+ if (tier) {
8566
+ throw new TypeError(
8567
+ `Unsupported cpu/memory combination (${supplied}); nearest supported is ${tier.size} (${cpuCount} vCPU / ${tier.memoryMb} MiB). Pass size: "${tier.size}" or memoryMb: ${tier.memoryMb}. Custom disk sizes are allowed on top of any tier.`
8568
+ );
8569
+ }
8570
+ throw new TypeError(
8571
+ `Unsupported cpu/memory combination (${supplied}). Supported tiers: xs (1/2048), small (2/4096), medium (4/8192), large (8/16384), xl (16/32768). Pass a matching cpuCount + memoryMb (any diskSizeMb is allowed) or use size.`
8572
+ );
8573
+ }
8695
8574
  function execBody(command, options = {}) {
8696
8575
  return stripUndefined26({
8697
8576
  command,
@@ -8700,6 +8579,19 @@ function execBody(command, options = {}) {
8700
8579
  timeout: options.timeout ?? options.timeoutSec ?? options.timeout_sec
8701
8580
  });
8702
8581
  }
8582
+ function normalizeExecEvent(event, payload) {
8583
+ const record = payload !== null && typeof payload === "object" ? payload : {};
8584
+ const isExit = event === "exit" || record.exit_code !== void 0 || record.exitCode !== void 0 || event === null && typeof payload === "number";
8585
+ if (isExit) {
8586
+ const code = Number(
8587
+ record.exit_code ?? record.exitCode ?? (typeof payload === "number" ? payload : 0)
8588
+ );
8589
+ return { type: "exit", exit_code: code, exitCode: code };
8590
+ }
8591
+ const type = event === "stderr" ? "stderr" : "stdout";
8592
+ const data = typeof payload === "string" ? payload : String(record.line ?? record.data ?? "");
8593
+ return { type, data, line: data };
8594
+ }
8703
8595
  function stripUndefined26(input) {
8704
8596
  return Object.fromEntries(
8705
8597
  Object.entries(input).filter(([, value]) => value !== void 0)
@@ -8777,30 +8669,11 @@ var SandboxFiles = class {
8777
8669
  }
8778
8670
  return response;
8779
8671
  }
8780
- /**
8781
- * GET /api/v1/sandboxes/{id}/files/watch (SSE) — live file change events.
8782
- *
8783
- * SANDBOXES-HOST ONLY. This route is declared in `Web.Router.Sandboxes`
8784
- * and not in `Web.Router.Platform`, so it 404s against the SDK's default
8785
- * `https://api.miosa.ai/api/v1` base URL. Construct the client with
8786
- * `baseUrl: "https://sandboxes.miosa.ai/api/v1"` to reach it, or poll
8787
- * {@link list}/{@link stat} instead. See `./sandbox-host-routes.ts`.
8788
- *
8789
- * The server opens with a handshake record (`event: connected` /
8790
- * `data: {"sandbox_id":...}`) before any change events. That record is
8791
- * not a file change, so it is dropped here rather than yielded as a
8792
- * `SandboxFileChange` with `type: "connected"` and no `path`. Real change
8793
- * records carry their own `type` (`created` | `modified` | `deleted`) in
8794
- * the JSON body, so the SSE `event: file_change` name never overwrites it.
8795
- */
8672
+ /** GET /api/v1/sandboxes/{id}/files/watch (SSE) — live file change events. */
8796
8673
  watch() {
8797
8674
  const http = this.sandbox.http;
8798
- return dropWatchHandshake(
8799
- streamSandboxesHostRoute(
8800
- http,
8801
- `/sandboxes/${this.sandbox.id}/files/watch`,
8802
- "poll list()/stat() instead"
8803
- )
8675
+ return http.stream(
8676
+ `/sandboxes/${this.sandbox.id}/files/watch`
8804
8677
  );
8805
8678
  }
8806
8679
  };
@@ -9149,13 +9022,18 @@ var Sandbox = class _Sandbox {
9149
9022
  }
9150
9023
  execStream(command, options) {
9151
9024
  this.assertRunning("exec.stream");
9152
- return this.http.stream(
9025
+ const frames = this.http.streamFrames(
9153
9026
  `/sandboxes/${this.id}/exec/stream`,
9154
9027
  {
9155
9028
  method: "POST",
9156
9029
  body: execBody(command, options)
9157
9030
  }
9158
9031
  );
9032
+ return (async function* () {
9033
+ for await (const frame of frames) {
9034
+ yield normalizeExecEvent(frame.event, frame.data);
9035
+ }
9036
+ })();
9159
9037
  }
9160
9038
  async writeFile(path, content) {
9161
9039
  this.assertRunning("writeFile");