@gorlitzer-labs/seldon 0.1.8 → 0.1.10
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 +49 -5
- package/bin/seldon.js +120 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Pick the tools you want from a checklist; each installs via its native method
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
pnpm add -g @gorlitzer-labs/seldon # or: bun add -g … · npm i -g …
|
|
20
20
|
seldon # open the checklist — ↑↓ move · space pick · a all · enter install
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -28,6 +28,45 @@ seldon doctor # check external deps (tmux, sops, age, tailsca
|
|
|
28
28
|
seldon list # everything available
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Start the stack
|
|
32
|
+
|
|
33
|
+
One command brings up the always-on services and prints where to go:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
seldon up # starts the voice (demerzel) + the supervisor (factory watch),
|
|
37
|
+
# then prints: Voice → http://localhost:8770, how to put agents
|
|
38
|
+
# to work, rooms, phone access
|
|
39
|
+
seldon status # what's running (pids)
|
|
40
|
+
seldon down # stop everything
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`seldon up` only starts what's installed. demerzel serves the voice UI on
|
|
44
|
+
`http://localhost:8770`. Its model set (~25 GB) is a one-time download —
|
|
45
|
+
`seldon install demerzel` **offers to fetch it right there** ([y/N]), and `seldon up`
|
|
46
|
+
offers again if it's still missing. Nothing that big is ever pulled silently.
|
|
47
|
+
Logs live in `~/.seldon/run/`.
|
|
48
|
+
|
|
49
|
+
**Try a different LLM** (e.g. PrismML's Bonsai 2, ~5.9 GB, much smaller than the
|
|
50
|
+
default): set `DEMERZEL_LLM` to any MLX-compatible HF repo before install/up, and
|
|
51
|
+
both the fetch and the runtime follow it:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
DEMERZEL_LLM=mlx-community/<bonsai-2-repo> seldon install demerzel # fetch that model
|
|
55
|
+
DEMERZEL_LLM=mlx-community/<bonsai-2-repo> seldon up # run it
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Uninstall
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
seldon uninstall comb demerzel # remove specific modules (asks to confirm)
|
|
62
|
+
seldon uninstall --all # remove the whole stack (also drops ~/.seldon)
|
|
63
|
+
seldon uninstall <ids> --yes # skip the confirm prompt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Node tools are removed from the global store; demerzel's isolated venv and
|
|
67
|
+
bifrost's binary + config are deleted. Your system Python, tmux, tailscale,
|
|
68
|
+
sops/age are never touched.
|
|
69
|
+
|
|
31
70
|
## The stack
|
|
32
71
|
|
|
33
72
|
| module | what it is | ships via |
|
|
@@ -51,10 +90,15 @@ It's a stack, not a bundle — install only what you use:
|
|
|
51
90
|
|
|
52
91
|
## Package managers
|
|
53
92
|
|
|
54
|
-
Node tools install
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
93
|
+
Node tools install with **pnpm** when it can global-install (much faster than npm),
|
|
94
|
+
else **npm**. **bun** is supported but opt-in — `seldon install --pm=bun` (also
|
|
95
|
+
`--pm=pnpm|npm` to force any of them).
|
|
96
|
+
|
|
97
|
+
demerzel (Python) always installs into its **own isolated venv** — never your
|
|
98
|
+
system or active Python. It pins **CPython 3.12** (its `kokoro` pin caps Python at
|
|
99
|
+
`<3.13`) and uses **uv** to provision a standalone 3.12: if `uv` isn't present,
|
|
100
|
+
seldon bootstraps it into `~/.seldon/bin` with no shell/PATH changes. If uv can't
|
|
101
|
+
be installed, it falls back to a matching `python3.12` on PATH.
|
|
58
102
|
|
|
59
103
|
## From a checkout
|
|
60
104
|
|
package/bin/seldon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// seldon — the Seldon stack installer. Pick your tools; install each via its
|
|
3
3
|
// native method. Zero dependencies: a raw-terminal checklist, no build step.
|
|
4
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
4
|
+
import { execSync, spawnSync, spawn } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
@@ -170,7 +170,9 @@ function installPython(m, dev) {
|
|
|
170
170
|
run(py, ["-m", "venv", venv]);
|
|
171
171
|
ok = run(path.join(venv, "bin", "pip"), ["install", "-r", req]);
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
// demerzel has no standalone binary — it runs as a module from its venv.
|
|
174
|
+
if (ok && m.id === "demerzel") console.log(C.dim(` run it: seldon up (starts the voice on http://localhost:8770; first run fetches models)`));
|
|
175
|
+
else if (ok) console.log(C.dim(` run it: ${path.join(dest, m.bin)} (isolated venv at ${venv})`));
|
|
174
176
|
return ok;
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -182,7 +184,7 @@ function installOne(m, dev) {
|
|
|
182
184
|
return ok;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
function doInstall(ids, { dev } = {}) {
|
|
187
|
+
async function doInstall(ids, { dev } = {}) {
|
|
186
188
|
ids = withRequires(ids);
|
|
187
189
|
console.log(C.bold(`\nInstalling: `) + ids.join(", ") + (dev ? C.dim(" (dev: link from checkout)") : ""));
|
|
188
190
|
console.log(C.bold("\nPreflight:"));
|
|
@@ -192,6 +194,8 @@ function doInstall(ids, { dev } = {}) {
|
|
|
192
194
|
const good = results.filter(([, r]) => r).map(([i]) => i);
|
|
193
195
|
const bad = results.filter(([, r]) => !r).map(([i]) => i);
|
|
194
196
|
console.log("\n" + C.bold("Done. ") + C.green(good.join(", ") || "—") + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
|
|
197
|
+
// demerzel's models are the heavy part — offer to pull them now, at install time.
|
|
198
|
+
if (good.includes("demerzel")) await ensureDemerzelModels({ ask: true });
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
// ---- uninstall --------------------------------------------------------------
|
|
@@ -278,6 +282,113 @@ async function doUninstall(ids, { yes = false, all = false } = {}) {
|
|
|
278
282
|
console.log("\n" + C.bold("Done. ") + C.green("removed: " + (good.join(", ") || "—")) + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
// ---- up / down / status : the friendly front door ---------------------------
|
|
286
|
+
// The stack's only always-on services are the voice (demerzel) and the
|
|
287
|
+
// supervisor loop (factory watch). We run them as detached background daemons,
|
|
288
|
+
// track them by pidfile under ~/.seldon/run, and print one map of where to go.
|
|
289
|
+
const RUN_DIR = path.join(process.env.HOME, ".seldon", "run");
|
|
290
|
+
const SELDON_HOME = path.join(process.env.HOME, ".seldon");
|
|
291
|
+
const pidFile = (name) => path.join(RUN_DIR, name + ".pid");
|
|
292
|
+
const readPid = (name) => { try { return parseInt(fs.readFileSync(pidFile(name), "utf8").trim(), 10) || 0; } catch { return 0; } };
|
|
293
|
+
const isAlive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
|
294
|
+
const daemonUp = (name) => { const p = readPid(name); return p && isAlive(p) ? p : 0; };
|
|
295
|
+
|
|
296
|
+
function startDaemon(name, cmd, args, opts = {}) {
|
|
297
|
+
if (daemonUp(name)) return { already: true, pid: readPid(name) };
|
|
298
|
+
fs.mkdirSync(RUN_DIR, { recursive: true });
|
|
299
|
+
const log = fs.openSync(path.join(RUN_DIR, name + ".log"), "a");
|
|
300
|
+
const child = spawn(cmd, args, { detached: true, stdio: ["ignore", log, log], ...opts });
|
|
301
|
+
fs.writeFileSync(pidFile(name), String(child.pid));
|
|
302
|
+
child.unref();
|
|
303
|
+
return { pid: child.pid };
|
|
304
|
+
}
|
|
305
|
+
function stopDaemon(name) {
|
|
306
|
+
const pid = readPid(name);
|
|
307
|
+
if (!pid || !isAlive(pid)) { try { fs.rmSync(pidFile(name), { force: true }); } catch {} return false; }
|
|
308
|
+
try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch {} }
|
|
309
|
+
try { fs.rmSync(pidFile(name), { force: true }); } catch {}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
// demerzel needs its model set in the HF cache before it can serve.
|
|
313
|
+
function demerzelModelsReady() {
|
|
314
|
+
const hub = path.join(process.env.HOME, ".cache", "huggingface", "hub");
|
|
315
|
+
try { return fs.existsSync(hub) && fs.readdirSync(hub).some((d) => d.startsWith("models--")); } catch { return false; }
|
|
316
|
+
}
|
|
317
|
+
const demerzelInstalled = () => fs.existsSync(path.join(SELDON_HOME, "demerzel", ".venv", "bin", "python"));
|
|
318
|
+
|
|
319
|
+
// Fetch demerzel's model set (~25 GB) — offered, never silent. Respects the
|
|
320
|
+
// chosen LLM (DEMERZEL_LLM) so `seldon install demerzel` / `seldon up` pull the
|
|
321
|
+
// model you'll actually run. --models forces yes, --no-models forces skip.
|
|
322
|
+
async function ensureDemerzelModels({ ask = true } = {}) {
|
|
323
|
+
if (!demerzelInstalled() || !platformOk(byId.demerzel)) return false;
|
|
324
|
+
if (demerzelModelsReady()) return true;
|
|
325
|
+
const home = path.join(SELDON_HOME, "demerzel");
|
|
326
|
+
const force = process.argv.includes("--models");
|
|
327
|
+
const skip = process.argv.includes("--no-models");
|
|
328
|
+
let doit = force;
|
|
329
|
+
if (!doit && !skip) doit = process.stdin.isTTY
|
|
330
|
+
? await confirm(C.gold("\n Download Demerzel's voice models now (~25 GB, one time)? [y/N] "))
|
|
331
|
+
: false; // never pull 25 GB non-interactively without --models
|
|
332
|
+
if (!doit) { console.log(C.dim(" models not fetched — the voice stays off until they are (seldon up will offer again).")); return false; }
|
|
333
|
+
const llm = process.env.DEMERZEL_LLM;
|
|
334
|
+
console.log(C.dim(` fetching Demerzel models${llm ? ` (LLM ${llm})` : ""} — ~25 GB, resumable…`));
|
|
335
|
+
return run(path.join(home, ".venv/bin/python"), [path.join(home, "scripts/fetch-models.py")], { cwd: home, env: { ...process.env } });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function up() {
|
|
339
|
+
console.log(C.gold("\n seldon — bringing the stack up\n"));
|
|
340
|
+
const go = []; // where-to-go lines
|
|
341
|
+
const later = []; // things that need one action first
|
|
342
|
+
|
|
343
|
+
// Voice — demerzel (background daemon)
|
|
344
|
+
const dem = byId.demerzel;
|
|
345
|
+
if (demerzelInstalled()) {
|
|
346
|
+
const home = path.join(SELDON_HOME, "demerzel");
|
|
347
|
+
if (!platformOk(dem)) later.push("Voice (demerzel) needs macOS on Apple silicon — skipped here.");
|
|
348
|
+
else {
|
|
349
|
+
if (!demerzelModelsReady()) await ensureDemerzelModels({ ask: true }); // offer the fetch inline
|
|
350
|
+
if (demerzelModelsReady()) {
|
|
351
|
+
const r = startDaemon("demerzel", path.join(home, ".venv/bin/python"), ["-m", "demerzel.server"], { cwd: home, env: { ...process.env } });
|
|
352
|
+
go.push(`${C.bold("Voice")} ${C.cyan("http://localhost:8770")} ${C.dim(r.already ? "(already up)" : "(starting — models load, ~30s)")}`);
|
|
353
|
+
} else {
|
|
354
|
+
later.push("Voice (demerzel): models not fetched yet — run `seldon up` again when you're ready to pull them.");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Supervisor — factory watch (background daemon, the "let it work" loop)
|
|
360
|
+
if (has("factory")) {
|
|
361
|
+
const r = startDaemon("factory-watch", "factory", ["watch", "--all"]);
|
|
362
|
+
go.push(`${C.bold("Supervisor")} ${C.dim(r.already ? "already running" : "running")} — heals agents, catches stalls ${C.dim("· live view: factory board")}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// The rest are on-demand, not daemons — point at the command.
|
|
366
|
+
if (has("factory")) go.push(`${C.bold("Put agents to work")} ${C.cyan('factory new "<your idea>"')} ${C.dim("→ repo · foundation · apiary hive")}`);
|
|
367
|
+
if (has("apiary")) go.push(`${C.bold("Agent rooms")} ${C.cyan("apiary ps")} ${C.dim("(active rooms + join links)")}`);
|
|
368
|
+
if (has("bifrost")) go.push(`${C.bold("Across machines / phone")} ${C.cyan("bifrost sessions")}`);
|
|
369
|
+
|
|
370
|
+
if (go.length) { console.log(C.bold(" Where to go:")); for (const l of go) console.log(" • " + l); }
|
|
371
|
+
else console.log(C.dim(" nothing to start — install services first: seldon install factory demerzel"));
|
|
372
|
+
if (later.length) { console.log("\n" + C.gold(" First, one step:")); for (const l of later) console.log(" • " + l); }
|
|
373
|
+
console.log("\n " + C.dim("stop everything: seldon down · check state: seldon status") + "\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function down() {
|
|
377
|
+
const names = ["demerzel", "factory-watch"];
|
|
378
|
+
const stopped = names.filter((n) => stopDaemon(n));
|
|
379
|
+
console.log(stopped.length ? C.green("stopped: ") + stopped.join(", ") : C.dim("nothing was running."));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function statusCmd() {
|
|
383
|
+
console.log(C.bold("\nseldon services\n"));
|
|
384
|
+
const rows = [["Voice (demerzel)", "demerzel"], ["Supervisor (factory watch)", "factory-watch"]];
|
|
385
|
+
for (const [label, name] of rows) {
|
|
386
|
+
const pid = daemonUp(name);
|
|
387
|
+
console.log(` ${label.padEnd(28)} ${pid ? C.green("up") + C.dim(` (pid ${pid})`) : C.dim("down")}`);
|
|
388
|
+
}
|
|
389
|
+
console.log(C.dim(`\n logs: ${RUN_DIR}/<service>.log\n`));
|
|
390
|
+
}
|
|
391
|
+
|
|
281
392
|
// ---- the checklist TUI ------------------------------------------------------
|
|
282
393
|
const BANNER = [
|
|
283
394
|
" ███████╗███████╗██╗ ██████╗ ██████╗ ███╗ ██╗",
|
|
@@ -349,6 +460,8 @@ function help() {
|
|
|
349
460
|
${C.bold("seldon")} — install the Seldon stack, pick what you use.
|
|
350
461
|
|
|
351
462
|
${C.bold("seldon")} open the checklist (space to pick, enter to install)
|
|
463
|
+
${C.bold("seldon up")} start the stack (voice + supervisor) and print where to go
|
|
464
|
+
${C.bold("seldon status")} what's running · ${C.bold("seldon down")} stop it
|
|
352
465
|
${C.bold("seldon install")} [ids…] install everything picked, or the named modules
|
|
353
466
|
${C.bold("seldon uninstall")} ids… remove the named modules (global CLI / isolated venv)
|
|
354
467
|
${C.bold("seldon uninstall --all")} remove the whole stack (also drops ~/.seldon)
|
|
@@ -387,11 +500,14 @@ const ids = tokens.filter((id) => byId[id]); // only the valid module
|
|
|
387
500
|
if (bad.length) { console.log(C.red(`unknown module(s): ${bad.join(", ")}`)); console.log(C.dim(`modules: ${MODULES.map((m) => m.id).join(", ")}`)); process.exitCode = 1; return; }
|
|
388
501
|
return doUninstall(ids, { all, yes: argv.includes("--yes") });
|
|
389
502
|
}
|
|
503
|
+
if (cmd === "up") return up();
|
|
504
|
+
if (cmd === "down") return down();
|
|
505
|
+
if (cmd === "status") return statusCmd();
|
|
390
506
|
if (cmd && cmd !== "install") { help(); process.exitCode = 1; return; }
|
|
391
507
|
// interactive
|
|
392
508
|
if (!process.stdin.isTTY) { console.log(C.red("no TTY — use: seldon install <ids…>")); process.exitCode = 1; return; }
|
|
393
509
|
const picked = await tui();
|
|
394
510
|
if (!picked) { console.log(C.dim("nothing installed.")); return; }
|
|
395
511
|
if (!picked.length) { console.log(C.dim("nothing selected.")); return; }
|
|
396
|
-
doInstall(picked, { dev });
|
|
512
|
+
await doInstall(picked, { dev });
|
|
397
513
|
})();
|