zelpi 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 ADDED
@@ -0,0 +1,149 @@
1
+ # ZelPi — `zos` CLI
2
+
3
+ **ZelPi** (Physical Intelligence Operating System) as an `npm install`-able,
4
+ self-contained command-line OS for autonomous robot fleets.
5
+
6
+ ```bash
7
+ npm install -g pios # installs the `zos` command
8
+ zos # interactive OS shell — boots a backend automatically
9
+ ```
10
+
11
+ No repo, no database, no API keys required: the package ships a **bundled
12
+ simulation engine** and an **embedded backend** that auto-starts on first use.
13
+ Point it at a real ZelPi backend later with one env var.
14
+
15
+ It implements the "AI Integration for Embedded" architecture as a top-to-bottom
16
+ command surface:
17
+
18
+ ```
19
+ CLI / REPL bin + cli/repl ← top layer
20
+
21
+ MCP / API Hooks cli/client · cli/mcp ← agents (north) + backend (south)
22
+
23
+ Kernel — QoS router cli/kernel + engine ← Robustness | Optimisation | System-Critical
24
+
25
+ Model layer (VLM·WM·VLA·LLM) models train/deploy
26
+
27
+ Cloud (train/store) hub pull · models train
28
+
29
+ Hub / Repo (SOCs · HF) cli/hub ← compatible SOCs + HuggingFace registry
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ ```bash
35
+ zos # OS shell (auto-starts the embedded backend)
36
+ zos status # health · profile · fleet · deployed models
37
+ zos intent "Inventory Aisle 4" # natural-language intent → System-2 plan → dispatch
38
+ zos profile optimisation # switch the kernel QoS profile
39
+ zos hub pull openvla/openvla-7b # load a model from HuggingFace
40
+ zos models deploy M4 # promote a trained model to the fleet
41
+ zos watch # live activity feed
42
+ zos down # stop the embedded backend daemon
43
+ ```
44
+
45
+ Bare text in the shell is treated as an intent, so `zos` feels like a
46
+ conversational OS console.
47
+
48
+ ## How it runs
49
+
50
+ - **Embedded (default).** Any command auto-starts a local backend (`cli/embedded.mjs`)
51
+ running the bundled engine over the same WS+HTTP protocol the full ZelPi server
52
+ speaks. State persists to `~/.pi-os/embedded-state.json`. Intent parsing is the
53
+ deterministic offline parser (no keys). `zos up` runs it in the foreground;
54
+ `zos down` stops the daemon.
55
+ - **Connected.** Point at a full backend (with a live LLM, ROS, MLflow, auth):
56
+
57
+ ```bash
58
+ export PIOS_HOST=http://your-host:8787
59
+ export PIOS_WS_URL=ws://your-host:8787
60
+ zos status
61
+ # or persist: zos config set host your-host && zos config set port 8787
62
+ ```
63
+
64
+ ## The Kernel — QoS profile router
65
+
66
+ Every intent is routed through one execution profile; the engine applies it,
67
+ biasing **Safety-Broker scrutiny** and **execution speed**.
68
+
69
+ | Profile | Speed | Safety gating | Use |
70
+ |---|---|---|---|
71
+ | `balanced` | nominal | nominal | kernel default |
72
+ | `robustness` | −5% | +20% | many SOCs/buses, multi-system spread |
73
+ | `optimisation` | +40% | −50% | fast routine tasks, speed > deliberation |
74
+ | `system-critical` | −18% | +80% | precise, high-integrity work |
75
+
76
+ ```bash
77
+ zos profile # list + active
78
+ zos profile system-critical
79
+ zos intent "Retrieve the green box from Aisle 3" --profile system-critical
80
+ ```
81
+
82
+ ## Commands
83
+
84
+ ```
85
+ status backend health, profile, fleet, deployed models
86
+ intent "<text>" [--profile p] submit a natural-language intent
87
+ safety [authorize|reroute|abort] show / resolve the Human-in-the-Loop gate
88
+ watch stream the live activity feed
89
+ pause | resume | reset fleet lifecycle
90
+
91
+ profile [name] kernel QoS router
92
+
93
+ fleet list robots
94
+ fleet enroll --chassis "<c>" [--name <n>] [--aisle <n>]
95
+
96
+ models model registry (vla→S2, act→S1, mhal→S0)
97
+ models train --kind vla|act|mhal [--name <n>] [--embodiment <e>]
98
+ models deploy <id>
99
+
100
+ hub [socs|chassis|models] compatible SOCs, embodiments, model registry
101
+ hub pull <key|hf-id> load a model from HF (e.g. pi0, openvla/openvla-7b)
102
+
103
+ up [--ros] [--force] [--embedded] boot a backend
104
+ down stop the embedded backend daemon
105
+ mcp run the MCP server (expose ZelPi to AI agents over stdio)
106
+ login [user] fetch + store a dev JWT
107
+ metrics Prometheus exposition
108
+ config [set <key> <value>] backend host/port/token
109
+ ```
110
+
111
+ ## MCP — drive the fleet from an AI agent
112
+
113
+ `zos mcp` runs a Model-Context-Protocol server over stdio (JSON-RPC 2.0). Tools:
114
+ `pios_status`, `pios_intent`, `pios_set_profile`, `pios_fleet`, `pios_models`,
115
+ `pios_deploy_model`, `pios_resolve_safety`, `pios_hub`.
116
+
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "pi-os": { "command": "zos", "args": ["mcp"], "env": { "PIOS_HOST": "http://127.0.0.1:8787" } }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ## Environment
126
+
127
+ | Var | Meaning |
128
+ |---|---|
129
+ | `PIOS_HOST` | backend HTTP base (e.g. `http://10.0.0.5:8787`) |
130
+ | `PIOS_WS_URL` | backend WS base |
131
+ | `PIOS_TOKEN` | JWT for an auth-enabled backend (`zos login` fetches a dev token) |
132
+ | `PIOS_NO_AUTOSTART` | disable embedded auto-start |
133
+ | `NO_COLOR` | disable ANSI color |
134
+
135
+ Config persists to `~/.pi-os/config.json`.
136
+
137
+ ## Publishing (maintainers)
138
+
139
+ The publishable package is assembled from this repo into `cli-dist/` — lean
140
+ (only `ws`), self-contained (bundled engine), free of the web-app deps.
141
+
142
+ ```bash
143
+ npm run build:pkg # → cli-dist/ (esbuild bundle + cli/ + bin/ + package.json)
144
+ cd cli-dist
145
+ npm publish --access public # requires `npm login`
146
+ ```
147
+
148
+ Requires Node ≥ 18 (uses the global WebSocket client on Node 21+, falls back to
149
+ `ws` on older runtimes).
package/bin/pi-os.mjs ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // pi-os — Physical Intelligence Operating System · CLI entrypoint.
3
+ // `npm install -g .` (or `npm link`) exposes this as the `pi-os` command.
4
+ // No args → interactive OS shell. Otherwise dispatch a single command.
5
+
6
+ import { run, NO_AUTOSTART } from "../cli/dispatch.mjs";
7
+ import { repl } from "../cli/repl.mjs";
8
+ import { ensureBackend } from "../cli/daemon.mjs";
9
+ import { HELP } from "../cli/help.mjs";
10
+ import { c, sym } from "../cli/ui.mjs";
11
+
12
+ const argv = process.argv.slice(2);
13
+ const first = argv[0];
14
+
15
+ async function main() {
16
+ if (first === "help" || first === "--help" || first === "-h") return console.log(HELP);
17
+ if (first === "version" || first === "--version" || first === "-v") {
18
+ const { createRequire } = await import("node:module");
19
+ const require = createRequire(import.meta.url);
20
+ const pkg = require("../package.json");
21
+ return console.log(`zelpi ${pkg.version}`);
22
+ }
23
+ // Interactive OS shell.
24
+ if (!first || first === "shell") {
25
+ await ensureBackend().catch(() => {});
26
+ return repl();
27
+ }
28
+ // For commands that talk to the backend, make sure one is up (auto-start the
29
+ // self-contained embedded backend for local targets — the npm-install path).
30
+ if (!NO_AUTOSTART.has(first)) await ensureBackend().catch(() => {});
31
+ await run(argv);
32
+ }
33
+
34
+ main().catch((e) => {
35
+ console.error(`${sym.err} ${c.red(e.message || String(e))}`);
36
+ process.exit(1);
37
+ });
package/cli/client.mjs ADDED
@@ -0,0 +1,148 @@
1
+ // pi-os CLI — API Hooks layer.
2
+ // The southbound connection from the CLI to the ZelPi backend: HTTP for health /
3
+ // DB views / metrics, and a WebSocket for the live command + snapshot channel
4
+ // (the same protocol the web console speaks). Uses Node's built-in fetch +
5
+ // global WebSocket — zero dependencies.
6
+
7
+ import { httpBase, wsUrl, load } from "./config.mjs";
8
+
9
+ class PiosError extends Error {}
10
+
11
+ // Prefer Node's global WebSocket client (Node 21+/22+); fall back to the `ws`
12
+ // package on older runtimes so the published package works broadly.
13
+ let _WS = globalThis.WebSocket;
14
+ async function WSClass() {
15
+ if (!_WS) _WS = (await import("ws")).WebSocket;
16
+ return _WS;
17
+ }
18
+
19
+ async function getJSON(pathname) {
20
+ const base = httpBase();
21
+ let res;
22
+ try {
23
+ res = await fetch(base + pathname, { signal: AbortSignal.timeout(5000) });
24
+ } catch (e) {
25
+ throw new PiosError(`backend unreachable at ${base} (${e.code || e.name}). Is it running? Try: npx zelpi up`);
26
+ }
27
+ if (!res.ok) throw new PiosError(`${pathname} → HTTP ${res.status}`);
28
+ const txt = await res.text();
29
+ try {
30
+ return JSON.parse(txt);
31
+ } catch {
32
+ return txt;
33
+ }
34
+ }
35
+
36
+ export const health = () => getJSON("/health");
37
+ export const dbTasks = () => getJSON("/db/tasks");
38
+ export const dbModels = () => getJSON("/db/models");
39
+ export const dbEmbodiments = () => getJSON("/db/embodiments");
40
+ export const metricsText = () => getJSON("/metrics");
41
+
42
+ export async function login(user = "operator") {
43
+ const base = httpBase();
44
+ const res = await fetch(base + "/auth/login", {
45
+ method: "POST",
46
+ headers: { "content-type": "application/json" },
47
+ body: JSON.stringify({ user }),
48
+ });
49
+ return res.json();
50
+ }
51
+
52
+ /** Open a WS, run `fn(ws)` once connected, and resolve when `done()` is called. */
53
+ function withSocket(fn, { timeout = 8000 } = {}) {
54
+ return new Promise(async (resolve, reject) => {
55
+ let ws;
56
+ try {
57
+ const WS = await WSClass();
58
+ ws = new WS(wsUrl());
59
+ } catch (e) {
60
+ return reject(new PiosError(`could not open socket: ${e.message}`));
61
+ }
62
+ const timer = setTimeout(() => {
63
+ try { ws.close(); } catch {}
64
+ reject(new PiosError(`timed out talking to backend (${wsUrl()})`));
65
+ }, timeout);
66
+ let result;
67
+ const done = (value) => {
68
+ result = value;
69
+ clearTimeout(timer);
70
+ try { ws.close(); } catch {}
71
+ };
72
+ ws.addEventListener("open", () => {
73
+ try { fn(ws, done); } catch (e) { clearTimeout(timer); reject(e); }
74
+ });
75
+ ws.addEventListener("close", (e) => {
76
+ clearTimeout(timer);
77
+ if (e && e.code === 1008) return reject(new PiosError("unauthorized — set a token: npx zelpi login"));
78
+ resolve(result);
79
+ });
80
+ ws.addEventListener("error", () => {
81
+ clearTimeout(timer);
82
+ reject(new PiosError(`socket error connecting to ${wsUrl()}. Is the backend up?`));
83
+ });
84
+ });
85
+ }
86
+
87
+ const parse = (data) => {
88
+ try { return JSON.parse(typeof data === "string" ? data : data.toString()); } catch { return null; }
89
+ };
90
+
91
+ /** Fetch a single live snapshot over the WS channel. */
92
+ export function snapshot() {
93
+ return withSocket((ws, done) => {
94
+ ws.addEventListener("message", (e) => {
95
+ const m = parse(e.data);
96
+ if (m && m.type === "snapshot") done(m.snap);
97
+ });
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Send one command and read back its effect. By default wait `settle` snapshots;
103
+ * if an `until(snap)` predicate is given, resolve on the first post-command
104
+ * snapshot that satisfies it (or after `timeout` ms) — needed for effects that
105
+ * complete asynchronously server-side, e.g. an LLM-planned intent.
106
+ */
107
+ export function command(msg, { settle = 0, until = null, timeout = 8000 } = {}) {
108
+ return withSocket(
109
+ (ws, done) => {
110
+ let sent = false;
111
+ ws.addEventListener("message", (e) => {
112
+ const m = parse(e.data);
113
+ if (!m || m.type !== "snapshot") return;
114
+ if (!sent) {
115
+ // ignore the immediate pre-command snapshot, then send
116
+ sent = true;
117
+ ws.send(JSON.stringify(msg));
118
+ if (!until && settle === 0) done(m.snap);
119
+ return;
120
+ }
121
+ if (until) { if (until(m.snap)) done(m.snap); }
122
+ else if (--settle <= 0) done(m.snap);
123
+ });
124
+ },
125
+ { timeout },
126
+ );
127
+ }
128
+
129
+ /** Stream snapshots to `onSnap` until the returned stop() is called. */
130
+ export function watch(onSnap) {
131
+ let socket;
132
+ let stopped = false;
133
+ const open = async () => {
134
+ const WS = await WSClass();
135
+ if (stopped) return;
136
+ socket = new WS(wsUrl());
137
+ socket.addEventListener("message", (e) => {
138
+ const m = parse(e.data);
139
+ if (m && m.type === "snapshot") onSnap(m.snap);
140
+ });
141
+ socket.addEventListener("close", () => { if (!stopped) setTimeout(open, 1000); });
142
+ socket.addEventListener("error", () => { try { socket.close(); } catch {} });
143
+ };
144
+ open();
145
+ return () => { stopped = true; try { socket.close(); } catch {} };
146
+ }
147
+
148
+ export { PiosError, load };