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 +149 -0
- package/bin/pi-os.mjs +37 -0
- package/cli/client.mjs +148 -0
- package/cli/commands.mjs +406 -0
- package/cli/config.mjs +58 -0
- package/cli/daemon.mjs +75 -0
- package/cli/dispatch.mjs +70 -0
- package/cli/embedded.mjs +106 -0
- package/cli/help.mjs +44 -0
- package/cli/hub.mjs +76 -0
- package/cli/kernel.mjs +63 -0
- package/cli/mcp.mjs +144 -0
- package/cli/repl.mjs +100 -0
- package/cli/ui.mjs +109 -0
- package/dist/pios-engine.mjs +948 -0
- package/package.json +37 -0
package/cli/embedded.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// pi-os CLI — embedded backend. Runs the bundled PiOsEngine in-process and
|
|
2
|
+
// serves the exact same HTTP /health + WebSocket snapshot/command protocol the
|
|
3
|
+
// full server (server/server.ts) speaks. This is what makes `npm i -g pios`
|
|
4
|
+
// self-contained: no repo, no tsx, no database — the OS runs anywhere Node does.
|
|
5
|
+
//
|
|
6
|
+
// Intent parsing here is the deterministic offline parser baked into the engine
|
|
7
|
+
// (no LLM/keys), matching the public offline web demo.
|
|
8
|
+
|
|
9
|
+
import http from "node:http";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { WebSocketServer } from "ws";
|
|
14
|
+
import { PiOsEngine } from "../dist/pios-engine.mjs";
|
|
15
|
+
|
|
16
|
+
const TICK_MS = 25; // ~40 Hz
|
|
17
|
+
const BROADCAST_EVERY = 3; // ~13 Hz snapshots
|
|
18
|
+
const STATE_FILE = path.join(os.homedir(), ".pi-os", "embedded-state.json");
|
|
19
|
+
|
|
20
|
+
export function createEmbeddedServer({ port = 8787, persist = true } = {}) {
|
|
21
|
+
const engine = new PiOsEngine();
|
|
22
|
+
|
|
23
|
+
if (persist) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
26
|
+
engine.importState(raw);
|
|
27
|
+
} catch { /* fresh world */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const health = () => {
|
|
31
|
+
const s = engine.getSnapshot();
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
clock: s.metrics.clock,
|
|
35
|
+
fleet: s.robots.length,
|
|
36
|
+
tasks: s.tasks.length,
|
|
37
|
+
profile: s.profile,
|
|
38
|
+
store: "embedded",
|
|
39
|
+
llm: { enabled: false, provider: "off", reachable: false, model: "(deterministic)", url: "" },
|
|
40
|
+
ros: { enabled: false, connected: false, url: "" },
|
|
41
|
+
foundry: { enabled: false, reachable: false, url: "", tracked: 0 },
|
|
42
|
+
auth: { enabled: false },
|
|
43
|
+
embedded: true,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const server = http.createServer((req, res) => {
|
|
48
|
+
res.setHeader("access-control-allow-origin", "*");
|
|
49
|
+
res.setHeader("content-type", "application/json");
|
|
50
|
+
const url = req.url ?? "";
|
|
51
|
+
if (url === "/health") { res.writeHead(200); res.end(JSON.stringify(health())); return; }
|
|
52
|
+
if (url === "/db/tasks") { res.writeHead(200); res.end(JSON.stringify(engine.getSnapshot().tasks)); return; }
|
|
53
|
+
if (url === "/db/models") { res.writeHead(200); res.end(JSON.stringify(engine.getSnapshot().models)); return; }
|
|
54
|
+
res.writeHead(404); res.end("{}");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const wss = new WebSocketServer({ server });
|
|
58
|
+
const snapshotMessage = () => JSON.stringify({ type: "snapshot", snap: engine.getSnapshot() });
|
|
59
|
+
const broadcast = () => { const msg = snapshotMessage(); for (const c of wss.clients) if (c.readyState === 1) c.send(msg); };
|
|
60
|
+
|
|
61
|
+
wss.on("connection", (ws) => {
|
|
62
|
+
ws.send(snapshotMessage());
|
|
63
|
+
ws.on("message", (data) => {
|
|
64
|
+
let m;
|
|
65
|
+
try { m = JSON.parse(data.toString()); } catch { return; }
|
|
66
|
+
switch (m.type) {
|
|
67
|
+
case "intent": engine.submitIntent(String(m.text ?? "")); break;
|
|
68
|
+
case "resolveSafety": engine.resolveSafety(String(m.option ?? "")); break;
|
|
69
|
+
case "createModel": engine.createModel(m.kind, String(m.name ?? ""), String(m.embodiment ?? "universal")); break;
|
|
70
|
+
case "deployModel": engine.deployModel(String(m.id ?? "")); break;
|
|
71
|
+
case "enrollEmbodiment": engine.enrollEmbodiment(m.spec); break;
|
|
72
|
+
case "setProfile": engine.setProfile(m.profile); break;
|
|
73
|
+
case "togglePause": engine.togglePause(); break;
|
|
74
|
+
case "forceSafety": engine.forceSafety(); break;
|
|
75
|
+
case "reset": engine.reset(); break;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let last = Date.now();
|
|
81
|
+
let n = 0;
|
|
82
|
+
const tick = setInterval(() => {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const dt = (now - last) / 1000;
|
|
85
|
+
last = now;
|
|
86
|
+
engine.tick(dt);
|
|
87
|
+
if (++n % BROADCAST_EVERY === 0) broadcast();
|
|
88
|
+
}, TICK_MS);
|
|
89
|
+
|
|
90
|
+
let saver = null;
|
|
91
|
+
const save = () => {
|
|
92
|
+
if (!persist) return;
|
|
93
|
+
try {
|
|
94
|
+
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
95
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(engine.exportState()));
|
|
96
|
+
} catch { /* best-effort */ }
|
|
97
|
+
};
|
|
98
|
+
if (persist) saver = setInterval(save, 3000);
|
|
99
|
+
|
|
100
|
+
const close = () => { clearInterval(tick); if (saver) clearInterval(saver); save(); try { wss.close(); } catch {} try { server.close(); } catch {} };
|
|
101
|
+
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
server.once("error", reject);
|
|
104
|
+
server.listen(port, "127.0.0.1", () => resolve({ engine, port, close }));
|
|
105
|
+
});
|
|
106
|
+
}
|
package/cli/help.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ZelPi CLI — help text.
|
|
2
|
+
import { c } from "./ui.mjs";
|
|
3
|
+
|
|
4
|
+
export const HELP = `
|
|
5
|
+
${c.bold(c.cyan("ZelPi"))} ${c.dim("— Physical Intelligence Operating System · CLI")}
|
|
6
|
+
|
|
7
|
+
${c.bold("USAGE")}
|
|
8
|
+
npx zelpi ${c.dim("interactive OS shell (REPL)")}
|
|
9
|
+
npx zelpi <command> [args] [--flags]
|
|
10
|
+
|
|
11
|
+
${c.bold("COMMAND & CONTROL")}
|
|
12
|
+
status ${c.dim("backend health, profile, fleet, deployed models")}
|
|
13
|
+
intent "<text>" [--profile p] ${c.dim('submit a natural-language intent (e.g. "Inventory Aisle 4")')}
|
|
14
|
+
safety [authorize|reroute|abort] ${c.dim("show / resolve the Human-in-the-Loop gate")}
|
|
15
|
+
watch ${c.dim("stream the live activity feed (Ctrl-C to stop)")}
|
|
16
|
+
pause | resume | reset ${c.dim("fleet lifecycle")}
|
|
17
|
+
|
|
18
|
+
${c.bold("KERNEL — QoS profile router")}
|
|
19
|
+
profile ${c.dim("list profiles + the active one")}
|
|
20
|
+
profile <name> ${c.dim("balanced | robustness | optimisation | system-critical")}
|
|
21
|
+
|
|
22
|
+
${c.bold("FLEET & EMBODIMENT")}
|
|
23
|
+
fleet ${c.dim("list robots")}
|
|
24
|
+
fleet enroll --chassis "<c>" [--name <n>] [--aisle <n>]
|
|
25
|
+
|
|
26
|
+
${c.bold("MODELS — VLM · World-Model · VLA · LLM")}
|
|
27
|
+
models ${c.dim("list the model registry")}
|
|
28
|
+
models train --kind vla|act|mhal [--name <n>] [--embodiment <e>]
|
|
29
|
+
models deploy <id> ${c.dim("promote a ready model to the live fleet")}
|
|
30
|
+
|
|
31
|
+
${c.bold("HUB / REPO — SOCs · chassis · HuggingFace")}
|
|
32
|
+
hub [socs|chassis|models] ${c.dim("compatible SOCs, embodiments, model registry")}
|
|
33
|
+
hub pull <key|hf-id> ${c.dim("load a model from HF (e.g. pi0, openvla/openvla-7b)")}
|
|
34
|
+
|
|
35
|
+
${c.bold("PLATFORM")}
|
|
36
|
+
up [--ros] [--force] ${c.dim("boot the backend from this checkout")}
|
|
37
|
+
mcp ${c.dim("run the MCP server (expose ZelPi to AI agents over stdio)")}
|
|
38
|
+
login [user] ${c.dim("fetch + store a dev JWT")}
|
|
39
|
+
metrics ${c.dim("Prometheus exposition")}
|
|
40
|
+
config [set <key> <value>] ${c.dim("backend host/port/token")}
|
|
41
|
+
|
|
42
|
+
${c.bold("ENV")}
|
|
43
|
+
PIOS_HOST=http://host:port PIOS_WS_URL=ws://host:port PIOS_TOKEN=<jwt> NO_COLOR=1
|
|
44
|
+
`;
|
package/cli/hub.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// pi-os CLI — Hub / Repo: the bottom layer of the architecture diagram.
|
|
2
|
+
// A catalog of compatible embedded SOCs, the chassis embodiments the M-HAL can
|
|
3
|
+
// adapt to, and the model-architecture registry that bridges the diagram's model
|
|
4
|
+
// families (VLM · World-Model · VLA · LLM) to Hugging Face checkpoints and to the
|
|
5
|
+
// engine's policy layers (vla → S2, act → S1, mhal → S0). `pull` loads a model.
|
|
6
|
+
|
|
7
|
+
// ── Compatible embedded SOCs (edge inference targets) ────────────────────────
|
|
8
|
+
export const SOCS = [
|
|
9
|
+
{ id: "jetson-agx-orin", vendor: "NVIDIA", accel: "Ampere GPU + 2×NVDLA", tops: 275, bus: "CAN/EtherCAT", ros: "ROS 2", profile: "system-critical", note: "Flagship edge — runs VLA + perception on-device." },
|
|
10
|
+
{ id: "jetson-orin-nx", vendor: "NVIDIA", accel: "Ampere GPU", tops: 100, bus: "CAN", ros: "ROS 2", profile: "robustness", note: "Balanced humanoid/AMR compute." },
|
|
11
|
+
{ id: "jetson-orin-nano", vendor: "NVIDIA", accel: "Ampere GPU", tops: 40, bus: "CAN", ros: "ROS 2", profile: "optimisation", note: "Low-power AMRs and sensors." },
|
|
12
|
+
{ id: "qualcomm-qrb5165", vendor: "Qualcomm", accel: "Hexagon DSP + Adreno", tops: 15, bus: "CAN", ros: "ROS 2", profile: "optimisation", note: "RB5 robotics platform, 5G." },
|
|
13
|
+
{ id: "rockchip-rk3588", vendor: "Rockchip", accel: "6 TOPS NPU", tops: 6, bus: "CAN", ros: "ROS 2 (community)", profile: "optimisation", note: "Cheap quantized-model inference." },
|
|
14
|
+
{ id: "hailo8-rpi5", vendor: "Raspberry Pi + Hailo", accel: "Hailo-8 NPU", tops: 26, bus: "CAN (HAT)", ros: "ROS 2", profile: "optimisation", note: "Hobby/edge perception accelerator." },
|
|
15
|
+
{ id: "ti-tda4vm", vendor: "Texas Instruments", accel: "C7x DSP + MMA", tops: 8, bus: "CAN-FD/EtherCAT", ros: "ROS 2", profile: "system-critical", note: "Automotive-grade, functional safety." },
|
|
16
|
+
{ id: "amd-kria-k26", vendor: "AMD", accel: "FPGA (Zynq UltraScale+)", tops: 26, bus: "EtherCAT", ros: "ROS 2", profile: "system-critical", note: "Deterministic real-time control." },
|
|
17
|
+
{ id: "renesas-rzv2h", vendor: "Renesas", accel: "DRP-AI3", tops: 80, bus: "EtherCAT", ros: "ROS 2", profile: "robustness", note: "Power-efficient vision + control." },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// ── Chassis embodiments (mirrors CHASSIS_PRESETS in lib/types.ts) ────────────
|
|
21
|
+
export const CHASSIS = [
|
|
22
|
+
{ chassis: "Humanoid biped", bus: "EtherCAT", joints: 28 },
|
|
23
|
+
{ chassis: "6-DoF industrial arm", bus: "CAN", joints: 6 },
|
|
24
|
+
{ chassis: "AMR cart", bus: "CAN", joints: 4 },
|
|
25
|
+
{ chassis: "Quadruped", bus: "EtherCAT", joints: 12 },
|
|
26
|
+
{ chassis: "Heavy-lift forklift", bus: "CAN", joints: 5 },
|
|
27
|
+
{ chassis: "Dual-arm torso", bus: "EtherCAT", joints: 14 },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// ── Model-architecture registry (the VLM · WM · VLA · LLM layer) ──────────────
|
|
31
|
+
// `kind` maps onto the engine's policy layers: vla→S2, act→S1, mhal→S0.
|
|
32
|
+
export const MODELS = [
|
|
33
|
+
{ key: "pi0", family: "VLA", kind: "vla", hf: "lerobot/pi0", license: "Apache-2.0", note: "Physical Intelligence π0 — generalist manipulation (flow-matching)." },
|
|
34
|
+
{ key: "openvla-7b", family: "VLA", kind: "vla", hf: "openvla/openvla-7b", license: "MIT", note: "7B open VLA, strong generalist baseline." },
|
|
35
|
+
{ key: "smolvla", family: "VLA", kind: "vla", hf: "lerobot/smolvla_base", license: "Apache-2.0", note: "Small efficient VLA for the edge." },
|
|
36
|
+
{ key: "act", family: "ACT", kind: "act", hf: "lerobot/act", license: "Apache-2.0", note: "Action-Chunking Transformer visuomotor policy." },
|
|
37
|
+
{ key: "diffusion-policy", family: "ACT", kind: "act", hf: "lerobot/diffusion_policy", license: "MIT", note: "Diffusion visuomotor policy." },
|
|
38
|
+
{ key: "groot-n1", family: "World-Model", kind: "mhal", hf: "nvidia/GR00T-N1", license: "NVIDIA OSL", note: "Humanoid whole-body foundation behaviors." },
|
|
39
|
+
{ key: "isaac-wbc", family: "World-Model", kind: "mhal", hf: "—", license: "BSD-3 (Isaac Lab)", note: "Per-embodiment RL whole-body control (you train it)." },
|
|
40
|
+
{ key: "llama-3.3-70b", family: "LLM", kind: "vla", hf: "meta-llama/Llama-3.3-70B-Instruct", license: "Llama 3.3", note: "Cloud reasoning for intent decomposition (via gateway)." },
|
|
41
|
+
{ key: "qwen2.5-vl", family: "VLM", kind: "vla", hf: "Qwen/Qwen2.5-VL-7B-Instruct", license: "Apache-2.0", note: "Vision-language grounding for the Command Center." },
|
|
42
|
+
{ key: "sam2", family: "VLM", kind: "vla", hf: "facebook/sam2-hiera-large", license: "Apache-2.0", note: "Segment-Anything 2 — perception (Vision agent)." },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const FAMILIES = ["VLM", "World-Model", "VLA", "LLM", "ACT"];
|
|
46
|
+
|
|
47
|
+
/** Resolve a catalog key or a raw HF id into createModel() arguments. */
|
|
48
|
+
export function resolveModel(idOrKey) {
|
|
49
|
+
if (!idOrKey) return null;
|
|
50
|
+
const q = String(idOrKey).trim();
|
|
51
|
+
const hit = MODELS.find((m) => m.key === q.toLowerCase() || m.hf === q);
|
|
52
|
+
if (hit) {
|
|
53
|
+
return { kind: hit.kind, name: hit.key, hf: hit.hf, family: hit.family, license: hit.license, known: true };
|
|
54
|
+
}
|
|
55
|
+
// Unknown HF id: accept it, infer the layer from the path heuristically.
|
|
56
|
+
const lower = q.toLowerCase();
|
|
57
|
+
const kind = /vla|pi0|openvla|smolvla|qwen|llama|vlm|sam/.test(lower)
|
|
58
|
+
? "vla"
|
|
59
|
+
: /diffusion|act|policy/.test(lower)
|
|
60
|
+
? "act"
|
|
61
|
+
: /groot|wbc|control|whole/.test(lower)
|
|
62
|
+
? "mhal"
|
|
63
|
+
: "vla";
|
|
64
|
+
const name = q.split("/").pop().replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 32);
|
|
65
|
+
return { kind, name, hf: q, family: "VLA", license: "unknown", known: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function chassisByName(name) {
|
|
69
|
+
if (!name) return null;
|
|
70
|
+
const q = String(name).toLowerCase();
|
|
71
|
+
return (
|
|
72
|
+
CHASSIS.find((c) => c.chassis.toLowerCase() === q) ||
|
|
73
|
+
CHASSIS.find((c) => c.chassis.toLowerCase().includes(q)) ||
|
|
74
|
+
null
|
|
75
|
+
);
|
|
76
|
+
}
|
package/cli/kernel.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// pi-os CLI — Kernel: the QoS profile router from the architecture diagram.
|
|
2
|
+
// Every intent is routed through one execution profile that trades execution
|
|
3
|
+
// speed against Safety-Broker scrutiny. These specs mirror PROFILE_META in
|
|
4
|
+
// lib/types.ts (the engine applies the same factors server-side).
|
|
5
|
+
|
|
6
|
+
export const PROFILES = {
|
|
7
|
+
balanced: {
|
|
8
|
+
label: "Balanced (Kernel default)",
|
|
9
|
+
glyph: "◇",
|
|
10
|
+
desc: "Neutral routing — the kernel's baseline scheduler.",
|
|
11
|
+
speed: 1.0,
|
|
12
|
+
scrutiny: 1.0,
|
|
13
|
+
aliases: ["default", "neutral"],
|
|
14
|
+
},
|
|
15
|
+
robustness: {
|
|
16
|
+
label: "Robustness",
|
|
17
|
+
glyph: "▣",
|
|
18
|
+
desc: "Handle many SOCs, buses & hardware; multi-system spread, extra checks.",
|
|
19
|
+
speed: 0.95,
|
|
20
|
+
scrutiny: 1.2,
|
|
21
|
+
aliases: ["robust", "r"],
|
|
22
|
+
},
|
|
23
|
+
optimisation: {
|
|
24
|
+
label: "Optimisation",
|
|
25
|
+
glyph: "⚡",
|
|
26
|
+
desc: "Fastest response for routine tasks where speed beats deliberation.",
|
|
27
|
+
speed: 1.4,
|
|
28
|
+
scrutiny: 0.5,
|
|
29
|
+
aliases: ["optimization", "optimise", "optimize", "fast", "opt", "o"],
|
|
30
|
+
},
|
|
31
|
+
"system-critical": {
|
|
32
|
+
label: "System-Critical",
|
|
33
|
+
glyph: "◆",
|
|
34
|
+
desc: "Precise, high-integrity work — maximum Safety-Broker scrutiny.",
|
|
35
|
+
speed: 0.82,
|
|
36
|
+
scrutiny: 1.8,
|
|
37
|
+
aliases: ["critical", "syscrit", "sc", "safe", "precise"],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const PROFILE_KEYS = Object.keys(PROFILES);
|
|
42
|
+
|
|
43
|
+
/** Resolve a user-typed profile name or alias to a canonical key. */
|
|
44
|
+
export function resolveProfile(input) {
|
|
45
|
+
if (!input) return null;
|
|
46
|
+
const k = String(input).toLowerCase().trim();
|
|
47
|
+
if (PROFILES[k]) return k;
|
|
48
|
+
for (const [key, spec] of Object.entries(PROFILES)) {
|
|
49
|
+
if (spec.aliases?.includes(k)) return key;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Human-readable summary of how a profile re-tunes the live stack. */
|
|
55
|
+
export function routingNote(key) {
|
|
56
|
+
const p = PROFILES[key];
|
|
57
|
+
if (!p) return "";
|
|
58
|
+
const spd = Math.round((p.speed - 1) * 100);
|
|
59
|
+
const scr = Math.round((p.scrutiny - 1) * 100);
|
|
60
|
+
const spdTxt = spd === 0 ? "nominal speed" : `${spd > 0 ? "+" : ""}${spd}% execution speed`;
|
|
61
|
+
const scrTxt = scr === 0 ? "nominal gating" : `${scr > 0 ? "+" : ""}${scr}% Safety-Broker gating`;
|
|
62
|
+
return `${spdTxt}, ${scrTxt}`;
|
|
63
|
+
}
|
package/cli/mcp.mjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// pi-os CLI — MCP server (the diagram's northbound "MCP / API Hooks" layer).
|
|
2
|
+
// Exposes ZelPi as Model-Context-Protocol tools so an AI agent can drive the
|
|
3
|
+
// fleet over stdio: newline-delimited JSON-RPC 2.0, zero dependencies.
|
|
4
|
+
// IMPORTANT: stdout carries only protocol frames; all logging goes to stderr.
|
|
5
|
+
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import * as api from "./client.mjs";
|
|
8
|
+
import { PROFILE_KEYS } from "./kernel.mjs";
|
|
9
|
+
import { SOCS, MODELS } from "./hub.mjs";
|
|
10
|
+
|
|
11
|
+
const log = (...a) => process.stderr.write("[pi-os mcp] " + a.join(" ") + "\n");
|
|
12
|
+
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
13
|
+
const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
14
|
+
const err = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
15
|
+
const textResult = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] });
|
|
16
|
+
|
|
17
|
+
const TOOLS = [
|
|
18
|
+
{
|
|
19
|
+
name: "pios_status",
|
|
20
|
+
description: "Get ZelPi backend health: store, active kernel profile, fleet size, active tasks, LLM/ROS/Foundry status, and deployed models.",
|
|
21
|
+
inputSchema: { type: "object", properties: {} },
|
|
22
|
+
run: async () => {
|
|
23
|
+
const h = await api.health();
|
|
24
|
+
const snap = await api.snapshot();
|
|
25
|
+
return { ...h, deployed: snap?.metrics?.deployed };
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "pios_intent",
|
|
30
|
+
description: "Submit a natural-language intent to the fleet (e.g. 'Inventory Aisle 4', 'Retrieve the green box from Aisle 3'). Optionally set the kernel QoS profile first.",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
text: { type: "string", description: "the natural-language command" },
|
|
35
|
+
profile: { type: "string", enum: PROFILE_KEYS, description: "optional kernel profile to route through" },
|
|
36
|
+
},
|
|
37
|
+
required: ["text"],
|
|
38
|
+
},
|
|
39
|
+
run: async (a) => {
|
|
40
|
+
if (a.profile) await api.command({ type: "setProfile", profile: a.profile }, { settle: 1 });
|
|
41
|
+
const before = await api.snapshot();
|
|
42
|
+
const beforeIds = new Set(before.tasks.map((t) => t.id));
|
|
43
|
+
const after = await api.command(
|
|
44
|
+
{ type: "intent", text: String(a.text) },
|
|
45
|
+
{ until: (s) => s.tasks.some((t) => !beforeIds.has(t.id)), timeout: 12000 },
|
|
46
|
+
);
|
|
47
|
+
const task = after.tasks.find((t) => !beforeIds.has(t.id)) || after.tasks.at(-1);
|
|
48
|
+
return { task, safety: after.safety };
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "pios_set_profile",
|
|
53
|
+
description: "Set the kernel QoS execution profile (the diagram's profile router). System-Critical raises Safety-Broker scrutiny; Optimisation favors speed.",
|
|
54
|
+
inputSchema: { type: "object", properties: { profile: { type: "string", enum: PROFILE_KEYS } }, required: ["profile"] },
|
|
55
|
+
run: async (a) => {
|
|
56
|
+
const snap = await api.command({ type: "setProfile", profile: a.profile }, { settle: 2 });
|
|
57
|
+
return { profile: snap?.profile ?? a.profile };
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "pios_fleet",
|
|
62
|
+
description: "List robots in the fleet with chassis, bus, status, battery, position, and assigned task.",
|
|
63
|
+
inputSchema: { type: "object", properties: {} },
|
|
64
|
+
run: async () => (await api.snapshot()).robots,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "pios_models",
|
|
68
|
+
description: "List the model registry (VLA/ACT/M-HAL artifacts) with status, version, quality, and training progress.",
|
|
69
|
+
inputSchema: { type: "object", properties: {} },
|
|
70
|
+
run: async () => (await api.snapshot()).models,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "pios_deploy_model",
|
|
74
|
+
description: "Deploy a ready model to the live fleet by id (promotes it, demotes the prior version on that layer).",
|
|
75
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
76
|
+
run: async (a) => {
|
|
77
|
+
await api.command({ type: "deployModel", id: String(a.id) }, { settle: 3 });
|
|
78
|
+
return { deployed: a.id };
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "pios_resolve_safety",
|
|
83
|
+
description: "Resolve the pending Human-in-the-Loop safety gate with an operator decision (e.g. Authorize, Reroute, Abort, Override).",
|
|
84
|
+
inputSchema: { type: "object", properties: { option: { type: "string" } }, required: ["option"] },
|
|
85
|
+
run: async (a) => {
|
|
86
|
+
const snap = await api.snapshot();
|
|
87
|
+
if (!snap.safety) return { resolved: false, reason: "no pending safety gate" };
|
|
88
|
+
await api.command({ type: "resolveSafety", option: String(a.option) }, { settle: 3 });
|
|
89
|
+
return { resolved: true, option: a.option };
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "pios_hub",
|
|
94
|
+
description: "List the hub/repo: compatible embedded SOCs and the HuggingFace-backed model registry (VLM/World-Model/VLA/LLM families).",
|
|
95
|
+
inputSchema: { type: "object", properties: {} },
|
|
96
|
+
run: async () => ({ socs: SOCS, models: MODELS }),
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const TOOL_MAP = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
|
101
|
+
|
|
102
|
+
async function handle(msg) {
|
|
103
|
+
const { id, method, params } = msg;
|
|
104
|
+
switch (method) {
|
|
105
|
+
case "initialize":
|
|
106
|
+
return ok(id, {
|
|
107
|
+
protocolVersion: params?.protocolVersion ?? "2024-11-05",
|
|
108
|
+
capabilities: { tools: {} },
|
|
109
|
+
serverInfo: { name: "zelpi", version: "0.1.0" },
|
|
110
|
+
});
|
|
111
|
+
case "notifications/initialized":
|
|
112
|
+
case "initialized":
|
|
113
|
+
return; // notification, no reply
|
|
114
|
+
case "ping":
|
|
115
|
+
return ok(id, {});
|
|
116
|
+
case "tools/list":
|
|
117
|
+
return ok(id, { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
|
|
118
|
+
case "tools/call": {
|
|
119
|
+
const tool = TOOL_MAP[params?.name];
|
|
120
|
+
if (!tool) return err(id, -32602, `unknown tool: ${params?.name}`);
|
|
121
|
+
try {
|
|
122
|
+
const result = await tool.run(params.arguments ?? {});
|
|
123
|
+
return ok(id, textResult(result));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return ok(id, { ...textResult(`error: ${e.message || e}`), isError: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
if (id !== undefined) return err(id, -32601, `method not found: ${method}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function mcp() {
|
|
134
|
+
log(`serving ${TOOLS.length} tools over stdio → ${process.env.PIOS_HOST || "default backend"}`);
|
|
135
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
136
|
+
rl.on("line", async (line) => {
|
|
137
|
+
const s = line.trim();
|
|
138
|
+
if (!s) return;
|
|
139
|
+
let msg;
|
|
140
|
+
try { msg = JSON.parse(s); } catch { return log("bad JSON frame"); }
|
|
141
|
+
try { await handle(msg); } catch (e) { if (msg?.id !== undefined) err(msg.id, -32603, String(e.message || e)); }
|
|
142
|
+
});
|
|
143
|
+
rl.on("close", () => process.exit(0));
|
|
144
|
+
}
|
package/cli/repl.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// pi-os CLI — interactive OS shell. A persistent prompt that speaks the same
|
|
2
|
+
// command set as the binary (via dispatch). Bare text is treated as an intent,
|
|
3
|
+
// so the shell behaves like a conversational operating system console.
|
|
4
|
+
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { run } from "./dispatch.mjs";
|
|
7
|
+
import * as api from "./client.mjs";
|
|
8
|
+
import * as cfg from "./config.mjs";
|
|
9
|
+
import { PROFILES } from "./kernel.mjs";
|
|
10
|
+
import { c, sym, banner } from "./ui.mjs";
|
|
11
|
+
import { HELP } from "./help.mjs";
|
|
12
|
+
|
|
13
|
+
function tokenize(line) {
|
|
14
|
+
// split on spaces, honoring "double quotes" so intents can contain spaces
|
|
15
|
+
const out = [];
|
|
16
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
17
|
+
let m;
|
|
18
|
+
while ((m = re.exec(line))) out.push(m[1] ?? m[2] ?? m[3]);
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function repl() {
|
|
23
|
+
console.log(banner());
|
|
24
|
+
let live = false;
|
|
25
|
+
let profile = cfg.load().profile;
|
|
26
|
+
try {
|
|
27
|
+
const h = await api.health();
|
|
28
|
+
live = true;
|
|
29
|
+
profile = h.profile ?? profile;
|
|
30
|
+
console.log(`\n ${sym.ok} ${c.ok("backend live")} ${c.dim(`· ${cfg.httpBase()} · fleet ${h.fleet} · store ${h.store}`)}`);
|
|
31
|
+
} catch {
|
|
32
|
+
console.log(`\n ${sym.warn} ${c.warn("backend offline")} ${c.dim(`· ${cfg.httpBase()} — type 'up' to start it`)}`);
|
|
33
|
+
}
|
|
34
|
+
console.log(` ${c.dim("type a command, an intent in plain English, or 'help'. 'exit' to quit.")}\n`);
|
|
35
|
+
|
|
36
|
+
const prompt = () => {
|
|
37
|
+
const p = PROFILES[profile] ?? PROFILES.balanced;
|
|
38
|
+
return `${c.cyan("zelpi")} ${c.dim(p.glyph)} ${c.gray("▸")} `;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: prompt() });
|
|
42
|
+
rl.prompt();
|
|
43
|
+
|
|
44
|
+
// Process lines strictly one-at-a-time. readline (esp. with piped input) can
|
|
45
|
+
// emit many 'line' events plus 'close' synchronously; a queue lets each async
|
|
46
|
+
// command finish before the next runs, and defers exit until the queue drains.
|
|
47
|
+
const queue = [];
|
|
48
|
+
let busy = false;
|
|
49
|
+
let closing = false;
|
|
50
|
+
|
|
51
|
+
const finish = () => { console.log(c.dim("\nzos session closed.")); process.exit(0); };
|
|
52
|
+
|
|
53
|
+
async function pump() {
|
|
54
|
+
if (busy) return;
|
|
55
|
+
const line = queue.shift();
|
|
56
|
+
if (line === undefined) { if (closing) finish(); return; }
|
|
57
|
+
busy = true;
|
|
58
|
+
try {
|
|
59
|
+
await handle(line);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.log(`${sym.err} ${c.red(e.message || String(e))}`);
|
|
62
|
+
} finally {
|
|
63
|
+
busy = false;
|
|
64
|
+
if (!closing) { rl.setPrompt(prompt()); rl.prompt(); }
|
|
65
|
+
pump();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function handle(line) {
|
|
70
|
+
const tokens = tokenize(line.trim());
|
|
71
|
+
const verb = tokens[0];
|
|
72
|
+
if (!verb) return;
|
|
73
|
+
if (verb === "exit" || verb === "quit" || verb === "q") { closing = true; return; }
|
|
74
|
+
if (verb === "help" || verb === "?") { console.log(HELP); return; }
|
|
75
|
+
if (verb === "clear" || verb === "cls") { console.clear(); return; }
|
|
76
|
+
// 'watch' would block the REPL on SIGINT; run a bounded tail instead.
|
|
77
|
+
if (verb === "watch" || verb === "logs" || verb === "feed") { await tailOnce(); return; }
|
|
78
|
+
await run(tokens);
|
|
79
|
+
// keep the prompt's profile glyph in sync after a profile switch
|
|
80
|
+
const snap = await api.snapshot().catch(() => null);
|
|
81
|
+
if (snap?.profile) profile = snap.profile;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rl.on("line", (line) => { queue.push(line); pump(); });
|
|
85
|
+
rl.on("close", () => { closing = true; pump(); });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// In-REPL 'watch': print one rolling window of the activity feed, then return.
|
|
89
|
+
function tailOnce() {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
let frames = 0;
|
|
92
|
+
let lastId = 0;
|
|
93
|
+
const stop = api.watch((snap) => {
|
|
94
|
+
if (frames === 0) lastId = snap.logs.length ? Math.max(...snap.logs.map((l) => l.id)) : 0;
|
|
95
|
+
for (const l of snap.logs) if (l.id > lastId) { lastId = l.id; console.log(` ${c.dim(l.t.toFixed(1))} ${l.system} ${l.text}`); }
|
|
96
|
+
if (++frames > 60) { stop(); resolve(); } // ~5s window
|
|
97
|
+
});
|
|
98
|
+
setTimeout(() => { stop(); resolve(); }, 5000);
|
|
99
|
+
});
|
|
100
|
+
}
|
package/cli/ui.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// pi-os CLI — terminal presentation layer (ANSI colors, boxes, tables, bars).
|
|
2
|
+
// Zero dependencies. Respects NO_COLOR and non-TTY output.
|
|
3
|
+
|
|
4
|
+
const COLOR = process.env.NO_COLOR == null && (process.stdout.isTTY ?? false);
|
|
5
|
+
|
|
6
|
+
const C = {
|
|
7
|
+
reset: "\x1b[0m",
|
|
8
|
+
bold: "\x1b[1m",
|
|
9
|
+
dim: "\x1b[2m",
|
|
10
|
+
red: "\x1b[31m",
|
|
11
|
+
green: "\x1b[32m",
|
|
12
|
+
yellow: "\x1b[33m",
|
|
13
|
+
blue: "\x1b[34m",
|
|
14
|
+
magenta: "\x1b[35m",
|
|
15
|
+
cyan: "\x1b[36m",
|
|
16
|
+
gray: "\x1b[90m",
|
|
17
|
+
brightGreen: "\x1b[92m",
|
|
18
|
+
brightYellow: "\x1b[93m",
|
|
19
|
+
brightCyan: "\x1b[96m",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function wrap(code, s) {
|
|
23
|
+
return COLOR ? code + s + C.reset : String(s);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const c = {
|
|
27
|
+
bold: (s) => wrap(C.bold, s),
|
|
28
|
+
dim: (s) => wrap(C.dim, s),
|
|
29
|
+
red: (s) => wrap(C.red, s),
|
|
30
|
+
green: (s) => wrap(C.green, s),
|
|
31
|
+
yellow: (s) => wrap(C.yellow, s),
|
|
32
|
+
blue: (s) => wrap(C.blue, s),
|
|
33
|
+
magenta: (s) => wrap(C.magenta, s),
|
|
34
|
+
cyan: (s) => wrap(C.cyan, s),
|
|
35
|
+
gray: (s) => wrap(C.gray, s),
|
|
36
|
+
ok: (s) => wrap(C.brightGreen, s),
|
|
37
|
+
warn: (s) => wrap(C.brightYellow, s),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Map a hex color from the domain meta to the nearest ANSI for accents. */
|
|
41
|
+
export function hexAccent(hex, s) {
|
|
42
|
+
const map = {
|
|
43
|
+
"#60a5fa": c.blue,
|
|
44
|
+
"#34d399": c.green,
|
|
45
|
+
"#fbbf24": c.yellow,
|
|
46
|
+
"#c084fc": c.magenta,
|
|
47
|
+
"#f87171": c.red,
|
|
48
|
+
"#fb923c": c.yellow,
|
|
49
|
+
"#94a3b8": c.gray,
|
|
50
|
+
};
|
|
51
|
+
return (map[hex] ?? ((x) => x))(s);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// strip ANSI for width calculations
|
|
55
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
56
|
+
export const visibleLen = (s) => String(s).replace(ANSI, "").length;
|
|
57
|
+
|
|
58
|
+
export function pad(s, n, align = "left") {
|
|
59
|
+
const len = visibleLen(s);
|
|
60
|
+
if (len >= n) return s;
|
|
61
|
+
const fill = " ".repeat(n - len);
|
|
62
|
+
return align === "right" ? fill + s : s + fill;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Render a small table from rows of cells (already color-formatted ok). */
|
|
66
|
+
export function table(headers, rows) {
|
|
67
|
+
const widths = headers.map((h, i) =>
|
|
68
|
+
Math.max(visibleLen(h), ...rows.map((r) => visibleLen(r[i] ?? "")))
|
|
69
|
+
);
|
|
70
|
+
const line = (cells) => cells.map((cell, i) => pad(cell ?? "", widths[i])).join(" ");
|
|
71
|
+
const out = [c.bold(line(headers)), c.gray(widths.map((w) => "─".repeat(w)).join(" "))];
|
|
72
|
+
for (const r of rows) out.push(line(r));
|
|
73
|
+
return out.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A 0..1 progress / level bar. */
|
|
77
|
+
export function bar(value, width = 16, color = c.green) {
|
|
78
|
+
const v = Math.max(0, Math.min(1, value || 0));
|
|
79
|
+
const filled = Math.round(v * width);
|
|
80
|
+
return color("█".repeat(filled)) + c.gray("░".repeat(width - filled));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function banner() {
|
|
84
|
+
const art = [
|
|
85
|
+
" ███████╗███████╗██╗ ██████╗ ██╗",
|
|
86
|
+
" ╚══███╔╝██╔════╝██║ ██╔══██╗██║",
|
|
87
|
+
" ███╔╝ █████╗ ██║ ██████╔╝██║",
|
|
88
|
+
" ███╔╝ ██╔══╝ ██║ ██╔═══╝ ██║",
|
|
89
|
+
" ███████╗███████╗███████╗██║ ██║",
|
|
90
|
+
" ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝",
|
|
91
|
+
];
|
|
92
|
+
return c.cyan(art.join("\n")) + "\n " + c.dim("Physical Intelligence Operating System · CLI");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const sym = {
|
|
96
|
+
ok: c.ok("✓"),
|
|
97
|
+
warn: c.warn("▲"),
|
|
98
|
+
err: c.red("✗"),
|
|
99
|
+
dot: c.gray("·"),
|
|
100
|
+
arrow: c.gray("→"),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export function heading(s) {
|
|
104
|
+
return "\n" + c.bold(c.cyan("▍" + s));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function kv(key, value, keyWidth = 14) {
|
|
108
|
+
return " " + c.gray(pad(key, keyWidth)) + " " + value;
|
|
109
|
+
}
|