@gorlitzer-labs/seldon 0.1.17 → 0.1.19
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 +16 -10
- package/bin/seldon.js +69 -13
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -49,11 +49,17 @@ Bonsai, seldon fetches its runtime + model (~7 GB, macOS/Apple-silicon) and runs
|
|
|
49
49
|
the server as a managed daemon; `seldon status` shows the active brain, `seldon
|
|
50
50
|
down` stops it.
|
|
51
51
|
|
|
52
|
-
**Phone access:** `seldon up --tailnet`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`
|
|
52
|
+
**Phone access:** `seldon up --tailnet` serves the voice over **HTTPS** on your
|
|
53
|
+
tailnet (via `tailscale serve`) and prints a `https://<machine>.<tailnet>.ts.net`
|
|
54
|
+
URL — open it on your phone and the **mic works** (browsers require HTTPS for the
|
|
55
|
+
microphone; plain http can't). The default `seldon up` stays loopback-only, and
|
|
56
|
+
the Mac's own `http://localhost:8770` already works (localhost is a secure context).
|
|
57
|
+
|
|
58
|
+
Needs **HTTPS Certificates enabled** for your tailnet — one toggle at
|
|
59
|
+
[login.tailscale.com/admin/dns](https://login.tailscale.com/admin/dns) → *HTTPS
|
|
60
|
+
Certificates*; `--tailnet` tells you if it isn't. The serve config persists across
|
|
61
|
+
reboots. Since Demerzel can act on the host, anyone on your tailnet can talk to it
|
|
62
|
+
— use `DEMERZEL_READONLY=1` when sharing.
|
|
57
63
|
|
|
58
64
|
`seldon up` only starts what's installed. demerzel serves the voice UI on
|
|
59
65
|
`http://localhost:8770`. Its model set (~25 GB) is a one-time download —
|
|
@@ -61,13 +67,13 @@ exposing it means anyone on your tailnet can talk to it — run with
|
|
|
61
67
|
offers again if it's still missing. Nothing that big is ever pulled silently.
|
|
62
68
|
Logs live in `~/.seldon/run/`.
|
|
63
69
|
|
|
64
|
-
**
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
**Swap the in-process Qwen model:** set `DEMERZEL_LLM` to any MLX-compatible HF
|
|
71
|
+
repo before install/up — the fetch and the runtime both follow it (this only
|
|
72
|
+
changes the *Qwen brain*; for Bonsai use `--brain=bonsai` above, which is a
|
|
73
|
+
separate llama-server, not an MLX repo).
|
|
67
74
|
|
|
68
75
|
```bash
|
|
69
|
-
DEMERZEL_LLM=mlx-community/<
|
|
70
|
-
DEMERZEL_LLM=mlx-community/<bonsai-2-repo> seldon up # run it
|
|
76
|
+
DEMERZEL_LLM=mlx-community/<some-mlx-repo> seldon up
|
|
71
77
|
```
|
|
72
78
|
|
|
73
79
|
## Uninstall
|
package/bin/seldon.js
CHANGED
|
@@ -317,8 +317,10 @@ async function doUninstall(ids, { yes = false, all = false } = {}) {
|
|
|
317
317
|
// The stack's only always-on services are the voice (demerzel) and the
|
|
318
318
|
// supervisor loop (factory watch). We run them as detached background daemons,
|
|
319
319
|
// track them by pidfile under ~/.seldon/run, and print one map of where to go.
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
// SELDON_HOME can be overridden (tests point it at a temp dir; also a legit
|
|
321
|
+
// user override). Everything — run/, bonsai/, demerzel/, brain — derives from it.
|
|
322
|
+
const SELDON_HOME = process.env.SELDON_HOME || path.join(process.env.HOME, ".seldon");
|
|
323
|
+
const RUN_DIR = path.join(SELDON_HOME, "run");
|
|
322
324
|
const pidFile = (name) => path.join(RUN_DIR, name + ".pid");
|
|
323
325
|
const readPid = (name) => { try { return parseInt(fs.readFileSync(pidFile(name), "utf8").trim(), 10) || 0; } catch { return 0; } };
|
|
324
326
|
const isAlive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
|
@@ -371,6 +373,55 @@ function tailnetIp() {
|
|
|
371
373
|
return null;
|
|
372
374
|
}
|
|
373
375
|
|
|
376
|
+
// Find the Tailscale CLI (PATH, homebrew, or the macOS app bundle).
|
|
377
|
+
function tailscaleBin() {
|
|
378
|
+
for (const b of ["tailscale", "/opt/homebrew/bin/tailscale", "/usr/local/bin/tailscale", "/Applications/Tailscale.app/Contents/MacOS/tailscale", "/usr/bin/tailscale"]) {
|
|
379
|
+
try { execSync(`${b} version`, { stdio: "ignore" }); return b; } catch {}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
function tailnetDnsName(ts) {
|
|
384
|
+
try {
|
|
385
|
+
const d = JSON.parse(execSync(`${ts} status --json`, { stdio: ["ignore", "pipe", "ignore"], timeout: 15000 }).toString());
|
|
386
|
+
return ((d.Self && d.Self.DNSName) || "").replace(/\.$/, "");
|
|
387
|
+
} catch { return null; }
|
|
388
|
+
}
|
|
389
|
+
function tailscaleServeUrl() {
|
|
390
|
+
const ts = tailscaleBin(); if (!ts) return null;
|
|
391
|
+
try {
|
|
392
|
+
const out = execSync(`${ts} serve status`, { stdio: ["ignore", "pipe", "ignore"], timeout: 10000 }).toString();
|
|
393
|
+
const m = out.match(/https:\/\/\S+/);
|
|
394
|
+
return m ? m[0] : null;
|
|
395
|
+
} catch { return null; }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Expose the loopback voice on the tailnet over HTTPS via `tailscale serve`, so a
|
|
399
|
+
// phone can use the mic (browsers require a secure context). Provisions the cert,
|
|
400
|
+
// then maps / -> :8770 and /ws -> :8765. Returns { url } or { err } with guidance.
|
|
401
|
+
function tailscaleServeSetup() {
|
|
402
|
+
const ts = tailscaleBin();
|
|
403
|
+
if (!ts) return { err: "Tailscale CLI not found — install Tailscale to reach the voice from your phone." };
|
|
404
|
+
const dns = tailnetDnsName(ts);
|
|
405
|
+
if (!dns) return { err: "couldn't read the tailnet name (is Tailscale connected?)." };
|
|
406
|
+
console.log(C.dim(" setting up HTTPS via tailscale serve (first cert can take ~30s)…"));
|
|
407
|
+
try { execSync(`${ts} cert ${dns}`, { stdio: ["ignore", "ignore", "pipe"], timeout: 120000 }); }
|
|
408
|
+
catch (e) {
|
|
409
|
+
const err = ((e.stderr && e.stderr.toString()) || "") + (e.message || "");
|
|
410
|
+
if (/does not support|HTTPS is not enabled|not enabled/i.test(err))
|
|
411
|
+
return { err: "HTTPS certs aren't enabled for your tailnet. Enable them at https://login.tailscale.com/admin/dns (HTTPS Certificates), then re-run `seldon up --tailnet`." };
|
|
412
|
+
if (/timed out|ETIMEDOUT/i.test(err))
|
|
413
|
+
return { err: "Tailscale is still provisioning the cert — re-run `seldon up --tailnet` in a moment." };
|
|
414
|
+
// otherwise the cert probably already exists — carry on
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
execSync(`${ts} serve --bg http://127.0.0.1:8770`, { stdio: "ignore", timeout: 30000 });
|
|
418
|
+
execSync(`${ts} serve --bg --set-path=/ws http://127.0.0.1:8765`, { stdio: "ignore", timeout: 30000 });
|
|
419
|
+
} catch (e) {
|
|
420
|
+
return { err: "tailscale serve failed: " + ((e.stderr || e.message || "").toString().split("\n")[0]) };
|
|
421
|
+
}
|
|
422
|
+
return { url: `https://${dns}` };
|
|
423
|
+
}
|
|
424
|
+
|
|
374
425
|
// Fetch demerzel's model set (~25 GB) — offered, never silent. Respects the
|
|
375
426
|
// chosen LLM (DEMERZEL_LLM) so `seldon install demerzel` / `seldon up` pull the
|
|
376
427
|
// model you'll actually run. --models forces yes, --no-models forces skip.
|
|
@@ -512,19 +563,22 @@ async function up() {
|
|
|
512
563
|
}
|
|
513
564
|
}
|
|
514
565
|
if (brain === "qwen") go.push(`${C.bold("Brain")} Qwen 3.6-35B ${C.dim("(in-process)")}`);
|
|
515
|
-
//
|
|
566
|
+
// The voice always binds loopback; --tailnet fronts it with HTTPS via
|
|
567
|
+
// `tailscale serve` so a phone can use the mic (needs a secure context).
|
|
568
|
+
env.DEMERZEL_HOST = "127.0.0.1";
|
|
569
|
+
let phoneUrl = null;
|
|
516
570
|
const wantTailnet = process.argv.includes("--tailnet") || process.argv.includes("--phone");
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
571
|
+
if (wantTailnet) {
|
|
572
|
+
const s = tailscaleServeSetup();
|
|
573
|
+
if (s.url) { phoneUrl = s.url; later.push(C.dim("Phone: anyone on your tailnet can talk to Demerzel (it can act on this Mac). Use DEMERZEL_READONLY=1 to share safely.")); }
|
|
574
|
+
else later.push("Phone (--tailnet): " + s.err);
|
|
575
|
+
}
|
|
576
|
+
// (Re)start the voice only when it isn't running, or when --brain changed.
|
|
577
|
+
const explicit = process.argv.some((a) => a.startsWith("--brain="));
|
|
523
578
|
if (explicit && daemonUp("demerzel")) stopDaemon("demerzel");
|
|
524
579
|
const r = startDaemon("demerzel", path.join(home, ".venv/bin/python"), ["-m", "demerzel.server"], { cwd: home, env });
|
|
525
|
-
|
|
526
|
-
go.push(`${C.bold("
|
|
527
|
-
if (host) later.push(C.dim("Voice is on your tailnet — anyone on it can talk to Demerzel (and it can act on this Mac). Run with DEMERZEL_READONLY=1 to share it safely."));
|
|
580
|
+
go.push(`${C.bold("Voice")} ${C.cyan("http://localhost:8770")} ${C.dim(r.already ? "(already up)" : "(starting — models load, ~30s)")}`);
|
|
581
|
+
if (phoneUrl) go.push(`${C.bold("Phone")} ${C.cyan(phoneUrl)} ${C.dim("(HTTPS — mic works from your phone, on your tailnet)")}`);
|
|
528
582
|
} else {
|
|
529
583
|
later.push("Voice (demerzel): models not fetched yet — run `seldon up` again when you're ready to pull them.");
|
|
530
584
|
}
|
|
@@ -633,6 +687,8 @@ function statusCmd() {
|
|
|
633
687
|
? (daemonUp("bonsai") || bonsaiHealthy() ? C.green("server up") : C.dim("server down"))
|
|
634
688
|
: C.dim("in-process");
|
|
635
689
|
console.log(C.dim(`\n voice brain: `) + C.bold(brain) + " " + bstate + C.dim(" · switch: seldon up --brain=qwen|bonsai"));
|
|
690
|
+
const phone = tailscaleServeUrl();
|
|
691
|
+
if (phone) console.log(C.dim(" phone (HTTPS): ") + C.cyan(phone));
|
|
636
692
|
}
|
|
637
693
|
console.log(C.dim(`\n start: seldon up · stop: seldon down · logs: ${RUN_DIR}/\n`));
|
|
638
694
|
}
|
|
@@ -717,7 +773,7 @@ ${C.bold("seldon")} — install the Seldon stack, pick what you use.
|
|
|
717
773
|
|
|
718
774
|
${C.bold("seldon")} open the checklist (space to pick, enter to install)
|
|
719
775
|
${C.bold("seldon up")} start the stack (voice + supervisor) and print where to go
|
|
720
|
-
${C.bold("seldon up --tailnet")}
|
|
776
|
+
${C.bold("seldon up --tailnet")} expose the voice over HTTPS (tailscale serve) so the mic works on your phone
|
|
721
777
|
${C.bold("seldon up --brain=X")} pick the voice brain: qwen (in-process) | bonsai (local server); remembered
|
|
722
778
|
${C.bold("seldon status")} what's running · ${C.bold("seldon down")} stop it
|
|
723
779
|
${C.bold("seldon install")} [ids…] install everything picked, or the named modules
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gorlitzer-labs/seldon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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": {
|
|
@@ -27,5 +27,8 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test test/*.test.mjs"
|
|
30
33
|
}
|
|
31
34
|
}
|