create-op-node 0.13.0 → 0.14.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
@@ -238,7 +238,7 @@ npx create-op-node verify --domain your-domain.example
238
238
  ```
239
239
 
240
240
  Off-LAN health probe of a live node, runnable from anywhere with internet
241
- access. Five phases:
241
+ access. Six phases:
242
242
 
243
243
  1. **TLS handshake** to `api.<domain>:443` — surfaces cert subject, issuer,
244
244
  and days-to-expiry. Warns when the cert is within `--cert-warn-days`
@@ -248,12 +248,22 @@ access. Five phases:
248
248
  3. **`POST https://api.<domain>/api`** with `{ __typename }` must return a
249
249
  valid GraphQL envelope (catches the "TLS green, but a misconfigured
250
250
  proxy returns HTML" case).
251
- 4. **Cloudflare Tunnel status** (optional) — looks up `connections` via
251
+ 4. **Ollama model presence** (optional, node-local) — asserts the configured
252
+ `LLM_MODEL` is actually pulled into the local Ollama, failing loudly with
253
+ the exact `ollama pull <model>` remedy if not. This catches the
254
+ config↔runtime drift that otherwise 404s at inference time (a node set to
255
+ a model that was never downloaded). The model is read from `--llm-model`
256
+ or, when omitted, the node's `.env` (`--repo-dir`, default cwd) — so
257
+ running `verify` **on the node** needs no flags. **Skipped** when no model
258
+ resolves, which keeps an off-LAN `verify --domain …` from tripping it. The
259
+ embedding model is asserted only under `--embeddings-provider ollama`
260
+ (xenova computes embeddings in-process).
261
+ 5. **Cloudflare Tunnel status** (optional) — looks up `connections` via
252
262
  the CF API. Zero connectors registered → warning that cloudflared on
253
263
  the Studio is offline. Requires all three of `--cf-token` (or
254
264
  `--cf-token-file`), `--cf-account-id`, `--tunnel-id`; partial
255
265
  configuration warns + names the missing flag.
256
- 5. **`cosign verify`** (optional, repeatable `--image`) — keyless
266
+ 6. **`cosign verify`** (optional, repeatable `--image`) — keyless
257
267
  verification against the GitHub Actions OIDC issuer + Fulcio +
258
268
  the Rekor transparency log. Silently skipped when `cosign` isn't on
259
269
  `PATH` (install with `brew install cosign` to enable).
@@ -281,6 +291,17 @@ invocations — the latter ends up in `ps` output, the former doesn't.
281
291
  Use `--api-host <host>` to override the default `api.<domain>`
282
292
  construction when your node exposes the API at a different subdomain.
283
293
 
294
+ Run it **on the node** (from the region repo dir) to also catch model
295
+ drift with zero extra flags — the Ollama phase reads `LLM_MODEL` from the
296
+ `.env` bootstrap wrote:
297
+
298
+ ```bash
299
+ cd ~/Development/opuspopuli-node-us-ca
300
+ npx create-op-node verify --domain yournode.example.org
301
+ # or pin the model explicitly from anywhere:
302
+ npx create-op-node verify --domain yournode.example.org --llm-model qwen3.6:35b-a3b
303
+ ```
304
+
284
305
  ## Bootstrapping a region config
285
306
 
286
307
  A node serves data; **what** data it serves is defined by a declarative region
package/dist/cli.js CHANGED
@@ -2122,6 +2122,11 @@ async function checkOllamaHealth(url = OLLAMA_URL) {
2122
2122
  clearTimeout(timer);
2123
2123
  }
2124
2124
  }
2125
+ function modelPresent(configured, installed) {
2126
+ const norm = (m) => m.includes(":") ? m : `${m}:latest`;
2127
+ const target = norm(configured);
2128
+ return installed.some((m) => norm(m) === target);
2129
+ }
2125
2130
  async function pullModel(name) {
2126
2131
  const res = await safeExeca("ollama", ["pull", name]);
2127
2132
  if (res === null) return { ok: false, reason: "`ollama` not on PATH" };
@@ -2321,6 +2326,25 @@ function joinTrimmed(region) {
2321
2326
  while (end > start && joined[end - 1] === "\n") end--;
2322
2327
  return joined.slice(start, end);
2323
2328
  }
2329
+ async function readEnvModelConfig(repoDir) {
2330
+ let content;
2331
+ try {
2332
+ content = await readFile(join(repoDir, ".env"), "utf8");
2333
+ } catch {
2334
+ return {};
2335
+ }
2336
+ const map = parseEnvContent(content);
2337
+ const cfg = {};
2338
+ const llm = map.get("LLM_MODEL");
2339
+ const emb = map.get("EMBEDDINGS_MODEL");
2340
+ const prov = map.get("EMBEDDINGS_PROVIDER");
2341
+ const node = map.get("NODE_ENV");
2342
+ if (llm !== void 0) cfg.llmModel = llm;
2343
+ if (emb !== void 0) cfg.embeddingModel = emb;
2344
+ if (prov !== void 0) cfg.embeddingsProvider = prov;
2345
+ if (node !== void 0) cfg.nodeEnv = node;
2346
+ return cfg;
2347
+ }
2324
2348
  async function writeManagedEnv(repoDir, selection, opts = {}) {
2325
2349
  const target = join(repoDir, ".env");
2326
2350
  let existing = "";
@@ -2476,7 +2500,7 @@ var bootstrapCommand = new Command("bootstrap").description(
2476
2500
  await runLaunchAgentPhase({ opts, secrets });
2477
2501
  await installWrapperPhase({ repoPath, region, promptServiceUrl: secrets.promptServiceUrl });
2478
2502
  await loginGhcrPhase();
2479
- await runOllamaPhase({ opts, embeddingModel, llmModel });
2503
+ await runOllamaPhase({ opts, embeddingModel, llmModel, embeddingsProvider });
2480
2504
  await runEnvFilePhase({
2481
2505
  repoPath,
2482
2506
  llmModel,
@@ -2898,8 +2922,12 @@ async function loginGhcrPhase() {
2898
2922
  }
2899
2923
  ghcrSpin.stop(pc2.green("\u2713 Logged in to ghcr.io."));
2900
2924
  }
2925
+ function modelsToPull(args) {
2926
+ const { provider, llmModel, embeddingModel } = args;
2927
+ return provider === "ollama" ? [embeddingModel, llmModel] : [llmModel];
2928
+ }
2901
2929
  async function runOllamaPhase(args) {
2902
- const { opts, embeddingModel, llmModel } = args;
2930
+ const { opts, embeddingModel, llmModel, embeddingsProvider } = args;
2903
2931
  if (opts.skipOllama) return;
2904
2932
  const olSpin = p3.spinner();
2905
2933
  olSpin.start(`Pulling + warming Ollama models\u2026 (${estimatedPullTime(llmModel)})`);
@@ -2926,7 +2954,8 @@ async function runOllamaPhase(args) {
2926
2954
  process.exit(1);
2927
2955
  }
2928
2956
  }
2929
- const modelReport = await setupModels([embeddingModel, llmModel], (model, status) => {
2957
+ const toPull = modelsToPull({ provider: embeddingsProvider, llmModel, embeddingModel });
2958
+ const modelReport = await setupModels(toPull, (model, status) => {
2930
2959
  olSpin.message(`${status}: ${model}`);
2931
2960
  });
2932
2961
  olSpin.stop(
@@ -2934,6 +2963,12 @@ async function runOllamaPhase(args) {
2934
2963
  `\u2713 ${modelReport.pulled.length} pulled, ${modelReport.alreadyPresent.length} present, ${modelReport.warmed.length} warmed`
2935
2964
  ) : pc2.yellow(`\u26A0 ${modelReport.failed.length} model pull(s) failed`)
2936
2965
  );
2966
+ if (embeddingsProvider !== "ollama") {
2967
+ p3.note(
2968
+ `${pc2.dim("\xB7")} Skipped pulling the embedding model \u2014 EMBEDDINGS_PROVIDER=${embeddingsProvider} computes embeddings in-process (no Ollama model needed).`,
2969
+ "Embeddings"
2970
+ );
2971
+ }
2937
2972
  const probe = await probeHostDockerInternal();
2938
2973
  if (!probe.ok) {
2939
2974
  p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
@@ -3962,7 +3997,8 @@ var DEFAULT_DEPS2 = {
3962
3997
  http: httpProbe,
3963
3998
  graphql: graphqlProbe,
3964
3999
  tunnel: tunnelStatus,
3965
- cosign: cosignVerifyImage
4000
+ cosign: cosignVerifyImage,
4001
+ ollama: checkOllamaHealth
3966
4002
  };
3967
4003
  async function runVerify(input, deps = DEFAULT_DEPS2) {
3968
4004
  const phases = [];
@@ -3973,6 +4009,7 @@ async function runVerify(input, deps = DEFAULT_DEPS2) {
3973
4009
  await verifyTlsPhase(input, deps, push);
3974
4010
  await verifyHealthPhase(input, deps, push);
3975
4011
  await verifyGraphqlPhase(input, deps, push);
4012
+ await verifyOllamaModelsPhase(input, deps, push);
3976
4013
  await verifyCloudflarePhase(input, deps, push);
3977
4014
  await verifyCosignPhase(input, deps, push);
3978
4015
  return { phases };
@@ -4010,6 +4047,39 @@ async function verifyGraphqlPhase(input, deps, push) {
4010
4047
  push({ name: "GraphQL { __typename }", status: "fail", detail: gql.reason });
4011
4048
  }
4012
4049
  }
4050
+ async function verifyOllamaModelsPhase(input, deps, push) {
4051
+ const cfg = input.ollama;
4052
+ if (!cfg?.llmModel) {
4053
+ push({
4054
+ name: "Ollama models",
4055
+ status: "skipped",
4056
+ detail: "pass --llm-model (or run on the node so its .env is read) to enable"
4057
+ });
4058
+ return;
4059
+ }
4060
+ const health = await deps.ollama();
4061
+ if (!health.reachable) {
4062
+ push({
4063
+ name: "Ollama models",
4064
+ status: "fail",
4065
+ detail: "configured model set but the local Ollama daemon is unreachable on :11434 \u2014 start it with `brew services start ollama` (run this check on the node)"
4066
+ });
4067
+ return;
4068
+ }
4069
+ const required = [cfg.llmModel];
4070
+ if (cfg.provider === "ollama" && cfg.embeddingModel) required.push(cfg.embeddingModel);
4071
+ const missing = required.filter((m) => !modelPresent(m, health.models));
4072
+ if (missing.length > 0) {
4073
+ const remedy = missing.map((m) => `ollama pull ${m}`).join(" && ");
4074
+ push({
4075
+ name: "Ollama models",
4076
+ status: "fail",
4077
+ detail: `not installed: ${missing.join(", ")} \u2014 remedy: ${remedy}`
4078
+ });
4079
+ return;
4080
+ }
4081
+ push({ name: "Ollama models", status: "ok", detail: `${required.join(", ")} present` });
4082
+ }
4013
4083
  async function verifyCloudflarePhase(input, deps, push) {
4014
4084
  const cfFields = [
4015
4085
  ["--cf-token", input.cf?.token],
@@ -4094,6 +4164,17 @@ var verifyCommand = new Command("verify").description(
4094
4164
  )
4095
4165
  ).addOption(
4096
4166
  new Option("--cert-warn-days <n>", "Warn if cert expires within N days").default("14")
4167
+ ).addOption(
4168
+ new Option(
4169
+ "--llm-model <model>",
4170
+ "Assert this model is present in the local Ollama (catches config\u2194runtime drift). Defaults to LLM_MODEL from the node .env when run on the node."
4171
+ )
4172
+ ).addOption(
4173
+ new Option("--embedding-model <model>", "Also assert this embedding model is present (only checked when --embeddings-provider=ollama).")
4174
+ ).addOption(
4175
+ new Option("--embeddings-provider <provider>", "Embeddings provider the node runs; the embedding model is only asserted for `ollama`.").choices(["xenova", "ollama"])
4176
+ ).addOption(
4177
+ new Option("--repo-dir <path>", "Node repo dir whose .env supplies the model config when --llm-model is omitted (default: cwd).")
4097
4178
  ).addOption(
4098
4179
  new Option("--show-skipped", "Include skipped phases in the summary").default(false)
4099
4180
  ).action(async (opts) => {
@@ -4103,14 +4184,16 @@ var verifyCommand = new Command("verify").description(
4103
4184
  const cfToken = resolveCfToken(opts);
4104
4185
  const apiHost = opts.apiHost ?? `api.${domain}`;
4105
4186
  const images = opts.image ?? [];
4106
- const totalPhases = 4 + (images.length === 0 ? 1 : images.length);
4187
+ const ollama = await resolveOllamaVerifyInput(opts);
4188
+ const totalPhases = 5 + (images.length === 0 ? 1 : images.length);
4107
4189
  const report = await runVerifyWithSpinner({
4108
4190
  apiHost,
4109
4191
  certWarnDays,
4110
4192
  cfToken,
4111
4193
  opts,
4112
4194
  images,
4113
- totalPhases
4195
+ totalPhases,
4196
+ ...ollama ? { ollama } : {}
4114
4197
  });
4115
4198
  renderVerifySummary(report, { opts, totalPhases });
4116
4199
  reportVerifyOutcome(report);
@@ -4181,8 +4264,34 @@ function resolveCfToken(opts) {
4181
4264
  process.exit(2);
4182
4265
  }
4183
4266
  }
4267
+ function normalizeProvider(v) {
4268
+ return v === "ollama" || v === "xenova" ? v : void 0;
4269
+ }
4270
+ function mergeOllamaModelConfig(opts, envCfg) {
4271
+ const llmModel = opts.llmModel ?? envCfg.llmModel;
4272
+ if (!llmModel) return void 0;
4273
+ const embeddingModel = opts.embeddingModel ?? envCfg.embeddingModel;
4274
+ const provider = normalizeProvider(opts.embeddingsProvider ?? envCfg.embeddingsProvider);
4275
+ return {
4276
+ llmModel,
4277
+ ...embeddingModel ? { embeddingModel } : {},
4278
+ ...provider ? { provider } : {}
4279
+ };
4280
+ }
4281
+ async function resolveOllamaVerifyInput(opts) {
4282
+ const explicit = opts.repoDir !== void 0;
4283
+ const dir = opts.repoDir ?? process.cwd();
4284
+ const envCfg = explicit || await looksLikeNodeRepo(dir) ? await readEnvModelConfig(dir) : {};
4285
+ if (opts.embeddingsProvider === void 0 && envCfg.embeddingsProvider !== void 0 && normalizeProvider(envCfg.embeddingsProvider) === void 0) {
4286
+ p3.note(
4287
+ `${pc2.yellow("\u26A0")} Unrecognized EMBEDDINGS_PROVIDER=${envCfg.embeddingsProvider} in .env \u2014 expected xenova|ollama; the embedding-model check is disabled.`,
4288
+ "verify"
4289
+ );
4290
+ }
4291
+ return mergeOllamaModelConfig(opts, envCfg);
4292
+ }
4184
4293
  async function runVerifyWithSpinner(args) {
4185
- const { apiHost, certWarnDays, cfToken, opts, images, totalPhases } = args;
4294
+ const { apiHost, certWarnDays, cfToken, opts, images, totalPhases, ollama } = args;
4186
4295
  let phaseIndex = 0;
4187
4296
  let activeSpin = null;
4188
4297
  const renderPhase = (ph) => {
@@ -4203,7 +4312,8 @@ async function runVerifyWithSpinner(args) {
4203
4312
  ...opts.cfAccountId ? { accountId: opts.cfAccountId } : {},
4204
4313
  ...opts.tunnelId ? { tunnelId: opts.tunnelId } : {}
4205
4314
  },
4206
- images
4315
+ images,
4316
+ ...ollama ? { ollama } : {}
4207
4317
  },
4208
4318
  deps
4209
4319
  );
@@ -5318,7 +5428,7 @@ function withDefaultSubcommand(argv, known) {
5318
5428
  }
5319
5429
 
5320
5430
  // src/cli.ts
5321
- var VERSION = "0.13.0";
5431
+ var VERSION = "0.14.0";
5322
5432
  var program = new Command();
5323
5433
  program.name("create-op-node").description(
5324
5434
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."