@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/README.md +44 -0
- package/dist/index.d.ts +185 -43
- package/dist/index.js +194 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 = "
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
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`,
|
|
7802
|
+
return this.http.post(`${this.base(hostId)}/exec`, {
|
|
7803
|
+
...params,
|
|
7804
|
+
stream: false
|
|
7805
|
+
});
|
|
7781
7806
|
}
|
|
7782
7807
|
/**
|
|
7783
|
-
*
|
|
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
|
-
|
|
7786
|
-
return
|
|
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
|
-
|
|
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
|
-
*
|
|
7852
|
+
* Re-attach to a job that is already running and stream what is left.
|
|
7796
7853
|
*
|
|
7797
|
-
*
|
|
7798
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
8649
|
-
|
|
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)
|
|
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")
|
|
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
|