@gorlitzer-labs/seldon 0.1.19 → 0.1.21

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 CHANGED
@@ -37,7 +37,9 @@ seldon up # starts the voice (demerzel) + the supervisor (factory wat
37
37
  # then prints: Voice → http://localhost:8770, how to put agents
38
38
  # to work, rooms, phone access
39
39
  seldon up --tailnet # also expose the voice on your tailnet → reach it from your phone
40
- seldon status # what's running (pids)
40
+ seldon status # what's running (pids) — and warns if docker's daemon is down
41
+ seldon adopt [dir] # put an existing repo on the line (foundation · hive · supervised)
42
+ # and print what's next from its queue. Safe to re-run.
41
43
  seldon down # stop everything
42
44
  ```
43
45
 
package/bin/seldon.js CHANGED
@@ -158,7 +158,11 @@ function installPython(m, dev) {
158
158
  const venvArgs = ["venv", venv, "--seed"]; // --seed puts pip in the venv:
159
159
  // kokoro/misaki/spaCy shells out to pip at runtime to load its model, and a
160
160
  // bare uv venv has none (crashes the voice on start otherwise).
161
- if (m.pyVersion) venvArgs.push("--python", m.pyVersion); // uv fetches a standalone CPython
161
+ // uv fetches a standalone CPython — but only if told to. By default it PREFERS
162
+ // a matching system interpreter, and a python.org framework build ships with
163
+ // no CA bundle, so the venv's urllib failed every HTTPS download (the CAM++
164
+ // voiceprint model silently never arrived). only-managed makes the comment true.
165
+ if (m.pyVersion) venvArgs.push("--python", m.pyVersion, "--python-preference", "only-managed");
162
166
  if (!run(uv, venvArgs)) return false;
163
167
  ok = run(uv, ["pip", "install", "--python", path.join(venv, "bin", "python"), "-r", req]);
164
168
  } else {
@@ -330,7 +334,9 @@ function startDaemon(name, cmd, args, opts = {}) {
330
334
  if (daemonUp(name)) return { already: true, pid: readPid(name) };
331
335
  fs.mkdirSync(RUN_DIR, { recursive: true });
332
336
  const log = fs.openSync(path.join(RUN_DIR, name + ".log"), "a");
333
- const child = spawn(cmd, args, { detached: true, stdio: ["ignore", log, log], ...opts });
337
+ // cwd defaults to SELDON_HOME: a long-lived daemon must not pin whatever folder
338
+ // `seldon up` was typed in (it did — the supervisor held a project dir open).
339
+ const child = spawn(cmd, args, { detached: true, stdio: ["ignore", log, log], cwd: SELDON_HOME, ...opts });
334
340
  fs.writeFileSync(pidFile(name), String(child.pid));
335
341
  child.unref();
336
342
  return { pid: child.pid };
@@ -342,11 +348,24 @@ function stopDaemon(name) {
342
348
  try { fs.rmSync(pidFile(name), { force: true }); } catch {}
343
349
  return true;
344
350
  }
345
- // demerzel needs its model set in the HF cache before it can serve.
346
- function demerzelModelsReady() {
351
+ // demerzel needs its model set before it can serve. Check each model it loads by
352
+ // name — "any models-- dir in the HF cache" was true after a fetch died halfway,
353
+ // so the one it died on (the CAM++ voiceprint, the last download) was never
354
+ // offered again. The LLM is not listed: which one depends on the brain, and
355
+ // bonsai brings its own. Returns the missing labels; [] means ready.
356
+ function demerzelModelsMissing() {
347
357
  const hub = path.join(process.env.HOME, ".cache", "huggingface", "hub");
348
- try { return fs.existsSync(hub) && fs.readdirSync(hub).some((d) => d.startsWith("models--")); } catch { return false; }
358
+ const snapshotOk = (repo) => {
359
+ const snaps = path.join(hub, "models--" + repo.replace("/", "--"), "snapshots");
360
+ try { return fs.readdirSync(snaps).some((d) => fs.readdirSync(path.join(snaps, d)).length > 0); } catch { return false; }
361
+ };
362
+ const missing = [];
363
+ if (!snapshotOk("mlx-community/Qwen3-ASR-1.7B-8bit")) missing.push("ears (Qwen3-ASR)");
364
+ if (!snapshotOk("hexgrad/Kokoro-82M")) missing.push("voice (Kokoro)");
365
+ if (!fs.existsSync(path.join(SELDON_HOME, "demerzel", "models", "campplus_en.onnx"))) missing.push("voiceprint (CAM++)");
366
+ return missing;
349
367
  }
368
+ const demerzelModelsReady = () => demerzelModelsMissing().length === 0;
350
369
  const demerzelInstalled = () => fs.existsSync(path.join(SELDON_HOME, "demerzel", ".venv", "bin", "python"));
351
370
 
352
371
  // This machine's Tailscale IPv4 (100.64.0.0/10), or null. Tries PATH then the
@@ -593,6 +612,7 @@ async function up() {
593
612
 
594
613
  // The rest are on-demand, not daemons — point at the command.
595
614
  if (has("factory")) go.push(`${C.bold("Put agents to work")} ${C.cyan('factory new "<your idea>"')} ${C.dim("→ repo · foundation · apiary hive")}`);
615
+ if (has("factory")) go.push(`${C.bold("An existing repo")} ${C.cyan("seldon adopt [dir]")} ${C.dim("→ foundation (keeps your docs) · hive · supervised")}`);
596
616
  if (has("apiary")) go.push(`${C.bold("Agent rooms")} ${C.cyan("apiary ps")} ${C.dim("(active rooms + join links)")}`);
597
617
  if (has("bifrost")) go.push(`${C.bold("Across machines / phone")} ${C.cyan("bifrost sessions")}`);
598
618
 
@@ -671,7 +691,7 @@ function statusCmd() {
671
691
  if (!inst) detail = platformOk(m) ? C.dim(`not installed ${C.dim("· seldon install " + m.id)}`) : C.dim("not supported on this machine");
672
692
  else if (m.method === "npm") detail = C.dim("v" + (npmGlobalVersion(m.pkg) || "?"));
673
693
  else if (m.id === "bifrost") detail = C.dim("v" + (bifrostVersion() || "?"));
674
- else if (m.id === "demerzel") detail = demerzelModelsReady() ? C.dim("venv + models ✓") : C.gold("venv ok · models not fetched (seldon up)");
694
+ else if (m.id === "demerzel") detail = demerzelModelsReady() ? C.dim("venv + models ✓") : C.gold(`venv ok · missing ${demerzelModelsMissing().join(", ")} (seldon up)`);
675
695
  else detail = C.dim("installed");
676
696
 
677
697
  // service state for the two daemons
@@ -690,9 +710,31 @@ function statusCmd() {
690
710
  const phone = tailscaleServeUrl();
691
711
  if (phone) console.log(C.dim(" phone (HTTPS): ") + C.cyan(phone));
692
712
  }
713
+ const dk = dockerState();
714
+ if (dk === "down") console.log(C.gold("\n ⚠ docker: installed but the daemon is not running") + C.dim(" — factory box and containerised gates need it (open Docker Desktop)"));
715
+ else if (dk === "up") console.log(C.dim("\n docker: ") + C.green("up"));
693
716
  console.log(C.dim(`\n start: seldon up · stop: seldon down · logs: ${RUN_DIR}/\n`));
694
717
  }
695
718
 
719
+ // "none" (no docker CLI) | "down" (CLI but no daemon) | "up". Only shown when docker exists.
720
+ function dockerState() {
721
+ if (!has("docker")) return "none";
722
+ try { execSync("docker info", { stdio: "ignore", timeout: 8000 }); return "up"; } catch { return "down"; }
723
+ }
724
+
725
+ // ---- adopt: bring an existing repo onto the line ----------------------------
726
+ // factory owns the registry + hive, so this is a front door onto `factory adopt`.
727
+ function adopt(target) {
728
+ if (!has("factory")) {
729
+ console.log(C.red(" adopt needs factory — install it: ") + C.cyan("seldon install factory"));
730
+ process.exitCode = 1; return;
731
+ }
732
+ const passthru = process.argv.slice(3).filter((a) => a !== "--dev");
733
+ const r = spawnSync("factory", ["adopt", ...passthru], { stdio: "inherit" });
734
+ if (r.status !== 0) { process.exitCode = r.status || 1; return; }
735
+ if (!daemonUp("factory-watch")) console.log(C.gold(" the supervisor is not running — ") + C.cyan("seldon up") + C.dim(" starts it, so agents here get healed and nudged.") + "\n");
736
+ }
737
+
696
738
  // ---- the checklist TUI ------------------------------------------------------
697
739
  const BANNER = [
698
740
  " ███████╗███████╗██╗ ██████╗ ██████╗ ███╗ ██╗",
@@ -776,6 +818,8 @@ ${C.bold("seldon")} — install the Seldon stack, pick what you use.
776
818
  ${C.bold("seldon up --tailnet")} expose the voice over HTTPS (tailscale serve) so the mic works on your phone
777
819
  ${C.bold("seldon up --brain=X")} pick the voice brain: qwen (in-process) | bonsai (local server); remembered
778
820
  ${C.bold("seldon status")} what's running · ${C.bold("seldon down")} stop it
821
+ ${C.bold("seldon adopt")} [dir] put an existing repo on the line: foundation docs (never overwritten),
822
+ an apiary hive, supervised by factory — and print what's next. Safe to re-run.
779
823
  ${C.bold("seldon install")} [ids…] install everything picked, or the named modules
780
824
  ${C.bold("seldon uninstall")} ids… remove the named modules (global CLI / isolated venv)
781
825
  ${C.bold("seldon uninstall --all")} remove the whole stack (also drops ~/.seldon)
@@ -817,6 +861,7 @@ const ids = tokens.filter((id) => byId[id]); // only the valid module
817
861
  if (cmd === "up") return up();
818
862
  if (cmd === "down") return down();
819
863
  if (cmd === "status") return statusCmd();
864
+ if (cmd === "adopt") return adopt(tokens[0]);
820
865
  if (cmd && cmd !== "install") { help(); process.exitCode = 1; return; }
821
866
  // interactive
822
867
  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.19",
3
+ "version": "0.1.21",
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": {