create-op-node 0.5.0 → 0.6.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 CHANGED
@@ -28,6 +28,52 @@ npx create-op-node bootstrap
28
28
 
29
29
  Configures macOS power settings, installs Homebrew + the CLI tool list, sets up Docker Desktop + Tailscale + Ollama, clones the node repo you created, reads the pgsodium key + Tunnel token from the Studio's Keychain (or prompts you to paste them once, then persists for re-runs), writes the LaunchAgent plist, logs into ghcr.io, pulls + warms the LLM model, and finally `docker compose --profile public pull && up -d` brings the whole stack online. Health-check loop waits until all containers are `(healthy)`.
30
30
 
31
+ ### Choosing the LLM model
32
+
33
+ By default bootstrap pulls `qwen3.5:9b` (LLM) and `nomic-embed-text`
34
+ (embeddings) — small enough to validate the inference path on first
35
+ run. Override with flags:
36
+
37
+ ```bash
38
+ # Just swap the LLM, keep the default embedding model:
39
+ npx create-op-node bootstrap --region us-ca --llm-model llama3.3:70b
40
+
41
+ # Override both:
42
+ npx create-op-node bootstrap \
43
+ --region us-ca \
44
+ --llm-model llama3.3:70b \
45
+ --embedding-model mxbai-embed-large
46
+ ```
47
+
48
+ The chosen models flow two places:
49
+
50
+ 1. **Ollama**: bootstrap pulls + warms them so the daemon has them
51
+ resident before the stack comes up. The embedding model pulls first
52
+ (small, fast feedback); the LLM pulls second (can be tens of GB).
53
+ 2. **LaunchAgent**: the plist exports `LLM_MODEL` and `EMBEDDINGS_MODEL`
54
+ into the launchd session, which Docker Desktop inherits — so compose
55
+ services read them via env, no `.env.production` edits needed.
56
+
57
+ > **`--embedding-model` only takes effect when the knowledge service
58
+ > runs with `EMBEDDINGS_PROVIDER=ollama`.** The default provider is
59
+ > `xenova` (in-process), which bundles its own embedding model and
60
+ > ignores both `EMBEDDINGS_MODEL` and the local Ollama model. Setting
61
+ > `EMBEDDINGS_PROVIDER=ollama` is a separate decision (set in your
62
+ > region repo's `.env.production`) — see `docs/provider-pattern.md`.
63
+
64
+ > **Template contract**: for `--llm-model` to actually change the
65
+ > running model, the region repo's `docker-compose-prod.yml` must use
66
+ > `${LLM_MODEL:-qwen3.5:9b}` (or similar) on the knowledge service's
67
+ > `environment:` block. The current `opuspopuli-node` template does;
68
+ > a fork that hardcodes the value would ignore the flag silently.
69
+
70
+ For RAM sizing, the [Docker resources doc](https://github.com/OpusPopuli/opuspopuli-node/blob/main/docs/docker-resources.md)
71
+ has a tier table: 9B-class needs ~8 GB Ollama; 70B-class needs ~50 GB;
72
+ frontier MoE needs ~80 GB. Allocate Docker the remainder.
73
+
74
+ To switch models post-bootstrap, re-run with the new flag and
75
+ `docker compose down && up -d` to pick up the changed env.
76
+
31
77
  ### Local-only mode (no Cloudflare)
32
78
 
33
79
  For local dev / testing — frontend on your laptop, backend on the Studio
package/dist/cli.js CHANGED
@@ -1189,10 +1189,21 @@ async function writePgsodiumKeyFile(key, keyFile) {
1189
1189
  return { ok: false, reason: `writing ${keyFile} failed: ${err.message}` };
1190
1190
  }
1191
1191
  }
1192
+ var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
1192
1193
  function renderLaunchAgentPlist(input) {
1193
1194
  if (input.tunnelToken !== void 0 && !TUNNEL_TOKEN_RE.test(input.tunnelToken)) {
1194
1195
  throw new Error("Tunnel token contains characters outside the expected base64-url set");
1195
1196
  }
1197
+ if (input.llmModel !== void 0 && !MODEL_NAME_RE.test(input.llmModel)) {
1198
+ throw new Error(
1199
+ `llmModel ${JSON.stringify(input.llmModel)} contains characters not allowed in a launchd setenv value`
1200
+ );
1201
+ }
1202
+ if (input.embeddingModel !== void 0 && !MODEL_NAME_RE.test(input.embeddingModel)) {
1203
+ throw new Error(
1204
+ `embeddingModel ${JSON.stringify(input.embeddingModel)} contains characters not allowed in a launchd setenv value`
1205
+ );
1206
+ }
1196
1207
  if (!SAFE_PATH_RE.test(input.keyFilePath)) {
1197
1208
  throw new Error(
1198
1209
  `keyFilePath ${JSON.stringify(input.keyFilePath)} contains characters not allowed in a launchd path interpolation`
@@ -1200,7 +1211,9 @@ function renderLaunchAgentPlist(input) {
1200
1211
  }
1201
1212
  const setenvLines = [
1202
1213
  `launchctl setenv PGSODIUM_ROOT_KEY "$(cat ${input.keyFilePath})"`,
1203
- ...input.tunnelToken !== void 0 ? [`launchctl setenv TUNNEL_TOKEN "${input.tunnelToken}"`] : []
1214
+ ...input.tunnelToken !== void 0 ? [`launchctl setenv TUNNEL_TOKEN "${input.tunnelToken}"`] : [],
1215
+ ...input.llmModel !== void 0 ? [`launchctl setenv LLM_MODEL "${input.llmModel}"`] : [],
1216
+ ...input.embeddingModel !== void 0 ? [`launchctl setenv EMBEDDINGS_MODEL "${input.embeddingModel}"`] : []
1204
1217
  ];
1205
1218
  const command = setenvLines.join("; ");
1206
1219
  return [
@@ -1255,7 +1268,9 @@ async function setupLaunchAgent(input) {
1255
1268
  try {
1256
1269
  plistContent = renderLaunchAgentPlist({
1257
1270
  keyFilePath: paths.keyFile,
1258
- ...input.tunnelToken !== void 0 ? { tunnelToken: input.tunnelToken } : {}
1271
+ ...input.tunnelToken !== void 0 ? { tunnelToken: input.tunnelToken } : {},
1272
+ ...input.llmModel !== void 0 ? { llmModel: input.llmModel } : {},
1273
+ ...input.embeddingModel !== void 0 ? { embeddingModel: input.embeddingModel } : {}
1259
1274
  });
1260
1275
  } catch (err) {
1261
1276
  return { ok: false, paths, step: "plist", reason: err.message };
@@ -1475,7 +1490,8 @@ function assessHealth(snapshots, requireHealthy) {
1475
1490
 
1476
1491
  // src/lib/ollama.ts
1477
1492
  var OLLAMA_URL = "http://localhost:11434";
1478
- var DEFAULT_MODELS = ["qwen3.5:9b", "nomic-embed-text"];
1493
+ var DEFAULT_LLM_MODEL = "qwen3.5:9b";
1494
+ var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
1479
1495
  var PROBE_ALPINE_TAG = "3.20";
1480
1496
  async function startOllamaService() {
1481
1497
  const res = await safeExeca("brew", ["services", "start", "ollama"]);
@@ -1646,7 +1662,17 @@ var bootstrapCommand = new Command("bootstrap").description(
1646
1662
  "--skip-launch-agent",
1647
1663
  "Skip the LaunchAgent setup (assumes one is already in place)"
1648
1664
  ).default(false)
1649
- ).addOption(new Option("--skip-ollama", "Skip the Ollama model pull + warm").default(false)).addOption(new Option("--skip-stack", "Stop before `docker compose pull && up`").default(false)).addOption(
1665
+ ).addOption(new Option("--skip-ollama", "Skip the Ollama model pull + warm").default(false)).addOption(
1666
+ new Option(
1667
+ "--llm-model <model>",
1668
+ `Ollama LLM model to pull and warm. Default: ${DEFAULT_LLM_MODEL}. Examples: \`llama3.3:70b\`, \`qwen2.5:72b\`. Memory sizing table: docs/docker-resources.md in the opuspopuli-node template (or your region repo's checkout).`
1669
+ )
1670
+ ).addOption(
1671
+ new Option(
1672
+ "--embedding-model <model>",
1673
+ `Ollama embedding model. Default: ${DEFAULT_EMBEDDING_MODEL}. Only takes effect when the knowledge service runs with EMBEDDINGS_PROVIDER=ollama (otherwise embeddings are computed in-process via xenova).`
1674
+ )
1675
+ ).addOption(new Option("--skip-stack", "Stop before `docker compose pull && up`").default(false)).addOption(
1650
1676
  new Option(
1651
1677
  "--local-only",
1652
1678
  "Run for local dev / testing: no Tunnel token required, cloudflared stays down. Auto-generates the pgsodium key if not in Keychain (init unnecessary)."
@@ -1664,6 +1690,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1664
1690
  const repoName = `opuspopuli-node-${region}`;
1665
1691
  const composeFileDefault = opts.localOnly ? ["docker-compose-prod.yml"] : ["docker-compose-prod.yml", "docker-compose-backup.yml"];
1666
1692
  const composeFile = opts.composeFile ?? composeFileDefault;
1693
+ const [embeddingModel, llmModel] = resolveModels(opts);
1667
1694
  const sysSpin = p3.spinner();
1668
1695
  sysSpin.start("Inspecting macOS\u2026");
1669
1696
  const snap = await inspectSystem();
@@ -1799,7 +1826,9 @@ var bootstrapCommand = new Command("bootstrap").description(
1799
1826
  laSpin.start("Writing pgsodium key file + LaunchAgent plist\u2026");
1800
1827
  const la = await setupLaunchAgent({
1801
1828
  pgsodiumKey,
1802
- ...tunnelToken !== void 0 ? { tunnelToken } : {}
1829
+ ...tunnelToken !== void 0 ? { tunnelToken } : {},
1830
+ llmModel,
1831
+ embeddingModel
1803
1832
  });
1804
1833
  if (!la.ok) {
1805
1834
  laSpin.stop(pc2.red(`\u2717 LaunchAgent step ${la.step} failed.`));
@@ -1847,7 +1876,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1847
1876
  process.exit(1);
1848
1877
  }
1849
1878
  }
1850
- const modelReport = await setupModels(DEFAULT_MODELS, (model, status) => {
1879
+ const modelReport = await setupModels([embeddingModel, llmModel], (model, status) => {
1851
1880
  olSpin.message(`${status}: ${model}`);
1852
1881
  });
1853
1882
  olSpin.stop(
@@ -1966,6 +1995,12 @@ function resolveComposeFiles(repoPath, composeFile) {
1966
1995
  const inputs = composeFile ?? ["docker-compose-prod.yml"];
1967
1996
  return inputs.map((f) => f.startsWith("/") ? f : join(repoPath, f));
1968
1997
  }
1998
+ function resolveModels(opts) {
1999
+ return [
2000
+ opts.embeddingModel ?? DEFAULT_EMBEDDING_MODEL,
2001
+ opts.llmModel ?? DEFAULT_LLM_MODEL
2002
+ ];
2003
+ }
1969
2004
  function kvBool(b) {
1970
2005
  return b ? pc2.green("on") : pc2.yellow("off");
1971
2006
  }
@@ -3808,7 +3843,7 @@ async function looksLikeRegionsRepo(dir) {
3808
3843
  }
3809
3844
 
3810
3845
  // src/cli.ts
3811
- var VERSION = "0.5.0";
3846
+ var VERSION = "0.6.0";
3812
3847
  var program = new Command();
3813
3848
  program.name("create-op-node").description(
3814
3849
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."