@unikernelai/sandbox-cli 1.0.0 → 1.2.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/SKILL.md +145 -0
- package/dist/cli.js +209 -182
- package/package.json +7 -5
package/SKILL.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# SKILL: unik — Unikernel AI Sandbox CLI
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
`unik` is a CLI for running code in unikernel-isolated sandboxes on Unikernel AI.
|
|
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
|
-
// ───
|
|
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(
|
|
29
|
+
function getClient() {
|
|
26
30
|
const cfg = loadConfig();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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", (
|
|
53
|
+
process.stdin.on("data", (c) => (buf += c));
|
|
47
54
|
process.stdin.on("end", () => resolve(buf.trim()));
|
|
48
55
|
});
|
|
49
56
|
}
|
|
50
|
-
// ─── CLI
|
|
57
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
51
58
|
program
|
|
52
59
|
.name("unik")
|
|
53
|
-
.description("
|
|
54
|
-
.version("1.
|
|
55
|
-
//
|
|
60
|
+
.description("Unikernel AI sandbox CLI. JSON output auto-enabled in pipe/agent mode.")
|
|
61
|
+
.version("1.2.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
+
out("✓ Credentials cleared", { ok: true });
|
|
100
99
|
});
|
|
101
|
-
//
|
|
100
|
+
// keys ────────────────────────────────────────────────────────────────────────
|
|
102
101
|
const keys = program.command("keys").description("Manage API keys (requires JWT)");
|
|
103
|
-
keys
|
|
104
|
-
.
|
|
105
|
-
.
|
|
106
|
-
.option("-
|
|
107
|
-
.option("-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await
|
|
154
|
-
|
|
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
|
-
//
|
|
157
|
-
program
|
|
158
|
-
.
|
|
159
|
-
.
|
|
160
|
-
"
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
169
|
+
console.error(`✗ ${result.status} (${result.execution_time_ms}ms)`);
|
|
188
170
|
process.exit(1);
|
|
189
171
|
}
|
|
190
172
|
});
|
|
191
|
-
//
|
|
173
|
+
// sandbox ─────────────────────────────────────────────────────────────────────
|
|
192
174
|
const sbx = program.command("sandbox").description("Manage persistent sandbox sessions");
|
|
193
|
-
sbx
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
.
|
|
209
|
-
.
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
await
|
|
242
|
-
|
|
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
|
-
.
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
const
|
|
249
|
-
|
|
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
|
-
.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
//
|
|
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
|
|
278
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
console.log("
|
|
285
|
-
|
|
286
|
-
|
|
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.2.0",
|
|
286
|
+
description: "Unikernel AI 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,21 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unikernelai/sandbox-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for the
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "CLI for the Unikernel AI sandbox — run code, manage sandboxes, provision API keys",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"unik": "./dist/cli.js"
|
|
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",
|
|
15
16
|
"dev": "node --loader ts-node/esm src/cli.ts"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@unikernelai/sandbox": "^1.
|
|
19
|
+
"@unikernelai/sandbox": "^1.2.0",
|
|
19
20
|
"commander": "^12.1.0"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"sandbox",
|
|
27
|
-
"
|
|
28
|
+
"unikernel",
|
|
29
|
+
"unikernel-ai",
|
|
28
30
|
"code-execution",
|
|
29
31
|
"cli"
|
|
30
32
|
],
|