@miosa/sdk 2.0.6 → 3.0.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.js CHANGED
@@ -168,7 +168,7 @@ var TokenRefreshFailedError = class extends MiosaError {
168
168
  };
169
169
 
170
170
  // src/version.ts
171
- var SDK_VERSION = "2.0.6";
171
+ var SDK_VERSION = "3.0.0";
172
172
  var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
173
173
 
174
174
  // src/http.ts
@@ -198,6 +198,7 @@ async function ensureHttp2Agent() {
198
198
  }
199
199
  }
200
200
  ensureHttp2Agent();
201
+ var SSE_ACCEPT = "text/event-stream, application/json;q=0.9";
201
202
  var DEFAULT_TIMEOUT = 3e4;
202
203
  var DEFAULT_MAX_RETRIES = 3;
203
204
  var RETRY_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
@@ -370,7 +371,7 @@ var HttpClient = class {
370
371
  async *stream(path, options = {}) {
371
372
  const method = options.method ?? "GET";
372
373
  let headers = this.baseHeaders({
373
- Accept: "text/event-stream",
374
+ Accept: SSE_ACCEPT,
374
375
  ...options.headers
375
376
  });
376
377
  let body5 = null;
@@ -408,12 +409,14 @@ var HttpClient = class {
408
409
  response.headers.get("x-request-id") ?? void 0
409
410
  );
410
411
  }
412
+ if (response.status === 204) return;
411
413
  if (!response.body) {
412
414
  throw new MiosaError("SSE stream has no body", 0, "NO_STREAM_BODY");
413
415
  }
414
416
  const reader = response.body.getReader();
415
417
  const decoder = new TextDecoder();
416
418
  let buffer = "";
419
+ let eventName;
417
420
  try {
418
421
  while (true) {
419
422
  const { done, value } = await reader.read();
@@ -422,11 +425,22 @@ var HttpClient = class {
422
425
  const lines = buffer.split("\n");
423
426
  buffer = lines.pop() ?? "";
424
427
  for (const line of lines) {
428
+ if (line.startsWith("event:")) {
429
+ eventName = line.slice(6).trim();
430
+ continue;
431
+ }
432
+ if (line === "") {
433
+ eventName = void 0;
434
+ continue;
435
+ }
425
436
  if (line.startsWith("data:")) {
426
437
  const raw = line.slice(5).trim();
438
+ const name = eventName;
439
+ eventName = void 0;
427
440
  if (raw === "[DONE]" || raw === "") continue;
428
441
  try {
429
- yield JSON.parse(raw);
442
+ const parsed = JSON.parse(raw);
443
+ yield name !== void 0 && parsed !== null && typeof parsed === "object" && !("type" in parsed) ? { type: name, ...parsed } : parsed;
430
444
  } catch {
431
445
  }
432
446
  }
@@ -7774,41 +7788,130 @@ var Jobs = class {
7774
7788
  return `/opencomputers/hosts/${hostId}`;
7775
7789
  }
7776
7790
  /**
7777
- * Dispatch a command to run on the remote host.
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.
7778
7800
  */
7779
7801
  async run(hostId, params) {
7780
- return this.http.post(`${this.base(hostId)}/exec`, params);
7802
+ return this.http.post(`${this.base(hostId)}/exec`, {
7803
+ ...params,
7804
+ stream: false
7805
+ });
7781
7806
  }
7782
7807
  /**
7783
- * List all jobs for a host.
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`.
7784
7812
  */
7785
- async list(hostId) {
7786
- return this.http.get(`${this.base(hostId)}/exec`);
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
+ );
7787
7836
  }
7788
7837
  /**
7789
7838
  * Fetch the current state of a job.
7839
+ *
7840
+ * The server wraps the record in a `job` envelope; this unwraps it.
7790
7841
  */
7791
7842
  async get(hostId, jobId) {
7792
- return this.http.get(`${this.base(hostId)}/exec/${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;
7793
7850
  }
7794
7851
  /**
7795
- * Stream live output from a running job.
7852
+ * Re-attach to a job that is already running and stream what is left.
7796
7853
  *
7797
- * Yields `JobEvent` objects with `type` of `stdout`, `stderr`, `exit`, or
7798
- * `done`. Break the loop when you receive `done` or `exit`.
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.
7799
7857
  */
7800
7858
  stream(hostId, jobId) {
7801
- return this.http.stream(
7802
- `${this.base(hostId)}/exec/${jobId}/stream`
7859
+ return translateExecStream(
7860
+ this.http.stream(`${this.base(hostId)}/exec/${jobId}/stream`)
7803
7861
  );
7804
7862
  }
7805
7863
  /**
7806
7864
  * Cancel a running or queued job.
7865
+ *
7866
+ * Answers `204` on success and `409` when the job is already terminal.
7807
7867
  */
7808
7868
  async cancel(hostId, jobId) {
7809
7869
  return this.http.delete(`${this.base(hostId)}/exec/${jobId}`);
7810
7870
  }
7811
7871
  };
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
+ }
7812
7915
 
7813
7916
  // src/resources/open-computers/secrets.ts
7814
7917
  var Secrets = class {
@@ -8419,6 +8522,32 @@ var RuntimeCapabilitiesResource = class {
8419
8522
  }
8420
8523
  };
8421
8524
 
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
+
8422
8551
  // src/resources/sandboxes.ts
8423
8552
  function encodeContent(content) {
8424
8553
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
@@ -8443,6 +8572,12 @@ var AGENT_WORKSPACE_KEEP_LAST_SNAPSHOTS = 1;
8443
8572
  function isLegacyForkParams(opts) {
8444
8573
  return "name" in opts || "metadata" in opts;
8445
8574
  }
8575
+ async function* dropWatchHandshake(source) {
8576
+ for await (const event of source) {
8577
+ if (event.type === "connected") continue;
8578
+ yield event;
8579
+ }
8580
+ }
8446
8581
  function unwrap48(payload) {
8447
8582
  if (payload !== null && typeof payload === "object" && "data" in payload && payload.data !== void 0) {
8448
8583
  return payload.data;
@@ -8642,11 +8777,30 @@ var SandboxFiles = class {
8642
8777
  }
8643
8778
  return response;
8644
8779
  }
8645
- /** GET /api/v1/sandboxes/{id}/files/watch (SSE) — live file change events. */
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
+ */
8646
8796
  watch() {
8647
8797
  const http = this.sandbox.http;
8648
- return http.stream(
8649
- `/sandboxes/${this.sandbox.id}/files/watch`
8798
+ return dropWatchHandshake(
8799
+ streamSandboxesHostRoute(
8800
+ http,
8801
+ `/sandboxes/${this.sandbox.id}/files/watch`,
8802
+ "poll list()/stat() instead"
8803
+ )
8650
8804
  );
8651
8805
  }
8652
8806
  };
@@ -9398,19 +9552,40 @@ var Sandbox = class _Sandbox {
9398
9552
  const stream = options.stream ?? true;
9399
9553
  if (stream) {
9400
9554
  const sseResult = await this.tryReadinessStream(timeout);
9401
- if (sseResult !== null) return sseResult;
9555
+ if (sseResult !== null) {
9556
+ if (sseResult) await this.adoptReadyState();
9557
+ return sseResult;
9558
+ }
9402
9559
  }
9403
9560
  const deadlineMs = Date.now() + timeout * 1e3;
9404
9561
  while (Date.now() < deadlineMs) {
9405
9562
  try {
9406
9563
  const data = await this.readiness();
9407
- if (data.ready === true || data.status === "ready") return true;
9564
+ if (data.ready === true || data.status === "ready") {
9565
+ await this.adoptReadyState();
9566
+ return true;
9567
+ }
9408
9568
  } catch {
9409
9569
  }
9410
9570
  await new Promise((resolve) => setTimeout(resolve, 10));
9411
9571
  }
9412
9572
  return false;
9413
9573
  }
9574
+ /**
9575
+ * Readiness answers from the server; `assertRunning` reads the local
9576
+ * snapshot. Leaving that snapshot behind meant a caller could await
9577
+ * `waitUntilReady()`, receive `true`, and have the very next call refused
9578
+ * for being "provisioning" — the sandbox was running the whole time, only
9579
+ * this object had not been told. Nothing here can fail the wait: readiness
9580
+ * has already answered, so a refresh that does not land is not the caller's
9581
+ * problem.
9582
+ */
9583
+ async adoptReadyState() {
9584
+ try {
9585
+ await this.refresh();
9586
+ } catch {
9587
+ }
9588
+ }
9414
9589
  /**
9415
9590
  * Returns `true` / `false` for terminal SSE events, or `null` if the
9416
9591
  * stream endpoint is unavailable (404 or transport error) so callers