@gorlitzer-labs/seldon 0.1.8 → 0.1.9

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/README.md +38 -5
  2. package/bin/seldon.js +93 -2
  3. 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
- npm i -g @gorlitzer-labs/seldon
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,34 @@ 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 first run needs a one-time model fetch (~25 GB) and
45
+ `up` prints the exact command if they're missing. Logs live in `~/.seldon/run/`.
46
+
47
+ ## Uninstall
48
+
49
+ ```bash
50
+ seldon uninstall comb demerzel # remove specific modules (asks to confirm)
51
+ seldon uninstall --all # remove the whole stack (also drops ~/.seldon)
52
+ seldon uninstall <ids> --yes # skip the confirm prompt
53
+ ```
54
+
55
+ Node tools are removed from the global store; demerzel's isolated venv and
56
+ bifrost's binary + config are deleted. Your system Python, tmux, tailscale,
57
+ sops/age are never touched.
58
+
31
59
  ## The stack
32
60
 
33
61
  | module | what it is | ships via |
@@ -51,10 +79,15 @@ It's a stack, not a bundle — install only what you use:
51
79
 
52
80
  ## Package managers
53
81
 
54
- Node tools install via **pnpm** or **bun** if present (much faster than npm), else npm —
55
- override with `seldon install --pm=pnpm|bun|npm`. demerzel (Python) always installs into
56
- its **own isolated venv** — never your system/active Python — using **uv** if available,
57
- else `python3 -m venv` + pip.
82
+ Node tools install with **pnpm** when it can global-install (much faster than npm),
83
+ else **npm**. **bun** is supported but opt-in — `seldon install --pm=bun` (also
84
+ `--pm=pnpm|npm` to force any of them).
85
+
86
+ demerzel (Python) always installs into its **own isolated venv** — never your
87
+ system or active Python. It pins **CPython 3.12** (its `kokoro` pin caps Python at
88
+ `<3.13`) and uses **uv** to provision a standalone 3.12: if `uv` isn't present,
89
+ seldon bootstraps it into `~/.seldon/bin` with no shell/PATH changes. If uv can't
90
+ be installed, it falls back to a matching `python3.12` on PATH.
58
91
 
59
92
  ## From a checkout
60
93
 
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
- if (ok) console.log(C.dim(` run it: ${path.join(dest, m.bin)} (isolated venv at ${venv})`));
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
 
@@ -278,6 +280,90 @@ async function doUninstall(ids, { yes = false, all = false } = {}) {
278
280
  console.log("\n" + C.bold("Done. ") + C.green("removed: " + (good.join(", ") || "—")) + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
279
281
  }
280
282
 
283
+ // ---- up / down / status : the friendly front door ---------------------------
284
+ // The stack's only always-on services are the voice (demerzel) and the
285
+ // supervisor loop (factory watch). We run them as detached background daemons,
286
+ // track them by pidfile under ~/.seldon/run, and print one map of where to go.
287
+ const RUN_DIR = path.join(process.env.HOME, ".seldon", "run");
288
+ const SELDON_HOME = path.join(process.env.HOME, ".seldon");
289
+ const pidFile = (name) => path.join(RUN_DIR, name + ".pid");
290
+ const readPid = (name) => { try { return parseInt(fs.readFileSync(pidFile(name), "utf8").trim(), 10) || 0; } catch { return 0; } };
291
+ const isAlive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
292
+ const daemonUp = (name) => { const p = readPid(name); return p && isAlive(p) ? p : 0; };
293
+
294
+ function startDaemon(name, cmd, args, opts = {}) {
295
+ if (daemonUp(name)) return { already: true, pid: readPid(name) };
296
+ fs.mkdirSync(RUN_DIR, { recursive: true });
297
+ const log = fs.openSync(path.join(RUN_DIR, name + ".log"), "a");
298
+ const child = spawn(cmd, args, { detached: true, stdio: ["ignore", log, log], ...opts });
299
+ fs.writeFileSync(pidFile(name), String(child.pid));
300
+ child.unref();
301
+ return { pid: child.pid };
302
+ }
303
+ function stopDaemon(name) {
304
+ const pid = readPid(name);
305
+ if (!pid || !isAlive(pid)) { try { fs.rmSync(pidFile(name), { force: true }); } catch {} return false; }
306
+ try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch {} }
307
+ try { fs.rmSync(pidFile(name), { force: true }); } catch {}
308
+ return true;
309
+ }
310
+ // demerzel needs its model set in the HF cache before it can serve.
311
+ function demerzelModelsReady() {
312
+ const hub = path.join(process.env.HOME, ".cache", "huggingface", "hub");
313
+ try { return fs.existsSync(hub) && fs.readdirSync(hub).some((d) => d.startsWith("models--")); } catch { return false; }
314
+ }
315
+ const demerzelInstalled = () => fs.existsSync(path.join(SELDON_HOME, "demerzel", ".venv", "bin", "python"));
316
+
317
+ function up() {
318
+ console.log(C.gold("\n seldon — bringing the stack up\n"));
319
+ const go = []; // where-to-go lines
320
+ const later = []; // things that need one action first
321
+
322
+ // Voice — demerzel (background daemon)
323
+ const dem = byId.demerzel;
324
+ if (demerzelInstalled()) {
325
+ const home = path.join(SELDON_HOME, "demerzel");
326
+ if (!platformOk(dem)) later.push("Voice (demerzel) needs macOS on Apple silicon — skipped here.");
327
+ else if (!demerzelModelsReady()) later.push(`Voice (demerzel) needs its models once (~25 GB):\n ${path.join(home, ".venv/bin/python")} ${path.join(home, "scripts/fetch-models.py")}\n then re-run seldon up`);
328
+ else {
329
+ const r = startDaemon("demerzel", path.join(home, ".venv/bin/python"), ["-m", "demerzel.server"], { cwd: home });
330
+ go.push(`${C.bold("Voice")} ${C.cyan("http://localhost:8770")} ${C.dim(r.already ? "(already up)" : "(starting — models load, ~30s)")}`);
331
+ }
332
+ }
333
+
334
+ // Supervisor — factory watch (background daemon, the "let it work" loop)
335
+ if (has("factory")) {
336
+ const r = startDaemon("factory-watch", "factory", ["watch", "--all"]);
337
+ go.push(`${C.bold("Supervisor")} ${C.dim(r.already ? "already running" : "running")} — heals agents, catches stalls ${C.dim("· live view: factory board")}`);
338
+ }
339
+
340
+ // The rest are on-demand, not daemons — point at the command.
341
+ if (has("factory")) go.push(`${C.bold("Put agents to work")} ${C.cyan('factory new "<your idea>"')} ${C.dim("→ repo · foundation · apiary hive")}`);
342
+ if (has("apiary")) go.push(`${C.bold("Agent rooms")} ${C.cyan("apiary ps")} ${C.dim("(active rooms + join links)")}`);
343
+ if (has("bifrost")) go.push(`${C.bold("Across machines / phone")} ${C.cyan("bifrost sessions")}`);
344
+
345
+ if (go.length) { console.log(C.bold(" Where to go:")); for (const l of go) console.log(" • " + l); }
346
+ else console.log(C.dim(" nothing to start — install services first: seldon install factory demerzel"));
347
+ if (later.length) { console.log("\n" + C.gold(" First, one step:")); for (const l of later) console.log(" • " + l); }
348
+ console.log("\n " + C.dim("stop everything: seldon down · check state: seldon status") + "\n");
349
+ }
350
+
351
+ function down() {
352
+ const names = ["demerzel", "factory-watch"];
353
+ const stopped = names.filter((n) => stopDaemon(n));
354
+ console.log(stopped.length ? C.green("stopped: ") + stopped.join(", ") : C.dim("nothing was running."));
355
+ }
356
+
357
+ function statusCmd() {
358
+ console.log(C.bold("\nseldon services\n"));
359
+ const rows = [["Voice (demerzel)", "demerzel"], ["Supervisor (factory watch)", "factory-watch"]];
360
+ for (const [label, name] of rows) {
361
+ const pid = daemonUp(name);
362
+ console.log(` ${label.padEnd(28)} ${pid ? C.green("up") + C.dim(` (pid ${pid})`) : C.dim("down")}`);
363
+ }
364
+ console.log(C.dim(`\n logs: ${RUN_DIR}/<service>.log\n`));
365
+ }
366
+
281
367
  // ---- the checklist TUI ------------------------------------------------------
282
368
  const BANNER = [
283
369
  " ███████╗███████╗██╗ ██████╗ ██████╗ ███╗ ██╗",
@@ -349,6 +435,8 @@ function help() {
349
435
  ${C.bold("seldon")} — install the Seldon stack, pick what you use.
350
436
 
351
437
  ${C.bold("seldon")} open the checklist (space to pick, enter to install)
438
+ ${C.bold("seldon up")} start the stack (voice + supervisor) and print where to go
439
+ ${C.bold("seldon status")} what's running · ${C.bold("seldon down")} stop it
352
440
  ${C.bold("seldon install")} [ids…] install everything picked, or the named modules
353
441
  ${C.bold("seldon uninstall")} ids… remove the named modules (global CLI / isolated venv)
354
442
  ${C.bold("seldon uninstall --all")} remove the whole stack (also drops ~/.seldon)
@@ -387,6 +475,9 @@ const ids = tokens.filter((id) => byId[id]); // only the valid module
387
475
  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
476
  return doUninstall(ids, { all, yes: argv.includes("--yes") });
389
477
  }
478
+ if (cmd === "up") return up();
479
+ if (cmd === "down") return down();
480
+ if (cmd === "status") return statusCmd();
390
481
  if (cmd && cmd !== "install") { help(); process.exitCode = 1; return; }
391
482
  // interactive
392
483
  if (!process.stdin.isTTY) { console.log(C.red("no TTY — use: seldon install <ids…>")); process.exitCode = 1; return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorlitzer-labs/seldon",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Install the Seldon stack — pick your tools from a checklist; each installs via its native method.",
5
5
  "type": "module",
6
6
  "bin": {