@arker-ai/sdk 0.1.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 +136 -0
- package/dist/index.cjs +321 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +289 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Arker — TypeScript SDK
|
|
2
|
+
|
|
3
|
+
TypeScript client for the [Arker](https://arker.ai) virtual computer
|
|
4
|
+
platform. Spawn isolated Linux sandboxes, run shell / Python / Node
|
|
5
|
+
code in them, read and write files. Zero runtime dependencies (uses
|
|
6
|
+
the platform's built-in `fetch` and `crypto`).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @arker-ai/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Works in Node ≥ 18. ESM + CJS + types all included.
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Arker, ArkerError } from "@arker-ai/sdk";
|
|
20
|
+
|
|
21
|
+
const arker = new Arker({ apiKey: "ark_live_..." });
|
|
22
|
+
const vm = await arker.vm("arkuntu").fork({ name: "hello" });
|
|
23
|
+
|
|
24
|
+
const result = await vm.run("python3 -c 'print(2+2)'");
|
|
25
|
+
console.log(new TextDecoder().decode(result.stdout)); // → "4\n"
|
|
26
|
+
|
|
27
|
+
await vm.sync.writeFile("/home/user/data.csv", "a,b\n1,2\n");
|
|
28
|
+
const data = await vm.sync.readFile("/home/user/data.csv");
|
|
29
|
+
|
|
30
|
+
const child = await vm.fork({ name: "branch" }); // constant-time copy-on-write
|
|
31
|
+
await child.delete();
|
|
32
|
+
await vm.delete();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
List your VMs:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const page = await arker.list({ limit: 10, sort: "-created_at" });
|
|
39
|
+
console.log(`${page.total} total`);
|
|
40
|
+
for (const summary of page.items) {
|
|
41
|
+
console.log(summary.vm_id, summary.name, summary.region, summary.created_at);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
new Arker({ apiKey, baseUrl? })
|
|
49
|
+
.vm(vmId) -> Computer // open handle (no network call)
|
|
50
|
+
.list({ limit?, offset?, q?, sort? }) -> Promise<VmList>
|
|
51
|
+
|
|
52
|
+
Computer
|
|
53
|
+
.id, .delete()
|
|
54
|
+
.fork({ name?, isPublic?, region? }) -> Promise<Computer>
|
|
55
|
+
.run(command, { sessionId?, timeout? }) -> Promise<RunResult>
|
|
56
|
+
.sync.readFile(path) -> Promise<Uint8Array>
|
|
57
|
+
.sync.writeFile(path, data: Uint8Array | string) -> Promise<void>
|
|
58
|
+
|
|
59
|
+
RunResult: stdout, stderr (Uint8Array), exitCode, durationMs, sessionId, cwd
|
|
60
|
+
VmSummary: vm_id, name, base_image, region, created_at (ISO 8601)
|
|
61
|
+
VmList: items (VmSummary[]), total (number)
|
|
62
|
+
|
|
63
|
+
ArkerError(code, message, status) extends Error // single error type
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Routing
|
|
67
|
+
|
|
68
|
+
`fork`, `run`, `sync`, and `delete` use the regional endpoint set on the
|
|
69
|
+
client (default `https://aws-us-west-2.burst.arker.ai`).
|
|
70
|
+
|
|
71
|
+
`list` always goes through `https://arker.ai` regardless of `baseUrl`,
|
|
72
|
+
because list data is served from a global host rather than a regional
|
|
73
|
+
one.
|
|
74
|
+
|
|
75
|
+
Public base-image names like `"arkuntu"` resolve to a ULID **client-side**
|
|
76
|
+
(see `SOURCE_ALIASES` in `src/index.ts`), so `arker.vm("arkuntu").fork()`
|
|
77
|
+
works on the default endpoint with no extra round-trip. Override
|
|
78
|
+
`baseUrl` or set `ARKER_BASE_URL` to point at a different region or a
|
|
79
|
+
self-hosted deployment.
|
|
80
|
+
|
|
81
|
+
### Errors
|
|
82
|
+
|
|
83
|
+
Every server-side error becomes an `ArkerError`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
try {
|
|
87
|
+
await vm.sync.readFile("/home/user/missing");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof ArkerError) {
|
|
90
|
+
console.log(err.code); // "not_found"
|
|
91
|
+
console.log(err.message); // "not_found: file not found: ..."
|
|
92
|
+
console.log(err.status); // 404
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`code` is a stable enum: `bad_request`, `unauthorized`, `payment_required`,
|
|
98
|
+
`forbidden`, `not_found`, `conflict`, `payload_too_large`, `internal`,
|
|
99
|
+
`not_implemented`, `vm_busy`, `unsupported_*`, `command_not_found`.
|
|
100
|
+
|
|
101
|
+
### What the SDK does for you
|
|
102
|
+
|
|
103
|
+
Hidden behind these six methods:
|
|
104
|
+
|
|
105
|
+
- **Write strategy**: files up to 100 MB. Small payloads go in one call;
|
|
106
|
+
larger ones use a direct upload path so the bytes don't traverse the
|
|
107
|
+
API layer. `writeFile` resolves once the bytes are durably stored.
|
|
108
|
+
- **Read coalescing**: `readFile` always resolves to a `Uint8Array`,
|
|
109
|
+
regardless of whether the server inlined the content or returned a
|
|
110
|
+
signed URL.
|
|
111
|
+
- **Idempotent retry**: transient errors are retried with exponential
|
|
112
|
+
backoff. Writes are server-side idempotent on `upload_id`, so retries
|
|
113
|
+
never produce duplicates.
|
|
114
|
+
- **Path validation**: only `/home/user/...` paths accepted; `..` rejected.
|
|
115
|
+
|
|
116
|
+
## Demo / smoke test
|
|
117
|
+
|
|
118
|
+
Run the full surface against a live deployment:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
ARKER_API_KEY=ark_live_... npx tsx tests/demo.ts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It exercises every method (`list`, `vm`, `fork`, `run`, `sync.writeFile`,
|
|
125
|
+
`sync.readFile`, error path, child fork, `delete`) and prints what each
|
|
126
|
+
call hits on the wire — useful as living documentation.
|
|
127
|
+
|
|
128
|
+
To fork from a specific source VM instead of the default `arkuntu`:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
ARKER_API_KEY=ark_live_... ARKER_SOURCE_VM=01KQ... npx tsx tests/demo.ts
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
Apache-2.0.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Arker: () => Arker,
|
|
24
|
+
ArkerError: () => ArkerError,
|
|
25
|
+
CHUNK_SIZE: () => CHUNK_SIZE,
|
|
26
|
+
Computer: () => Computer,
|
|
27
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
28
|
+
LIST_BASE_URL: () => LIST_BASE_URL,
|
|
29
|
+
SOURCE_ALIASES: () => SOURCE_ALIASES,
|
|
30
|
+
Sync: () => Sync
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
var DEFAULT_BASE_URL = "https://aws-us-west-2.burst.arker.ai";
|
|
34
|
+
var LIST_BASE_URL = "https://arker.ai";
|
|
35
|
+
var SOURCE_ALIASES = {
|
|
36
|
+
arkuntu: "01KQBYKEV5WJ7YB010603T1DCT_d8c0"
|
|
37
|
+
};
|
|
38
|
+
var CHUNK_SIZE = 4 * 1024 * 1024;
|
|
39
|
+
var PRESIGN_PUT_TIMEOUT_MS = 6e5;
|
|
40
|
+
var RETRYABLE_HTTP = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
41
|
+
var TRANSIENT_HINTS = ["503", "Service Unavailable", "throttle", "SlowDown", "ThrottlingException"];
|
|
42
|
+
var MAX_ATTEMPTS = 4;
|
|
43
|
+
var BACKOFF_MS = 200;
|
|
44
|
+
var ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
45
|
+
var ArkerError = class extends Error {
|
|
46
|
+
code;
|
|
47
|
+
status;
|
|
48
|
+
constructor(code, message, status) {
|
|
49
|
+
super(`${code}: ${message}`);
|
|
50
|
+
this.name = "ArkerError";
|
|
51
|
+
this.code = code;
|
|
52
|
+
this.status = status;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
56
|
+
var jitterMs = () => Math.floor(Math.random() * 50);
|
|
57
|
+
function ulid() {
|
|
58
|
+
const time = BigInt(Date.now()) & (1n << 48n) - 1n;
|
|
59
|
+
const rand = new Uint8Array(10);
|
|
60
|
+
crypto.getRandomValues(rand);
|
|
61
|
+
let raw = time << 80n | rand.reduce((acc, b) => acc << 8n | BigInt(b), 0n);
|
|
62
|
+
const out = [];
|
|
63
|
+
for (let i = 0; i < 26; i++) {
|
|
64
|
+
out.push(ULID_ALPHABET[Number(raw & 31n)]);
|
|
65
|
+
raw >>= 5n;
|
|
66
|
+
}
|
|
67
|
+
return out.reverse().join("");
|
|
68
|
+
}
|
|
69
|
+
function looksLikeVmId(s) {
|
|
70
|
+
let head = s;
|
|
71
|
+
if (s.includes("_")) {
|
|
72
|
+
const [h, ...rest] = s.split("_");
|
|
73
|
+
const tail = rest.join("_");
|
|
74
|
+
if (!tail || !/^[A-Za-z0-9]+$/.test(tail)) return false;
|
|
75
|
+
head = h;
|
|
76
|
+
}
|
|
77
|
+
if (head.length !== 26) return false;
|
|
78
|
+
return [...head.toUpperCase()].every((c) => ULID_ALPHABET.includes(c));
|
|
79
|
+
}
|
|
80
|
+
function decodeStream(text, encoding) {
|
|
81
|
+
const s = typeof text === "string" ? text : "";
|
|
82
|
+
if (encoding === "base64") {
|
|
83
|
+
try {
|
|
84
|
+
const bin = atob(s);
|
|
85
|
+
const out = new Uint8Array(bin.length);
|
|
86
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
87
|
+
return out;
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return new TextEncoder().encode(s);
|
|
92
|
+
}
|
|
93
|
+
function bytesToBase64(data) {
|
|
94
|
+
let bin = "";
|
|
95
|
+
for (let i = 0; i < data.length; i++) bin += String.fromCharCode(data[i]);
|
|
96
|
+
return btoa(bin);
|
|
97
|
+
}
|
|
98
|
+
function isTransient(err) {
|
|
99
|
+
if (!err || err.code !== "internal") return false;
|
|
100
|
+
const msg = err.message ?? "";
|
|
101
|
+
return TRANSIENT_HINTS.some((h) => msg.includes(h));
|
|
102
|
+
}
|
|
103
|
+
var Arker = class {
|
|
104
|
+
apiKey;
|
|
105
|
+
baseUrl;
|
|
106
|
+
constructor(opts) {
|
|
107
|
+
if (!opts.apiKey) throw new Error("apiKey is required");
|
|
108
|
+
this.apiKey = opts.apiKey;
|
|
109
|
+
const base = opts.baseUrl ?? process.env.ARKER_BASE_URL ?? DEFAULT_BASE_URL;
|
|
110
|
+
this.baseUrl = base.replace(/\/+$/, "");
|
|
111
|
+
}
|
|
112
|
+
/** @internal */
|
|
113
|
+
async _request(method, path, body, overrideBaseUrl) {
|
|
114
|
+
const url = (overrideBaseUrl ? overrideBaseUrl.replace(/\/+$/, "") : this.baseUrl) + path;
|
|
115
|
+
const headers = {
|
|
116
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
117
|
+
"content-type": "application/json"
|
|
118
|
+
};
|
|
119
|
+
let lastStatus = 0;
|
|
120
|
+
let lastErr = null;
|
|
121
|
+
let lastText = "";
|
|
122
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
123
|
+
const resp = await fetch(url, {
|
|
124
|
+
method,
|
|
125
|
+
headers,
|
|
126
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
127
|
+
});
|
|
128
|
+
const text = await resp.text();
|
|
129
|
+
let payload = null;
|
|
130
|
+
try {
|
|
131
|
+
payload = text ? JSON.parse(text) : {};
|
|
132
|
+
} catch {
|
|
133
|
+
payload = null;
|
|
134
|
+
}
|
|
135
|
+
lastStatus = resp.status;
|
|
136
|
+
lastText = text;
|
|
137
|
+
const envelopeErr = payload && typeof payload === "object" && payload.ok === false ? payload.error : null;
|
|
138
|
+
lastErr = envelopeErr;
|
|
139
|
+
if (RETRYABLE_HTTP.has(resp.status) || isTransient(envelopeErr)) {
|
|
140
|
+
if (attempt === MAX_ATTEMPTS - 1) break;
|
|
141
|
+
await sleep(BACKOFF_MS * 2 ** attempt + jitterMs());
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (envelopeErr) {
|
|
145
|
+
throw new ArkerError(envelopeErr.code ?? "internal", envelopeErr.message ?? "", resp.status);
|
|
146
|
+
}
|
|
147
|
+
if (resp.status >= 400) {
|
|
148
|
+
throw new ArkerError("internal", text.slice(0, 200) || "request failed", resp.status);
|
|
149
|
+
}
|
|
150
|
+
return payload;
|
|
151
|
+
}
|
|
152
|
+
if (lastErr) {
|
|
153
|
+
throw new ArkerError(lastErr.code ?? "internal", lastErr.message ?? "", lastStatus);
|
|
154
|
+
}
|
|
155
|
+
throw new ArkerError("internal", lastText.slice(0, 200) || "no response", lastStatus);
|
|
156
|
+
}
|
|
157
|
+
/** Open a handle to a VM by ULID *or* template name. No network call. */
|
|
158
|
+
vm(vmId) {
|
|
159
|
+
return new Computer(this, vmId);
|
|
160
|
+
}
|
|
161
|
+
/** List VMs in the caller's organization. Always hits `https://arker.ai`. */
|
|
162
|
+
async list(opts = {}) {
|
|
163
|
+
const params = new URLSearchParams();
|
|
164
|
+
if (opts.limit !== void 0 && opts.limit !== 25) params.set("limit", String(opts.limit));
|
|
165
|
+
if (opts.offset !== void 0 && opts.offset !== 0) params.set("offset", String(opts.offset));
|
|
166
|
+
if (opts.q !== void 0) params.set("q", opts.q);
|
|
167
|
+
if (opts.sort !== void 0) params.set("sort", opts.sort);
|
|
168
|
+
const qs = params.toString();
|
|
169
|
+
const path = "/api/v1/vms/list" + (qs ? `?${qs}` : "");
|
|
170
|
+
const r = await this._request("GET", path, void 0, LIST_BASE_URL);
|
|
171
|
+
return { items: r.items ?? [], total: r.total ?? 0 };
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var Computer = class _Computer {
|
|
175
|
+
id;
|
|
176
|
+
sync;
|
|
177
|
+
/** @internal */
|
|
178
|
+
_client;
|
|
179
|
+
constructor(client, vmId) {
|
|
180
|
+
this._client = client;
|
|
181
|
+
this.id = vmId;
|
|
182
|
+
this.sync = new Sync(this);
|
|
183
|
+
}
|
|
184
|
+
async delete() {
|
|
185
|
+
await this._client._request("DELETE", `/api/v1/vms/${this.id}`);
|
|
186
|
+
}
|
|
187
|
+
/** Fork this VM. Aliases like `"arkuntu"` resolve client-side. */
|
|
188
|
+
async fork(opts = {}) {
|
|
189
|
+
const body = { is_public: opts.isPublic ?? false };
|
|
190
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
191
|
+
if (opts.region !== void 0) body.region = opts.region;
|
|
192
|
+
const resolved = SOURCE_ALIASES[this.id] ?? this.id;
|
|
193
|
+
let r;
|
|
194
|
+
if (looksLikeVmId(resolved)) {
|
|
195
|
+
r = await this._client._request("POST", `/api/v1/vms/${resolved}/fork`, body);
|
|
196
|
+
} else {
|
|
197
|
+
body.from = this.id;
|
|
198
|
+
r = await this._client._request("POST", "/api/v1/vms/fork", body, LIST_BASE_URL);
|
|
199
|
+
}
|
|
200
|
+
if (!r.vm_id) throw new ArkerError("internal", "fork response missing vm_id", 200);
|
|
201
|
+
return new _Computer(this._client, r.vm_id);
|
|
202
|
+
}
|
|
203
|
+
async run(command, opts = {}) {
|
|
204
|
+
const body = {
|
|
205
|
+
command,
|
|
206
|
+
session_id: opts.sessionId ?? 0
|
|
207
|
+
};
|
|
208
|
+
if (opts.timeout !== void 0) body.timeout = opts.timeout;
|
|
209
|
+
const r = await this._client._request("POST", `/api/v1/vms/${this.id}/run`, body);
|
|
210
|
+
return {
|
|
211
|
+
stdout: decodeStream(r.stdout, r.stdout_encoding),
|
|
212
|
+
stderr: decodeStream(r.stderr, r.stderr_encoding),
|
|
213
|
+
exitCode: Number(r.exit_code ?? 0),
|
|
214
|
+
durationMs: Number(r.duration_ms ?? 0),
|
|
215
|
+
sessionId: String(r.session_id ?? ""),
|
|
216
|
+
cwd: String(r.cwd ?? "")
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var Sync = class {
|
|
221
|
+
/** @internal */
|
|
222
|
+
_vm;
|
|
223
|
+
constructor(vm) {
|
|
224
|
+
this._vm = vm;
|
|
225
|
+
}
|
|
226
|
+
path() {
|
|
227
|
+
return `/api/v1/vms/${this._vm.id}/sync`;
|
|
228
|
+
}
|
|
229
|
+
async readFile(path) {
|
|
230
|
+
const r = await this._vm._client._request("POST", this.path(), { op: "read", path });
|
|
231
|
+
if (r.content !== void 0 && r.content !== null) {
|
|
232
|
+
return decodeStream(r.content, r.encoding);
|
|
233
|
+
}
|
|
234
|
+
if (r.presigned_url) {
|
|
235
|
+
const resp = await fetch(r.presigned_url);
|
|
236
|
+
if (!resp.ok) {
|
|
237
|
+
throw new ArkerError("internal", `signed GET failed: ${resp.status}`, resp.status);
|
|
238
|
+
}
|
|
239
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
240
|
+
}
|
|
241
|
+
throw new ArkerError("internal", "read response missing content/presigned_url", 200);
|
|
242
|
+
}
|
|
243
|
+
async writeFile(path, data) {
|
|
244
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
245
|
+
if (!bytes.length) throw new ArkerError("bad_request", "writeFile: empty data", 400);
|
|
246
|
+
if (bytes.length <= CHUNK_SIZE) {
|
|
247
|
+
await this.fastPath(path, bytes);
|
|
248
|
+
} else {
|
|
249
|
+
await this.presignedPath(path, bytes);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async fastPath(path, data) {
|
|
253
|
+
const size = data.length;
|
|
254
|
+
const result = await this.sendOneWrite({
|
|
255
|
+
path,
|
|
256
|
+
size,
|
|
257
|
+
upload_id: ulid(),
|
|
258
|
+
start: 0,
|
|
259
|
+
end: size,
|
|
260
|
+
content: bytesToBase64(data)
|
|
261
|
+
});
|
|
262
|
+
if (!(result.complete && result.written)) {
|
|
263
|
+
throw new ArkerError("internal", "fast-path write returned without complete+written", 200);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async presignedPath(path, data) {
|
|
267
|
+
const size = data.length;
|
|
268
|
+
const e1 = await this.sendOneWrite({ path, size, presigned: true });
|
|
269
|
+
const url = e1.presigned_url;
|
|
270
|
+
const uploadId = e1.upload_id;
|
|
271
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
272
|
+
const ctrl = new AbortController();
|
|
273
|
+
const t = setTimeout(() => ctrl.abort(), PRESIGN_PUT_TIMEOUT_MS);
|
|
274
|
+
try {
|
|
275
|
+
const resp = await fetch(url, { method: "PUT", body: data, signal: ctrl.signal });
|
|
276
|
+
clearTimeout(t);
|
|
277
|
+
if (resp.ok) break;
|
|
278
|
+
if (!RETRYABLE_HTTP.has(resp.status) || attempt === MAX_ATTEMPTS - 1) {
|
|
279
|
+
throw new ArkerError("internal", `upload PUT failed: ${resp.status}`, resp.status);
|
|
280
|
+
}
|
|
281
|
+
await sleep(BACKOFF_MS * 2 ** attempt);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
clearTimeout(t);
|
|
284
|
+
if (err instanceof ArkerError) throw err;
|
|
285
|
+
if (attempt === MAX_ATTEMPTS - 1) {
|
|
286
|
+
throw new ArkerError("internal", `upload PUT failed: ${err.message}`, 0);
|
|
287
|
+
}
|
|
288
|
+
await sleep(BACKOFF_MS * 2 ** attempt);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
await this.sendOneWrite({ path, size, upload_id: uploadId });
|
|
292
|
+
}
|
|
293
|
+
async sendOneWrite(entry) {
|
|
294
|
+
let lastErr = null;
|
|
295
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
296
|
+
const r = await this._vm._client._request("POST", this.path(), {
|
|
297
|
+
op: "write",
|
|
298
|
+
writes: [entry]
|
|
299
|
+
});
|
|
300
|
+
const result = (r.results ?? [null])[0];
|
|
301
|
+
if (!result) throw new ArkerError("internal", "write response missing results[0]", 200);
|
|
302
|
+
const err = result.error ?? null;
|
|
303
|
+
if (!err) return result;
|
|
304
|
+
lastErr = err;
|
|
305
|
+
if (!isTransient(err) || attempt === MAX_ATTEMPTS - 1) break;
|
|
306
|
+
await sleep(BACKOFF_MS * 2 ** attempt + jitterMs());
|
|
307
|
+
}
|
|
308
|
+
throw new ArkerError(lastErr?.code ?? "internal", lastErr?.message ?? "write failed", 200);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
312
|
+
0 && (module.exports = {
|
|
313
|
+
Arker,
|
|
314
|
+
ArkerError,
|
|
315
|
+
CHUNK_SIZE,
|
|
316
|
+
Computer,
|
|
317
|
+
DEFAULT_BASE_URL,
|
|
318
|
+
LIST_BASE_URL,
|
|
319
|
+
SOURCE_ALIASES,
|
|
320
|
+
Sync
|
|
321
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arker SDK — TypeScript client.
|
|
3
|
+
*
|
|
4
|
+
* Quickstart:
|
|
5
|
+
*
|
|
6
|
+
* import { Arker } from "@arker-ai/sdk";
|
|
7
|
+
* const arker = new Arker({ apiKey: "ark_live_..." });
|
|
8
|
+
* const vm = await arker.vm("arkuntu").fork({ name: "hello" });
|
|
9
|
+
*
|
|
10
|
+
* const result = await vm.run("echo hi");
|
|
11
|
+
* console.log(new TextDecoder().decode(result.stdout)); // → "hi\n"
|
|
12
|
+
*
|
|
13
|
+
* await vm.sync.writeFile("/home/user/data.bin", new Uint8Array([1, 2, 3]));
|
|
14
|
+
* const blob = await vm.sync.readFile("/home/user/data.bin");
|
|
15
|
+
*
|
|
16
|
+
* const child = await vm.fork({ name: "branch" });
|
|
17
|
+
* await child.delete();
|
|
18
|
+
* await vm.delete();
|
|
19
|
+
*
|
|
20
|
+
* // List your VMs (paginated):
|
|
21
|
+
* const page = await arker.list({ limit: 10 });
|
|
22
|
+
* for (const summary of page.items) {
|
|
23
|
+
* console.log(summary.vm_id, summary.name, summary.created_at);
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Errors are thrown as `ArkerError(code, message, status)`.
|
|
27
|
+
*/
|
|
28
|
+
declare const DEFAULT_BASE_URL = "https://aws-us-west-2.burst.arker.ai";
|
|
29
|
+
/** `list` is served from a different host than the rest; used regardless of the client's baseUrl. */
|
|
30
|
+
declare const LIST_BASE_URL = "https://arker.ai";
|
|
31
|
+
/** Public base-image aliases resolved client-side so `vm("arkuntu").fork()` works on the default endpoint. */
|
|
32
|
+
declare const SOURCE_ALIASES: Record<string, string>;
|
|
33
|
+
/** Files above this size go through a direct upload path. */
|
|
34
|
+
declare const CHUNK_SIZE: number;
|
|
35
|
+
interface ArkerOptions {
|
|
36
|
+
apiKey: string;
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
interface ForkOptions {
|
|
40
|
+
name?: string;
|
|
41
|
+
isPublic?: boolean;
|
|
42
|
+
region?: string;
|
|
43
|
+
}
|
|
44
|
+
interface RunOptions {
|
|
45
|
+
sessionId?: string | number;
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}
|
|
48
|
+
interface RunResult {
|
|
49
|
+
stdout: Uint8Array;
|
|
50
|
+
stderr: Uint8Array;
|
|
51
|
+
exitCode: number;
|
|
52
|
+
durationMs: number;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
cwd: string;
|
|
55
|
+
}
|
|
56
|
+
interface VmSummary {
|
|
57
|
+
vm_id: string;
|
|
58
|
+
name: string | null;
|
|
59
|
+
base_image: string;
|
|
60
|
+
region: string;
|
|
61
|
+
/** ISO 8601 UTC. */
|
|
62
|
+
created_at: string;
|
|
63
|
+
}
|
|
64
|
+
interface VmList {
|
|
65
|
+
items: VmSummary[];
|
|
66
|
+
/** Total matching the query, ignoring limit/offset. */
|
|
67
|
+
total: number;
|
|
68
|
+
}
|
|
69
|
+
interface ListOptions {
|
|
70
|
+
limit?: number;
|
|
71
|
+
offset?: number;
|
|
72
|
+
q?: string;
|
|
73
|
+
/** `created_at` | `-created_at` | `region` | `-region`. Default `-created_at`. */
|
|
74
|
+
sort?: string;
|
|
75
|
+
}
|
|
76
|
+
declare class ArkerError extends Error {
|
|
77
|
+
readonly code: string;
|
|
78
|
+
readonly status: number;
|
|
79
|
+
constructor(code: string, message: string, status: number);
|
|
80
|
+
}
|
|
81
|
+
declare class Arker {
|
|
82
|
+
private readonly apiKey;
|
|
83
|
+
private readonly baseUrl;
|
|
84
|
+
constructor(opts: ArkerOptions);
|
|
85
|
+
/** @internal */
|
|
86
|
+
_request<T = any>(method: string, path: string, body?: unknown, overrideBaseUrl?: string): Promise<T>;
|
|
87
|
+
/** Open a handle to a VM by ULID *or* template name. No network call. */
|
|
88
|
+
vm(vmId: string): Computer;
|
|
89
|
+
/** List VMs in the caller's organization. Always hits `https://arker.ai`. */
|
|
90
|
+
list(opts?: ListOptions): Promise<VmList>;
|
|
91
|
+
}
|
|
92
|
+
declare class Computer {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly sync: Sync;
|
|
95
|
+
/** @internal */
|
|
96
|
+
readonly _client: Arker;
|
|
97
|
+
constructor(client: Arker, vmId: string);
|
|
98
|
+
delete(): Promise<void>;
|
|
99
|
+
/** Fork this VM. Aliases like `"arkuntu"` resolve client-side. */
|
|
100
|
+
fork(opts?: ForkOptions): Promise<Computer>;
|
|
101
|
+
run(command: string, opts?: RunOptions): Promise<RunResult>;
|
|
102
|
+
}
|
|
103
|
+
declare class Sync {
|
|
104
|
+
/** @internal */
|
|
105
|
+
readonly _vm: Computer;
|
|
106
|
+
constructor(vm: Computer);
|
|
107
|
+
private path;
|
|
108
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
109
|
+
writeFile(path: string, data: Uint8Array | string): Promise<void>;
|
|
110
|
+
private fastPath;
|
|
111
|
+
private presignedPath;
|
|
112
|
+
private sendOneWrite;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { Arker, ArkerError, type ArkerOptions, CHUNK_SIZE, Computer, DEFAULT_BASE_URL, type ForkOptions, LIST_BASE_URL, type ListOptions, type RunOptions, type RunResult, SOURCE_ALIASES, Sync, type VmList, type VmSummary };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arker SDK — TypeScript client.
|
|
3
|
+
*
|
|
4
|
+
* Quickstart:
|
|
5
|
+
*
|
|
6
|
+
* import { Arker } from "@arker-ai/sdk";
|
|
7
|
+
* const arker = new Arker({ apiKey: "ark_live_..." });
|
|
8
|
+
* const vm = await arker.vm("arkuntu").fork({ name: "hello" });
|
|
9
|
+
*
|
|
10
|
+
* const result = await vm.run("echo hi");
|
|
11
|
+
* console.log(new TextDecoder().decode(result.stdout)); // → "hi\n"
|
|
12
|
+
*
|
|
13
|
+
* await vm.sync.writeFile("/home/user/data.bin", new Uint8Array([1, 2, 3]));
|
|
14
|
+
* const blob = await vm.sync.readFile("/home/user/data.bin");
|
|
15
|
+
*
|
|
16
|
+
* const child = await vm.fork({ name: "branch" });
|
|
17
|
+
* await child.delete();
|
|
18
|
+
* await vm.delete();
|
|
19
|
+
*
|
|
20
|
+
* // List your VMs (paginated):
|
|
21
|
+
* const page = await arker.list({ limit: 10 });
|
|
22
|
+
* for (const summary of page.items) {
|
|
23
|
+
* console.log(summary.vm_id, summary.name, summary.created_at);
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Errors are thrown as `ArkerError(code, message, status)`.
|
|
27
|
+
*/
|
|
28
|
+
declare const DEFAULT_BASE_URL = "https://aws-us-west-2.burst.arker.ai";
|
|
29
|
+
/** `list` is served from a different host than the rest; used regardless of the client's baseUrl. */
|
|
30
|
+
declare const LIST_BASE_URL = "https://arker.ai";
|
|
31
|
+
/** Public base-image aliases resolved client-side so `vm("arkuntu").fork()` works on the default endpoint. */
|
|
32
|
+
declare const SOURCE_ALIASES: Record<string, string>;
|
|
33
|
+
/** Files above this size go through a direct upload path. */
|
|
34
|
+
declare const CHUNK_SIZE: number;
|
|
35
|
+
interface ArkerOptions {
|
|
36
|
+
apiKey: string;
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
interface ForkOptions {
|
|
40
|
+
name?: string;
|
|
41
|
+
isPublic?: boolean;
|
|
42
|
+
region?: string;
|
|
43
|
+
}
|
|
44
|
+
interface RunOptions {
|
|
45
|
+
sessionId?: string | number;
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}
|
|
48
|
+
interface RunResult {
|
|
49
|
+
stdout: Uint8Array;
|
|
50
|
+
stderr: Uint8Array;
|
|
51
|
+
exitCode: number;
|
|
52
|
+
durationMs: number;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
cwd: string;
|
|
55
|
+
}
|
|
56
|
+
interface VmSummary {
|
|
57
|
+
vm_id: string;
|
|
58
|
+
name: string | null;
|
|
59
|
+
base_image: string;
|
|
60
|
+
region: string;
|
|
61
|
+
/** ISO 8601 UTC. */
|
|
62
|
+
created_at: string;
|
|
63
|
+
}
|
|
64
|
+
interface VmList {
|
|
65
|
+
items: VmSummary[];
|
|
66
|
+
/** Total matching the query, ignoring limit/offset. */
|
|
67
|
+
total: number;
|
|
68
|
+
}
|
|
69
|
+
interface ListOptions {
|
|
70
|
+
limit?: number;
|
|
71
|
+
offset?: number;
|
|
72
|
+
q?: string;
|
|
73
|
+
/** `created_at` | `-created_at` | `region` | `-region`. Default `-created_at`. */
|
|
74
|
+
sort?: string;
|
|
75
|
+
}
|
|
76
|
+
declare class ArkerError extends Error {
|
|
77
|
+
readonly code: string;
|
|
78
|
+
readonly status: number;
|
|
79
|
+
constructor(code: string, message: string, status: number);
|
|
80
|
+
}
|
|
81
|
+
declare class Arker {
|
|
82
|
+
private readonly apiKey;
|
|
83
|
+
private readonly baseUrl;
|
|
84
|
+
constructor(opts: ArkerOptions);
|
|
85
|
+
/** @internal */
|
|
86
|
+
_request<T = any>(method: string, path: string, body?: unknown, overrideBaseUrl?: string): Promise<T>;
|
|
87
|
+
/** Open a handle to a VM by ULID *or* template name. No network call. */
|
|
88
|
+
vm(vmId: string): Computer;
|
|
89
|
+
/** List VMs in the caller's organization. Always hits `https://arker.ai`. */
|
|
90
|
+
list(opts?: ListOptions): Promise<VmList>;
|
|
91
|
+
}
|
|
92
|
+
declare class Computer {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly sync: Sync;
|
|
95
|
+
/** @internal */
|
|
96
|
+
readonly _client: Arker;
|
|
97
|
+
constructor(client: Arker, vmId: string);
|
|
98
|
+
delete(): Promise<void>;
|
|
99
|
+
/** Fork this VM. Aliases like `"arkuntu"` resolve client-side. */
|
|
100
|
+
fork(opts?: ForkOptions): Promise<Computer>;
|
|
101
|
+
run(command: string, opts?: RunOptions): Promise<RunResult>;
|
|
102
|
+
}
|
|
103
|
+
declare class Sync {
|
|
104
|
+
/** @internal */
|
|
105
|
+
readonly _vm: Computer;
|
|
106
|
+
constructor(vm: Computer);
|
|
107
|
+
private path;
|
|
108
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
109
|
+
writeFile(path: string, data: Uint8Array | string): Promise<void>;
|
|
110
|
+
private fastPath;
|
|
111
|
+
private presignedPath;
|
|
112
|
+
private sendOneWrite;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { Arker, ArkerError, type ArkerOptions, CHUNK_SIZE, Computer, DEFAULT_BASE_URL, type ForkOptions, LIST_BASE_URL, type ListOptions, type RunOptions, type RunResult, SOURCE_ALIASES, Sync, type VmList, type VmSummary };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://aws-us-west-2.burst.arker.ai";
|
|
3
|
+
var LIST_BASE_URL = "https://arker.ai";
|
|
4
|
+
var SOURCE_ALIASES = {
|
|
5
|
+
arkuntu: "01KQBYKEV5WJ7YB010603T1DCT_d8c0"
|
|
6
|
+
};
|
|
7
|
+
var CHUNK_SIZE = 4 * 1024 * 1024;
|
|
8
|
+
var PRESIGN_PUT_TIMEOUT_MS = 6e5;
|
|
9
|
+
var RETRYABLE_HTTP = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
10
|
+
var TRANSIENT_HINTS = ["503", "Service Unavailable", "throttle", "SlowDown", "ThrottlingException"];
|
|
11
|
+
var MAX_ATTEMPTS = 4;
|
|
12
|
+
var BACKOFF_MS = 200;
|
|
13
|
+
var ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
14
|
+
var ArkerError = class extends Error {
|
|
15
|
+
code;
|
|
16
|
+
status;
|
|
17
|
+
constructor(code, message, status) {
|
|
18
|
+
super(`${code}: ${message}`);
|
|
19
|
+
this.name = "ArkerError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.status = status;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
var jitterMs = () => Math.floor(Math.random() * 50);
|
|
26
|
+
function ulid() {
|
|
27
|
+
const time = BigInt(Date.now()) & (1n << 48n) - 1n;
|
|
28
|
+
const rand = new Uint8Array(10);
|
|
29
|
+
crypto.getRandomValues(rand);
|
|
30
|
+
let raw = time << 80n | rand.reduce((acc, b) => acc << 8n | BigInt(b), 0n);
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = 0; i < 26; i++) {
|
|
33
|
+
out.push(ULID_ALPHABET[Number(raw & 31n)]);
|
|
34
|
+
raw >>= 5n;
|
|
35
|
+
}
|
|
36
|
+
return out.reverse().join("");
|
|
37
|
+
}
|
|
38
|
+
function looksLikeVmId(s) {
|
|
39
|
+
let head = s;
|
|
40
|
+
if (s.includes("_")) {
|
|
41
|
+
const [h, ...rest] = s.split("_");
|
|
42
|
+
const tail = rest.join("_");
|
|
43
|
+
if (!tail || !/^[A-Za-z0-9]+$/.test(tail)) return false;
|
|
44
|
+
head = h;
|
|
45
|
+
}
|
|
46
|
+
if (head.length !== 26) return false;
|
|
47
|
+
return [...head.toUpperCase()].every((c) => ULID_ALPHABET.includes(c));
|
|
48
|
+
}
|
|
49
|
+
function decodeStream(text, encoding) {
|
|
50
|
+
const s = typeof text === "string" ? text : "";
|
|
51
|
+
if (encoding === "base64") {
|
|
52
|
+
try {
|
|
53
|
+
const bin = atob(s);
|
|
54
|
+
const out = new Uint8Array(bin.length);
|
|
55
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
56
|
+
return out;
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return new TextEncoder().encode(s);
|
|
61
|
+
}
|
|
62
|
+
function bytesToBase64(data) {
|
|
63
|
+
let bin = "";
|
|
64
|
+
for (let i = 0; i < data.length; i++) bin += String.fromCharCode(data[i]);
|
|
65
|
+
return btoa(bin);
|
|
66
|
+
}
|
|
67
|
+
function isTransient(err) {
|
|
68
|
+
if (!err || err.code !== "internal") return false;
|
|
69
|
+
const msg = err.message ?? "";
|
|
70
|
+
return TRANSIENT_HINTS.some((h) => msg.includes(h));
|
|
71
|
+
}
|
|
72
|
+
var Arker = class {
|
|
73
|
+
apiKey;
|
|
74
|
+
baseUrl;
|
|
75
|
+
constructor(opts) {
|
|
76
|
+
if (!opts.apiKey) throw new Error("apiKey is required");
|
|
77
|
+
this.apiKey = opts.apiKey;
|
|
78
|
+
const base = opts.baseUrl ?? process.env.ARKER_BASE_URL ?? DEFAULT_BASE_URL;
|
|
79
|
+
this.baseUrl = base.replace(/\/+$/, "");
|
|
80
|
+
}
|
|
81
|
+
/** @internal */
|
|
82
|
+
async _request(method, path, body, overrideBaseUrl) {
|
|
83
|
+
const url = (overrideBaseUrl ? overrideBaseUrl.replace(/\/+$/, "") : this.baseUrl) + path;
|
|
84
|
+
const headers = {
|
|
85
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
86
|
+
"content-type": "application/json"
|
|
87
|
+
};
|
|
88
|
+
let lastStatus = 0;
|
|
89
|
+
let lastErr = null;
|
|
90
|
+
let lastText = "";
|
|
91
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
92
|
+
const resp = await fetch(url, {
|
|
93
|
+
method,
|
|
94
|
+
headers,
|
|
95
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
96
|
+
});
|
|
97
|
+
const text = await resp.text();
|
|
98
|
+
let payload = null;
|
|
99
|
+
try {
|
|
100
|
+
payload = text ? JSON.parse(text) : {};
|
|
101
|
+
} catch {
|
|
102
|
+
payload = null;
|
|
103
|
+
}
|
|
104
|
+
lastStatus = resp.status;
|
|
105
|
+
lastText = text;
|
|
106
|
+
const envelopeErr = payload && typeof payload === "object" && payload.ok === false ? payload.error : null;
|
|
107
|
+
lastErr = envelopeErr;
|
|
108
|
+
if (RETRYABLE_HTTP.has(resp.status) || isTransient(envelopeErr)) {
|
|
109
|
+
if (attempt === MAX_ATTEMPTS - 1) break;
|
|
110
|
+
await sleep(BACKOFF_MS * 2 ** attempt + jitterMs());
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (envelopeErr) {
|
|
114
|
+
throw new ArkerError(envelopeErr.code ?? "internal", envelopeErr.message ?? "", resp.status);
|
|
115
|
+
}
|
|
116
|
+
if (resp.status >= 400) {
|
|
117
|
+
throw new ArkerError("internal", text.slice(0, 200) || "request failed", resp.status);
|
|
118
|
+
}
|
|
119
|
+
return payload;
|
|
120
|
+
}
|
|
121
|
+
if (lastErr) {
|
|
122
|
+
throw new ArkerError(lastErr.code ?? "internal", lastErr.message ?? "", lastStatus);
|
|
123
|
+
}
|
|
124
|
+
throw new ArkerError("internal", lastText.slice(0, 200) || "no response", lastStatus);
|
|
125
|
+
}
|
|
126
|
+
/** Open a handle to a VM by ULID *or* template name. No network call. */
|
|
127
|
+
vm(vmId) {
|
|
128
|
+
return new Computer(this, vmId);
|
|
129
|
+
}
|
|
130
|
+
/** List VMs in the caller's organization. Always hits `https://arker.ai`. */
|
|
131
|
+
async list(opts = {}) {
|
|
132
|
+
const params = new URLSearchParams();
|
|
133
|
+
if (opts.limit !== void 0 && opts.limit !== 25) params.set("limit", String(opts.limit));
|
|
134
|
+
if (opts.offset !== void 0 && opts.offset !== 0) params.set("offset", String(opts.offset));
|
|
135
|
+
if (opts.q !== void 0) params.set("q", opts.q);
|
|
136
|
+
if (opts.sort !== void 0) params.set("sort", opts.sort);
|
|
137
|
+
const qs = params.toString();
|
|
138
|
+
const path = "/api/v1/vms/list" + (qs ? `?${qs}` : "");
|
|
139
|
+
const r = await this._request("GET", path, void 0, LIST_BASE_URL);
|
|
140
|
+
return { items: r.items ?? [], total: r.total ?? 0 };
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var Computer = class _Computer {
|
|
144
|
+
id;
|
|
145
|
+
sync;
|
|
146
|
+
/** @internal */
|
|
147
|
+
_client;
|
|
148
|
+
constructor(client, vmId) {
|
|
149
|
+
this._client = client;
|
|
150
|
+
this.id = vmId;
|
|
151
|
+
this.sync = new Sync(this);
|
|
152
|
+
}
|
|
153
|
+
async delete() {
|
|
154
|
+
await this._client._request("DELETE", `/api/v1/vms/${this.id}`);
|
|
155
|
+
}
|
|
156
|
+
/** Fork this VM. Aliases like `"arkuntu"` resolve client-side. */
|
|
157
|
+
async fork(opts = {}) {
|
|
158
|
+
const body = { is_public: opts.isPublic ?? false };
|
|
159
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
160
|
+
if (opts.region !== void 0) body.region = opts.region;
|
|
161
|
+
const resolved = SOURCE_ALIASES[this.id] ?? this.id;
|
|
162
|
+
let r;
|
|
163
|
+
if (looksLikeVmId(resolved)) {
|
|
164
|
+
r = await this._client._request("POST", `/api/v1/vms/${resolved}/fork`, body);
|
|
165
|
+
} else {
|
|
166
|
+
body.from = this.id;
|
|
167
|
+
r = await this._client._request("POST", "/api/v1/vms/fork", body, LIST_BASE_URL);
|
|
168
|
+
}
|
|
169
|
+
if (!r.vm_id) throw new ArkerError("internal", "fork response missing vm_id", 200);
|
|
170
|
+
return new _Computer(this._client, r.vm_id);
|
|
171
|
+
}
|
|
172
|
+
async run(command, opts = {}) {
|
|
173
|
+
const body = {
|
|
174
|
+
command,
|
|
175
|
+
session_id: opts.sessionId ?? 0
|
|
176
|
+
};
|
|
177
|
+
if (opts.timeout !== void 0) body.timeout = opts.timeout;
|
|
178
|
+
const r = await this._client._request("POST", `/api/v1/vms/${this.id}/run`, body);
|
|
179
|
+
return {
|
|
180
|
+
stdout: decodeStream(r.stdout, r.stdout_encoding),
|
|
181
|
+
stderr: decodeStream(r.stderr, r.stderr_encoding),
|
|
182
|
+
exitCode: Number(r.exit_code ?? 0),
|
|
183
|
+
durationMs: Number(r.duration_ms ?? 0),
|
|
184
|
+
sessionId: String(r.session_id ?? ""),
|
|
185
|
+
cwd: String(r.cwd ?? "")
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var Sync = class {
|
|
190
|
+
/** @internal */
|
|
191
|
+
_vm;
|
|
192
|
+
constructor(vm) {
|
|
193
|
+
this._vm = vm;
|
|
194
|
+
}
|
|
195
|
+
path() {
|
|
196
|
+
return `/api/v1/vms/${this._vm.id}/sync`;
|
|
197
|
+
}
|
|
198
|
+
async readFile(path) {
|
|
199
|
+
const r = await this._vm._client._request("POST", this.path(), { op: "read", path });
|
|
200
|
+
if (r.content !== void 0 && r.content !== null) {
|
|
201
|
+
return decodeStream(r.content, r.encoding);
|
|
202
|
+
}
|
|
203
|
+
if (r.presigned_url) {
|
|
204
|
+
const resp = await fetch(r.presigned_url);
|
|
205
|
+
if (!resp.ok) {
|
|
206
|
+
throw new ArkerError("internal", `signed GET failed: ${resp.status}`, resp.status);
|
|
207
|
+
}
|
|
208
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
209
|
+
}
|
|
210
|
+
throw new ArkerError("internal", "read response missing content/presigned_url", 200);
|
|
211
|
+
}
|
|
212
|
+
async writeFile(path, data) {
|
|
213
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
214
|
+
if (!bytes.length) throw new ArkerError("bad_request", "writeFile: empty data", 400);
|
|
215
|
+
if (bytes.length <= CHUNK_SIZE) {
|
|
216
|
+
await this.fastPath(path, bytes);
|
|
217
|
+
} else {
|
|
218
|
+
await this.presignedPath(path, bytes);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async fastPath(path, data) {
|
|
222
|
+
const size = data.length;
|
|
223
|
+
const result = await this.sendOneWrite({
|
|
224
|
+
path,
|
|
225
|
+
size,
|
|
226
|
+
upload_id: ulid(),
|
|
227
|
+
start: 0,
|
|
228
|
+
end: size,
|
|
229
|
+
content: bytesToBase64(data)
|
|
230
|
+
});
|
|
231
|
+
if (!(result.complete && result.written)) {
|
|
232
|
+
throw new ArkerError("internal", "fast-path write returned without complete+written", 200);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async presignedPath(path, data) {
|
|
236
|
+
const size = data.length;
|
|
237
|
+
const e1 = await this.sendOneWrite({ path, size, presigned: true });
|
|
238
|
+
const url = e1.presigned_url;
|
|
239
|
+
const uploadId = e1.upload_id;
|
|
240
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
241
|
+
const ctrl = new AbortController();
|
|
242
|
+
const t = setTimeout(() => ctrl.abort(), PRESIGN_PUT_TIMEOUT_MS);
|
|
243
|
+
try {
|
|
244
|
+
const resp = await fetch(url, { method: "PUT", body: data, signal: ctrl.signal });
|
|
245
|
+
clearTimeout(t);
|
|
246
|
+
if (resp.ok) break;
|
|
247
|
+
if (!RETRYABLE_HTTP.has(resp.status) || attempt === MAX_ATTEMPTS - 1) {
|
|
248
|
+
throw new ArkerError("internal", `upload PUT failed: ${resp.status}`, resp.status);
|
|
249
|
+
}
|
|
250
|
+
await sleep(BACKOFF_MS * 2 ** attempt);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
clearTimeout(t);
|
|
253
|
+
if (err instanceof ArkerError) throw err;
|
|
254
|
+
if (attempt === MAX_ATTEMPTS - 1) {
|
|
255
|
+
throw new ArkerError("internal", `upload PUT failed: ${err.message}`, 0);
|
|
256
|
+
}
|
|
257
|
+
await sleep(BACKOFF_MS * 2 ** attempt);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
await this.sendOneWrite({ path, size, upload_id: uploadId });
|
|
261
|
+
}
|
|
262
|
+
async sendOneWrite(entry) {
|
|
263
|
+
let lastErr = null;
|
|
264
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
265
|
+
const r = await this._vm._client._request("POST", this.path(), {
|
|
266
|
+
op: "write",
|
|
267
|
+
writes: [entry]
|
|
268
|
+
});
|
|
269
|
+
const result = (r.results ?? [null])[0];
|
|
270
|
+
if (!result) throw new ArkerError("internal", "write response missing results[0]", 200);
|
|
271
|
+
const err = result.error ?? null;
|
|
272
|
+
if (!err) return result;
|
|
273
|
+
lastErr = err;
|
|
274
|
+
if (!isTransient(err) || attempt === MAX_ATTEMPTS - 1) break;
|
|
275
|
+
await sleep(BACKOFF_MS * 2 ** attempt + jitterMs());
|
|
276
|
+
}
|
|
277
|
+
throw new ArkerError(lastErr?.code ?? "internal", lastErr?.message ?? "write failed", 200);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
export {
|
|
281
|
+
Arker,
|
|
282
|
+
ArkerError,
|
|
283
|
+
CHUNK_SIZE,
|
|
284
|
+
Computer,
|
|
285
|
+
DEFAULT_BASE_URL,
|
|
286
|
+
LIST_BASE_URL,
|
|
287
|
+
SOURCE_ALIASES,
|
|
288
|
+
Sync
|
|
289
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arker-ai/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for the Arker virtual computer platform — spawn sandboxed VMs, run code, sync files.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --target=node18",
|
|
22
|
+
"demo": "tsx tests/demo.ts",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["arker", "vm", "sandbox", "agent", "code-execution"],
|
|
26
|
+
"license": "Apache-2.0",
|
|
27
|
+
"homepage": "https://arker.ai",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/ArkerHQ/arker-sdks.git",
|
|
31
|
+
"directory": "typescript"
|
|
32
|
+
},
|
|
33
|
+
"bugs": "https://github.com/ArkerHQ/arker-sdks/issues",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.6.0",
|
|
36
|
+
"tsup": "^8.3.0",
|
|
37
|
+
"tsx": "^4.19.0",
|
|
38
|
+
"typescript": "^5.6.0"
|
|
39
|
+
}
|
|
40
|
+
}
|