@unikernelai/sandbox-cli 1.0.0 → 1.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.
Files changed (3) hide show
  1. package/SKILL.md +145 -0
  2. package/dist/cli.js +209 -182
  3. package/package.json +3 -2
package/SKILL.md ADDED
@@ -0,0 +1,145 @@
1
+ # SKILL: unik — Unikraft Sandbox CLI
2
+
3
+ ## Overview
4
+ `unik` is a CLI for running code in unikernel-isolated sandboxes on Unikraft Cloud.
5
+ It is designed to be used by autonomous AI agents as well as humans.
6
+
7
+ ## Agent Mode (automatic)
8
+ When stdout is not a TTY (piped, redirected, or called by an agent), **all output is JSON by default**.
9
+ No `--json` flag needed. Agents should always pipe output.
10
+
11
+ ```bash
12
+ # Agent-style usage (JSON output automatic):
13
+ unik run python 'print("hello")' | jq .stdout
14
+ unik sandbox create | jq .id
15
+ unik status | jq .checks.cloud.ok
16
+ ```
17
+
18
+ ## Invariants
19
+ - All outputs are JSON when stdout is not a TTY.
20
+ - Exit code **0** = success. Exit code **1** = error.
21
+ - Errors always go to **stderr** as JSON: `{ "ok": false, "error": "..." }`.
22
+ - Sandbox IDs always start with `sbx_`.
23
+ - API keys always start with `uk_live_`.
24
+ - Execution timeout max is **8000ms**.
25
+ - `--dry-run` is safe and non-destructive on all delete/revoke operations.
26
+ - Credentials live in `~/.unik/config.json`.
27
+
28
+ ## Onboarding a new agent (typical flow)
29
+ ```bash
30
+ # 1. Set a JWT from your auth provider:
31
+ unik auth set-token <jwt>
32
+
33
+ # 2. Create an API key (auto-saved to ~/.unik/config.json):
34
+ unik keys create --label "my-agent"
35
+ # → prints { "ok": true, "key": "uk_live_...", "saved": true }
36
+ # JWT is cleared; the new API key is used for all future calls.
37
+
38
+ # 3. Verify:
39
+ unik auth whoami
40
+ unik status
41
+ ```
42
+
43
+ ## Self-discovery
44
+ ```bash
45
+ # Machine-readable schema of all commands + I/O:
46
+ unik schema
47
+ ```
48
+
49
+ ## Common agent patterns
50
+
51
+ ### Stateless execution (one-shot)
52
+ ```bash
53
+ unik run python 'import math; print(math.sqrt(144))'
54
+ unik run nodejs 'console.log(JSON.stringify({x: 1+1}))'
55
+ echo 'print(sum(range(100)))' | unik run python
56
+ cat script.py | unik run python
57
+ ```
58
+
59
+ ### Persistent sandbox (multi-step state)
60
+ ```bash
61
+ # Create once, reuse for multiple executions
62
+ SBX=$(unik sandbox create | jq -r .id)
63
+
64
+ unik sandbox exec "$SBX" python 'x = 42'
65
+ unik sandbox exec "$SBX" python 'print(x)' # state persists within sandbox
66
+
67
+ # Upload a file, then run it
68
+ echo 'def greet(n): return f"hello {n}"' | unik sandbox upload "$SBX" /app/utils.py
69
+ unik sandbox exec "$SBX" python 'import sys; sys.path.insert(0,"/app"); from utils import greet; print(greet("agent"))'
70
+
71
+ unik sandbox ls "$SBX"
72
+ unik sandbox delete "$SBX"
73
+ ```
74
+
75
+ ### API key management
76
+ ```bash
77
+ unik keys list | jq '.items[] | {prefix, label, id}'
78
+ unik keys revoke <id> --dry-run # safe preview
79
+ unik keys revoke <id>
80
+ ```
81
+
82
+ ### MCP server mode (for Claude Code / Cursor / other MCP clients)
83
+ Add to your MCP config:
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "unik": {
88
+ "command": "unik",
89
+ "args": ["mcp"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+ Credentials from `~/.unik/config.json` are used automatically.
95
+
96
+ ## Output shapes
97
+
98
+ ### `unik run` / `unik sandbox exec`
99
+ ```json
100
+ {
101
+ "ok": true,
102
+ "status": "success",
103
+ "stdout": "42\n",
104
+ "stderr": "",
105
+ "execution_time_ms": 45
106
+ }
107
+ ```
108
+
109
+ ### `unik sandbox create`
110
+ ```json
111
+ { "ok": true, "id": "sbx_..." }
112
+ ```
113
+
114
+ ### `unik keys create`
115
+ ```json
116
+ {
117
+ "ok": true,
118
+ "id": "uuid",
119
+ "key": "uk_live_...",
120
+ "prefix": "uk_live_XXXX",
121
+ "expires_at": null,
122
+ "saved": true
123
+ }
124
+ ```
125
+
126
+ ### `unik status`
127
+ ```json
128
+ {
129
+ "status": "ready",
130
+ "checks": { "cloud": { "ok": true }, "database": { "ok": true } }
131
+ }
132
+ ```
133
+
134
+ ### Errors (stderr)
135
+ ```json
136
+ { "ok": false, "error": "no credentials found", "hint": "unik auth set-key <key>" }
137
+ ```
138
+
139
+ ## Best practices for agents
140
+ - Always use `unik schema` at the start of a session to get current command shapes.
141
+ - Use `--dry-run` before any destructive operation to verify intent.
142
+ - Parse `ok` field first — if `false`, check `error` on stderr.
143
+ - Sandbox TTL is 15 minutes. Re-create if `sandbox_not_found` is returned.
144
+ - Pipe code via stdin for multi-line scripts: `cat script.py | unik run python`.
145
+ - `unik run` is stateless. Use `unik sandbox exec` when you need to persist state between steps.
package/dist/cli.js CHANGED
@@ -3,8 +3,12 @@ import { program } from "commander";
3
3
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
4
4
  import { homedir } from "os";
5
5
  import { join } from "path";
6
+ import { createInterface } from "readline";
6
7
  import { UnikernelsClient, DEFAULT_BASE_URL, } from "@unikernelai/sandbox";
7
- // ─── Config file: ~/.unik/config.json ───────────────────────────────────────
8
+ // ─── Agent mode detection ────────────────────────────────────────────────────
9
+ // If stdout is not a TTY (piped, redirected, called by an agent), default to JSON.
10
+ const AGENT_MODE = !process.stdout.isTTY;
11
+ // ─── Config ───────────────────────────────────────────────────────────────────
8
12
  const CONFIG_DIR = join(homedir(), ".unik");
9
13
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
10
14
  function loadConfig() {
@@ -22,161 +26,139 @@ function saveConfig(cfg) {
22
26
  mkdirSync(CONFIG_DIR, { recursive: true });
23
27
  writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
24
28
  }
25
- function getClient(overrides = {}) {
29
+ function getClient() {
26
30
  const cfg = loadConfig();
27
- const apiKey = overrides.apiKey ?? cfg.api_key;
28
- const authToken = overrides.authToken ?? cfg.auth_token;
29
- const baseUrl = overrides.baseUrl ?? cfg.base_url ?? DEFAULT_BASE_URL;
30
- if (!apiKey && !authToken) {
31
- console.error("Error: no credentials found.\n" +
32
- " Set an API key: unik auth set-key <key>\n" +
33
- " Or set a JWT: unik auth set-token <jwt>\n" +
34
- " Or login: unik auth login");
35
- process.exit(1);
36
- }
37
- return new UnikernelsClient({ apiKey, authToken, baseUrl });
31
+ if (!cfg.api_key && !cfg.auth_token)
32
+ die("no credentials found", { hint: "unik auth set-key <key> or unik auth set-token <jwt>" });
33
+ return new UnikernelsClient({ apiKey: cfg.api_key, authToken: cfg.auth_token, baseUrl: cfg.base_url ?? DEFAULT_BASE_URL });
34
+ }
35
+ // ─── Output helpers ───────────────────────────────────────────────────────────
36
+ function out(human, data) {
37
+ if (AGENT_MODE)
38
+ process.stdout.write(JSON.stringify(data) + "\n");
39
+ else
40
+ console.log(human);
38
41
  }
39
42
  function jsonOut(data) {
40
- console.log(JSON.stringify(data, null, 2));
43
+ process.stdout.write(JSON.stringify(data) + "\n");
44
+ }
45
+ function die(message, extra) {
46
+ process.stderr.write(JSON.stringify({ ok: false, error: message, ...extra }) + "\n");
47
+ process.exit(1);
41
48
  }
42
49
  function readStdin() {
43
50
  return new Promise((resolve) => {
44
51
  let buf = "";
45
52
  process.stdin.setEncoding("utf8");
46
- process.stdin.on("data", (chunk) => (buf += chunk));
53
+ process.stdin.on("data", (c) => (buf += c));
47
54
  process.stdin.on("end", () => resolve(buf.trim()));
48
55
  });
49
56
  }
50
- // ─── CLI definition ──────────────────────────────────────────────────────────
57
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
51
58
  program
52
59
  .name("unik")
53
- .description("Unikraft sandbox CLI run code in unikernel-isolated sandboxes")
54
- .version("1.0.0");
55
- // ── auth ─────────────────────────────────────────────────────────────────────
60
+ .description("Unikraft sandbox CLI. JSON output auto-enabled in pipe/agent mode.")
61
+ .version("1.1.0");
62
+ // schema ─────────────────────────────────────────────────────────────────────
63
+ program.command("schema").description("Output a machine-readable JSON schema of all commands. For agents.").action(() => jsonOut(SCHEMA));
64
+ // auth ────────────────────────────────────────────────────────────────────────
56
65
  const auth = program.command("auth").description("Manage credentials");
57
- auth
58
- .command("set-key <key>")
59
- .description("Save an API key to ~/.unik/config.json")
60
- .action((key) => {
66
+ auth.command("set-key <key>").description("Save an API key to ~/.unik/config.json").action((key) => {
61
67
  const cfg = loadConfig();
62
68
  cfg.api_key = key;
63
69
  delete cfg.auth_token;
64
70
  saveConfig(cfg);
65
- console.log(`✓ API key saved (prefix: ${key.slice(0, 12)}...)`);
71
+ out(`✓ API key saved (prefix: ${key.slice(0, 12)}...)`, { ok: true, prefix: key.slice(0, 12) });
66
72
  });
67
- auth
68
- .command("set-token <jwt>")
69
- .description("Save a JWT bearer token (for admin operations like creating API keys)")
70
- .action((jwt) => {
73
+ auth.command("set-token <jwt>").description("Save a JWT bearer token (for creating API keys)").action((jwt) => {
71
74
  const cfg = loadConfig();
72
75
  cfg.auth_token = jwt;
73
76
  saveConfig(cfg);
74
- console.log("✓ JWT saved");
77
+ out("✓ JWT saved", { ok: true });
75
78
  });
76
- auth
77
- .command("set-url <url>")
78
- .description("Override the base URL (default: DEFAULT_BASE_URL)")
79
- .action((url) => {
79
+ auth.command("set-url <url>").description("Override the API base URL").action((url) => {
80
80
  const cfg = loadConfig();
81
81
  cfg.base_url = url;
82
82
  saveConfig(cfg);
83
- console.log(`✓ Base URL set to ${url}`);
83
+ out(`✓ Base URL set to ${url}`, { ok: true, base_url: url });
84
84
  });
85
- auth
86
- .command("whoami")
87
- .description("Show stored credentials")
88
- .action(() => {
85
+ auth.command("whoami").description("Show stored credentials").action(() => {
89
86
  const cfg = loadConfig();
90
- console.log("Base URL:", cfg.base_url ?? DEFAULT_BASE_URL);
91
- console.log("API key: ", cfg.api_key ? cfg.api_key.slice(0, 16) + "..." : "(none)");
92
- console.log("JWT: ", cfg.auth_token ? cfg.auth_token.slice(0, 20) + "..." : "(none)");
87
+ const data = { base_url: cfg.base_url ?? DEFAULT_BASE_URL, api_key_prefix: cfg.api_key ? cfg.api_key.slice(0, 16) : null, has_jwt: !!cfg.auth_token };
88
+ if (AGENT_MODE) {
89
+ jsonOut(data);
90
+ return;
91
+ }
92
+ console.log("Base URL:", data.base_url);
93
+ console.log("API key: ", data.api_key_prefix ? data.api_key_prefix + "..." : "(none)");
94
+ console.log("JWT: ", data.has_jwt ? "(set)" : "(none)");
93
95
  });
94
- auth
95
- .command("logout")
96
- .description("Clear stored credentials")
97
- .action(() => {
96
+ auth.command("logout").description("Clear stored credentials").action(() => {
98
97
  saveConfig({});
99
- console.log("✓ Credentials cleared");
98
+ out("✓ Credentials cleared", { ok: true });
100
99
  });
101
- // ── api-keys ─────────────────────────────────────────────────────────────────
100
+ // keys ────────────────────────────────────────────────────────────────────────
102
101
  const keys = program.command("keys").description("Manage API keys (requires JWT)");
103
- keys
104
- .command("create")
105
- .description("Create a new API key and save it locally")
106
- .option("-l, --label <label>", "Human-readable label for the key")
107
- .option("-e, --expires <date>", "Expiry date (ISO 8601, e.g. 2026-12-31T00:00:00Z)")
108
- .option("--no-save", "Print the key without saving to ~/.unik/config.json")
102
+ keys.command("create")
103
+ .description("Create a new API key. Saves to ~/.unik/config.json by default.")
104
+ .option("-l, --label <label>", "Label for the key")
105
+ .option("-e, --expires <date>", "Expiry (ISO 8601)")
106
+ .option("--no-save", "Print only, don't save")
109
107
  .action(async (opts) => {
110
- const client = getClient();
111
- const key = await client.createApiKey({
112
- label: opts.label,
113
- expires_at: opts.expires,
114
- });
115
- console.log("\n✓ API key created:");
116
- console.log(" Key: ", key.key);
117
- console.log(" ID: ", key.id);
118
- console.log(" Prefix: ", key.prefix);
119
- if (key.expires_at)
120
- console.log(" Expires:", key.expires_at);
108
+ const key = await getClient().createApiKey({ label: opts.label, expires_at: opts.expires });
121
109
  if (opts.save !== false) {
122
110
  const cfg = loadConfig();
123
111
  cfg.api_key = key.key;
124
112
  delete cfg.auth_token;
125
113
  saveConfig(cfg);
126
- console.log("\n✓ Key saved to ~/.unik/config.json. Future commands will use it automatically.");
127
114
  }
128
- else {
129
- console.log("\n ⚠ Store this key it will NOT be shown again.");
115
+ if (AGENT_MODE) {
116
+ jsonOut({ ok: true, ...key, saved: opts.save !== false });
117
+ return;
130
118
  }
119
+ console.log("\n✓ API key created:");
120
+ console.log(" Key: ", key.key);
121
+ console.log(" ID: ", key.id);
122
+ console.log(" Prefix: ", key.prefix);
123
+ if (key.expires_at)
124
+ console.log(" Expires:", key.expires_at);
125
+ console.log(opts.save !== false ? "\n✓ Key saved to ~/.unik/config.json." : "\n ⚠ Store this key — it will NOT be shown again.");
131
126
  });
132
- keys
133
- .command("list")
134
- .description("List all API keys for this account")
135
- .action(async () => {
136
- const client = getClient();
137
- const list = await client.listApiKeys();
138
- if (list.length === 0) {
139
- console.log("No API keys found.");
127
+ keys.command("list").description("List all API keys").action(async () => {
128
+ const list = await getClient().listApiKeys();
129
+ if (AGENT_MODE) {
130
+ jsonOut({ ok: true, count: list.length, items: list });
140
131
  return;
141
132
  }
142
- console.log(`\n${list.length} key(s):\n`);
143
- for (const k of list) {
144
- const status = k.revoked_at ? "REVOKED" : "active";
145
- console.log(` ${k.prefix}... [${status}] id=${k.id} label=${k.label ?? "-"} created=${k.created_at.slice(0, 10)}`);
133
+ if (!list.length) {
134
+ console.log("No keys.");
135
+ return;
146
136
  }
137
+ for (const k of list)
138
+ console.log(` ${k.prefix}... [${k.revoked_at ? "REVOKED" : "active"}] id=${k.id} label=${k.label ?? "-"} created=${k.created_at.slice(0, 10)}`);
147
139
  });
148
- keys
149
- .command("revoke <id>")
150
- .description("Revoke an API key by its ID")
151
- .action(async (id) => {
152
- const client = getClient();
153
- await client.revokeApiKey(id);
154
- console.log(`✓ Key ${id} revoked`);
140
+ keys.command("revoke <id>").description("Revoke a key by ID").option("--dry-run", "Preview only").action(async (id, opts) => {
141
+ if (opts.dryRun) {
142
+ out(`[dry-run] Would revoke key id=${id}`, { ok: true, dry_run: true, id });
143
+ return;
144
+ }
145
+ await getClient().revokeApiKey(id);
146
+ out(`✓ Key ${id} revoked`, { ok: true, id, revoked: true });
155
147
  });
156
- // ── run ───────────────────────────────────────────────────────────────────────
157
- program
158
- .command("run <language> [code]")
159
- .description("Execute code directly (stateless). Language: python | nodejs | shell.\n" +
160
- " Pass code as argument or pipe via stdin:\n" +
161
- " unik run python 'print(\"hello\")'\n" +
162
- " echo 'print(1+1)' | unik run python\n" +
163
- " cat script.py | unik run python")
164
- .option("-t, --timeout <ms>", "Timeout in milliseconds (max 8000)", "8000")
165
- .option("--json", "Output raw JSON response")
148
+ // run ─────────────────────────────────────────────────────────────────────────
149
+ program.command("run <language> [code]")
150
+ .description("Stateless execution. Language: python | nodejs | shell. Reads stdin if code omitted.")
151
+ .option("-t, --timeout <ms>", "Max 8000ms", "8000")
152
+ .option("--json", "Force JSON output in interactive mode")
166
153
  .action(async (language, codeArg, opts) => {
167
154
  const code = codeArg ?? (await readStdin());
168
- if (!code) {
169
- console.error("Error: provide code as argument or pipe via stdin");
170
- process.exit(1);
171
- }
172
- const client = getClient();
173
- const result = await client.execute({
174
- language: language,
175
- code,
176
- timeout_ms: parseInt(opts.timeout),
177
- });
178
- if (opts.json) {
179
- jsonOut(result);
155
+ if (!code)
156
+ die("provide code as argument or pipe via stdin");
157
+ const result = await getClient().execute({ language: language, code, timeout_ms: parseInt(opts.timeout) });
158
+ if (AGENT_MODE || opts.json) {
159
+ jsonOut({ ok: result.status === "success", ...result });
160
+ if (result.status !== "success")
161
+ process.exit(1);
180
162
  return;
181
163
  }
182
164
  if (result.stdout)
@@ -184,46 +166,31 @@ program
184
166
  if (result.stderr)
185
167
  process.stderr.write(result.stderr);
186
168
  if (result.status !== "success") {
187
- console.error(`\n✗ Exited with status: ${result.status} (${result.execution_time_ms}ms)`);
169
+ console.error(`✗ ${result.status} (${result.execution_time_ms}ms)`);
188
170
  process.exit(1);
189
171
  }
190
172
  });
191
- // ── sandbox ───────────────────────────────────────────────────────────────────
173
+ // sandbox ─────────────────────────────────────────────────────────────────────
192
174
  const sbx = program.command("sandbox").description("Manage persistent sandbox sessions");
193
- sbx
194
- .command("create")
195
- .description("Create a new sandbox and print its ID")
196
- .option("--json", "Output raw JSON")
197
- .action(async (opts) => {
198
- const client = getClient();
199
- const sandbox = await client.createSandbox();
200
- if (opts.json) {
201
- jsonOut({ id: sandbox.id });
202
- }
203
- else {
175
+ sbx.command("create").description("Create a sandbox (15 min TTL). Prints ID on stdout.").option("--json", "Force JSON").action(async (opts) => {
176
+ const sandbox = await getClient().createSandbox();
177
+ if (AGENT_MODE || opts.json)
178
+ jsonOut({ ok: true, id: sandbox.id });
179
+ else
204
180
  console.log(sandbox.id);
205
- }
206
181
  });
207
- sbx
208
- .command("exec <id> <language> [code]")
209
- .description("Run code inside an existing sandbox")
210
- .option("-t, --timeout <ms>", "Timeout in milliseconds (max 8000)", "8000")
211
- .option("--json", "Output raw JSON")
182
+ sbx.command("exec <id> <language> [code]").description("Run code in a sandbox. Reads stdin if code omitted.")
183
+ .option("-t, --timeout <ms>", "Max 8000ms", "8000")
184
+ .option("--json", "Force JSON")
212
185
  .action(async (id, language, codeArg, opts) => {
213
186
  const code = codeArg ?? (await readStdin());
214
- if (!code) {
215
- console.error("Error: provide code as argument or pipe via stdin");
216
- process.exit(1);
217
- }
218
- const client = getClient();
219
- const sandbox = client.getSandboxHandle(id);
220
- const result = await sandbox.execute({
221
- language: language,
222
- code,
223
- timeout_ms: parseInt(opts.timeout),
224
- });
225
- if (opts.json) {
226
- jsonOut(result);
187
+ if (!code)
188
+ die("provide code as argument or pipe via stdin");
189
+ const result = await getClient().getSandboxHandle(id).execute({ language: language, code, timeout_ms: parseInt(opts.timeout) });
190
+ if (AGENT_MODE || opts.json) {
191
+ jsonOut({ ok: result.status === "success", sandbox_id: id, ...result });
192
+ if (result.status !== "success")
193
+ process.exit(1);
227
194
  return;
228
195
  }
229
196
  if (result.stdout)
@@ -233,58 +200,118 @@ sbx
233
200
  if (result.status !== "success")
234
201
  process.exit(1);
235
202
  });
236
- sbx
237
- .command("delete <id>")
238
- .description("Delete a sandbox")
239
- .action(async (id) => {
240
- const client = getClient();
241
- await client.deleteSandbox(id);
242
- console.log(`✓ Sandbox ${id} deleted`);
203
+ sbx.command("delete <id>").description("Delete a sandbox").option("--dry-run", "Preview only").action(async (id, opts) => {
204
+ if (opts.dryRun) {
205
+ out(`[dry-run] Would delete sandbox id=${id}`, { ok: true, dry_run: true, id });
206
+ return;
207
+ }
208
+ await getClient().deleteSandbox(id);
209
+ out(`✓ Sandbox ${id} deleted`, { ok: true, id, deleted: true });
243
210
  });
244
- sbx
245
- .command("upload <id> <path> [content]")
246
- .description("Upload a file into a sandbox (base64 content, or pipe stdin)")
247
- .action(async (id, path, contentArg) => {
248
- const raw = contentArg ?? (await readStdin());
249
- const b64 = Buffer.from(raw).toString("base64");
250
- const client = getClient();
251
- const sandbox = client.getSandboxHandle(id);
252
- const file = await sandbox.uploadFile(path, b64);
253
- console.log(`✓ Uploaded ${file.path} (${file.size_bytes} bytes)`);
211
+ sbx.command("upload <id> <remote-path>").description("Upload a file. Pipe content to stdin or use -f <local>.")
212
+ .option("-f, --file <path>", "Local file path")
213
+ .action(async (id, remotePath, opts) => {
214
+ const raw = opts.file ? readFileSync(opts.file) : Buffer.from(await readStdin());
215
+ const file = await getClient().getSandboxHandle(id).uploadFile(remotePath, Buffer.from(raw).toString("base64"));
216
+ out(`✓ Uploaded ${file.path} (${file.size_bytes} bytes)`, { ok: true, file });
254
217
  });
255
- sbx
256
- .command("ls <id> [path]")
257
- .description("List files in a sandbox")
258
- .action(async (id, path = "") => {
259
- const client = getClient();
260
- const sandbox = client.getSandboxHandle(id);
261
- const files = await sandbox.listFiles(path);
262
- if (files.length === 0) {
218
+ sbx.command("ls <id> [path]").description("List files in a sandbox").action(async (id, path = "") => {
219
+ const files = await getClient().getSandboxHandle(id).listFiles(path);
220
+ if (AGENT_MODE) {
221
+ jsonOut({ ok: true, count: files.length, items: files });
222
+ return;
223
+ }
224
+ if (!files.length) {
263
225
  console.log("(empty)");
264
226
  return;
265
227
  }
266
- for (const f of files) {
228
+ for (const f of files)
267
229
  console.log(` ${f.path} (${f.size_bytes}B)`);
268
- }
269
230
  });
270
- // ── status ────────────────────────────────────────────────────────────────────
271
- program
272
- .command("status")
273
- .description("Check if the server is healthy")
274
- .option("--json", "Output raw JSON")
275
- .action(async (opts) => {
231
+ // status ──────────────────────────────────────────────────────────────────────
232
+ program.command("status").description("Check server health").option("--json", "Force JSON").action(async (opts) => {
276
233
  const cfg = loadConfig();
277
- const baseUrl = cfg.base_url ?? DEFAULT_BASE_URL;
278
- const res = await fetch(`${baseUrl}/readyz`).then((r) => r.json());
279
- if (opts.json) {
234
+ const res = await fetch(`${cfg.base_url ?? DEFAULT_BASE_URL}/readyz`).then((r) => r.json());
235
+ if (AGENT_MODE || opts.json) {
280
236
  jsonOut(res);
237
+ return;
281
238
  }
282
- else {
283
- const r = res;
284
- console.log("Status:", r.status);
285
- for (const [name, check] of Object.entries(r.checks ?? {})) {
286
- console.log(` ${check.ok ? "✓" : "✗"} ${name}`);
239
+ console.log("Status:", res.status);
240
+ for (const [name, check] of Object.entries(res.checks ?? {}))
241
+ console.log(` ${check.ok ? "✓" : "✗"} ${name}`);
242
+ });
243
+ // mcp ─────────────────────────────────────────────────────────────────────────
244
+ // One binary = CLI + MCP server. Add to MCP config: { "command": "unik", "args": ["mcp"] }
245
+ program.command("mcp")
246
+ .description('Run as an MCP server over stdio (JSON-RPC 2.0). Add to MCP config: { "command": "unik", "args": ["mcp"] }')
247
+ .action(async () => {
248
+ const cfg = loadConfig();
249
+ if (!cfg.api_key && !cfg.auth_token)
250
+ die("no credentials for MCP mode", { hint: "unik auth set-key <key>" });
251
+ const baseUrl = cfg.base_url ?? DEFAULT_BASE_URL;
252
+ const headers = { "Content-Type": "application/json" };
253
+ if (cfg.api_key)
254
+ headers["x-api-key"] = cfg.api_key;
255
+ if (cfg.auth_token)
256
+ headers["Authorization"] = `Bearer ${cfg.auth_token}`;
257
+ const rl = createInterface({ input: process.stdin, terminal: false });
258
+ rl.on("line", async (line) => {
259
+ const trimmed = line.trim();
260
+ if (!trimmed)
261
+ return;
262
+ let msg;
263
+ try {
264
+ msg = JSON.parse(trimmed);
287
265
  }
288
- }
266
+ catch {
267
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }) + "\n");
268
+ return;
269
+ }
270
+ try {
271
+ const resp = await fetch(`${baseUrl}/mcp`, { method: "POST", headers, body: JSON.stringify(msg) });
272
+ const body = await resp.text();
273
+ for (const chunk of body.split("\n").filter(Boolean))
274
+ process.stdout.write(chunk + "\n");
275
+ }
276
+ catch (err) {
277
+ const req = msg;
278
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: req?.id ?? null, error: { code: -32603, message: String(err) } }) + "\n");
279
+ }
280
+ });
281
+ await new Promise((resolve) => rl.on("close", resolve));
289
282
  });
283
+ // ─── Schema (for unik schema command) ────────────────────────────────────────
284
+ const SCHEMA = {
285
+ name: "unik", version: "1.1.0",
286
+ description: "Unikraft sandbox CLI — unikernel-isolated code execution",
287
+ agent_note: "stdout is JSON when stdout is not a TTY. Exit 0 = success, exit 1 = error. Errors go to stderr as JSON { ok: false, error: string }.",
288
+ commands: [
289
+ { command: "unik run <language> [code]", description: "Stateless execution. Reads stdin if code omitted.", args: { language: "python|nodejs|shell", code: "string (optional)" }, options: { "--timeout <ms>": "max 8000 (default 8000)" }, output: { ok: "bool", status: "success|error|timeout", stdout: "string", stderr: "string", execution_time_ms: "number" } },
290
+ { command: "unik sandbox create", output: { ok: "bool", id: "sbx_..." } },
291
+ { command: "unik sandbox exec <id> <language> [code]", output: { ok: "bool", sandbox_id: "string", status: "success|error|timeout", stdout: "string", stderr: "string", execution_time_ms: "number" } },
292
+ { command: "unik sandbox delete <id>", options: { "--dry-run": "safe preview" }, output: { ok: "bool", id: "string", deleted: "bool" } },
293
+ { command: "unik sandbox upload <id> <remote-path>", options: { "-f <localPath>": "file path (else stdin)" }, output: { ok: "bool", file: { path: "string", size_bytes: "number" } } },
294
+ { command: "unik sandbox ls <id> [path]", output: { ok: "bool", count: "number", items: [{ path: "string", size_bytes: "number" }] } },
295
+ { command: "unik keys create", options: { "--label": "string", "--expires": "ISO 8601", "--no-save": "don't persist" }, output: { ok: "bool", id: "string", key: "uk_live_...", prefix: "string", saved: "bool" } },
296
+ { command: "unik keys list", output: { ok: "bool", count: "number", items: [{ id: "string", prefix: "string", label: "string|null", revoked_at: "string|null" }] } },
297
+ { command: "unik keys revoke <id>", options: { "--dry-run": "safe preview" }, output: { ok: "bool", revoked: "bool" } },
298
+ { command: "unik auth set-key <key>", output: { ok: "bool", prefix: "string" } },
299
+ { command: "unik auth set-token <jwt>", output: { ok: "bool" } },
300
+ { command: "unik auth set-url <url>", output: { ok: "bool", base_url: "string" } },
301
+ { command: "unik auth whoami", output: { base_url: "string", api_key_prefix: "string|null", has_jwt: "bool" } },
302
+ { command: "unik status", output: { status: "ready|not_ready", checks: { cloud: { ok: "bool" }, database: { ok: "bool" } } } },
303
+ { command: "unik mcp", description: "MCP server over stdio. Add to MCP config: { \"command\": \"unik\", \"args\": [\"mcp\"] }" },
304
+ { command: "unik schema", description: "Output this schema as JSON." },
305
+ ],
306
+ invariants: [
307
+ "All outputs are JSON when stdout is not a TTY.",
308
+ "Exit 0 = success. Exit 1 = error.",
309
+ "Errors go to stderr as JSON: { ok: false, error: string }.",
310
+ "sandbox IDs start with 'sbx_'.",
311
+ "API keys start with 'uk_live_'.",
312
+ "Execution timeout max is 8000ms.",
313
+ "--dry-run is safe on all destructive ops (delete, revoke).",
314
+ "Credentials live in ~/.unik/config.json.",
315
+ ],
316
+ };
290
317
  program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unikernelai/sandbox-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CLI for the Unikraft sandbox API — run code, manage sandboxes, provision API keys",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "main": "dist/cli.js",
10
10
  "files": [
11
- "dist"
11
+ "dist",
12
+ "SKILL.md"
12
13
  ],
13
14
  "scripts": {
14
15
  "build": "tsc -p tsconfig.json",