@kilogent/runner 0.1.4 → 0.1.5

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 (2) hide show
  1. package/dist/cli.js +200 -85
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1277,7 +1277,7 @@ import os from "node:os";
1277
1277
  import path from "node:path";
1278
1278
 
1279
1279
  // src/version.ts
1280
- var RUNNER_VERSION = true ? "0.1.4" : "0.0.0-dev";
1280
+ var RUNNER_VERSION = true ? "0.1.5" : "0.0.0-dev";
1281
1281
  var RUNNER_PACKAGE = true ? "@kilogent/runner" : "@kilogent/runner-dev";
1282
1282
  var RUNNER_BIN = true ? "kilogent-runner" : "kilogent-runner-dev";
1283
1283
 
@@ -3273,10 +3273,10 @@ async function claudeHealthCheck(binOverride) {
3273
3273
  const bin = binOverride ?? claudeBinary();
3274
3274
  return new Promise((resolve) => {
3275
3275
  let settled = false;
3276
- const done = (health) => {
3276
+ const done = (health2) => {
3277
3277
  if (settled) return;
3278
3278
  settled = true;
3279
- resolve({ ...health, binary: bin });
3279
+ resolve({ ...health2, binary: bin });
3280
3280
  };
3281
3281
  let stdout = "";
3282
3282
  const child = spawn3(bin, ["--version"], { env: withNodeOnPath(), stdio: ["ignore", "pipe", "ignore"] });
@@ -3806,10 +3806,10 @@ async function codexHealthCheck(binOverride) {
3806
3806
  const bin = binOverride ?? codexBinary();
3807
3807
  return new Promise((resolve) => {
3808
3808
  let settled = false;
3809
- const done = (health) => {
3809
+ const done = (health2) => {
3810
3810
  if (settled) return;
3811
3811
  settled = true;
3812
- resolve({ ...health, binary: bin });
3812
+ resolve({ ...health2, binary: bin });
3813
3813
  };
3814
3814
  let stdout = "";
3815
3815
  const child = spawn4(bin, ["--version"], { env: withNodeOnPath(), stdio: ["ignore", "pipe", "ignore"] });
@@ -4026,7 +4026,7 @@ function filterLocal(servers, input) {
4026
4026
  continue;
4027
4027
  }
4028
4028
  input.log(
4029
- `MCP connection "${server.key}" skipped: local MCP is off for this Ship on this machine \u2014 run \`lumi-runner config set local-mcp on --ship ${input.shipId}\` here to allow it.`
4029
+ `MCP connection "${server.key}" skipped: local MCP is off for this Ship on this machine \u2014 run \`${RUNNER_BIN} config set local-mcp on --ship ${input.shipId}\` here to allow it.`
4030
4030
  );
4031
4031
  }
4032
4032
  return kept;
@@ -4051,7 +4051,7 @@ async function probeAndReport(input) {
4051
4051
  await report(input, {
4052
4052
  ok: false,
4053
4053
  toolNames: [],
4054
- error: `This machine has not allowed this Ship to run local MCP connections. On the machine, run: lumi-runner config set local-mcp on --ship ${shipId}`
4054
+ error: `This machine has not allowed this Ship to run local MCP connections. On the machine, run: ${RUNNER_BIN} config set local-mcp on --ship ${shipId}`
4055
4055
  });
4056
4056
  return;
4057
4057
  }
@@ -4108,7 +4108,7 @@ async function probeStdio(server) {
4108
4108
  send(child, 1, "initialize", {
4109
4109
  protocolVersion: PROTOCOL_VERSION,
4110
4110
  capabilities: {},
4111
- clientInfo: { name: "lumi-runner", version: RUNNER_VERSION }
4111
+ clientInfo: { name: RUNNER_BIN, version: RUNNER_VERSION }
4112
4112
  });
4113
4113
  await replies.waitFor(1, () => stderr);
4114
4114
  child.stdin.write(
@@ -4148,7 +4148,7 @@ async function probeHttp(server) {
4148
4148
  await post(1, "initialize", {
4149
4149
  protocolVersion: PROTOCOL_VERSION,
4150
4150
  capabilities: {},
4151
- clientInfo: { name: "lumi-runner", version: RUNNER_VERSION }
4151
+ clientInfo: { name: RUNNER_BIN, version: RUNNER_VERSION }
4152
4152
  });
4153
4153
  return toolNamesFrom(await post(2, "tools/list", {}));
4154
4154
  }
@@ -5249,9 +5249,9 @@ async function installEngine(input) {
5249
5249
  return { ok: false, ran: true, detail: installed.detail };
5250
5250
  }
5251
5251
  const bin = engineBinPath(root, engineId, pkg.binName);
5252
- const health = await input.healthCheck(bin);
5253
- if (!health.ok || health.binary !== bin) {
5254
- const detail = health.ok ? `npm reported success, but the check ran ${health.binary ?? "something else"} rather than ${bin} \u2014 the install did not land where this runner looks.` : health.detail;
5252
+ const health2 = await input.healthCheck(bin);
5253
+ if (!health2.ok || health2.binary !== bin) {
5254
+ const detail = health2.ok ? `npm reported success, but the check ran ${health2.binary ?? "something else"} rather than ${bin} \u2014 the install did not land where this runner looks.` : health2.detail;
5255
5255
  recordEngineInstall(root, engineId, { outcome: "failed", at: Date.now(), detail: detail.slice(0, 400) });
5256
5256
  return { ok: false, ran: true, detail };
5257
5257
  }
@@ -5378,6 +5378,7 @@ function decideUpdate(f) {
5378
5378
  if (f.current === DEV_VERSION) return { update: false, reason: "dev-build" };
5379
5379
  if (!f.autoUpdate) return { update: false, reason: "disabled" };
5380
5380
  const service2 = f.service();
5381
+ if (service2.state === "unsupported") return { update: false, reason: "service-unsupported" };
5381
5382
  if (service2.state !== "running") return { update: false, reason: "service-not-running" };
5382
5383
  if (!service2.execPath || service2.execPath !== f.cliPath) {
5383
5384
  return { update: false, reason: "foreign-unit" };
@@ -5401,6 +5402,8 @@ function describeRefusal(reason, f) {
5401
5402
  return `Auto-update is off (\`${RUNNER_BIN} config set autoUpdate on\` to enable).`;
5402
5403
  case "service-not-running":
5403
5404
  return `Auto-update needs the background service (this daemon would have nothing to restart it). Run \`${RUNNER_BIN} service install\`, or \`${RUNNER_BIN} update\` by hand.`;
5405
+ case "service-unsupported":
5406
+ return `This machine has no service supervisor, so a daemon that exited would not come back \u2014 auto-update is off for it. Run \`${RUNNER_BIN} update\` and restart it yourself.`;
5404
5407
  case "foreign-unit":
5405
5408
  return `This process is not the one the installed service runs, so auto-update is off for it. Run \`${RUNNER_BIN} update\` if you meant to upgrade the installed copy.`;
5406
5409
  case "no-npm":
@@ -5523,7 +5526,13 @@ async function refreshEngineTools(input) {
5523
5526
  for (const engineId of input.engineIds) {
5524
5527
  if (seen.has(engineId)) continue;
5525
5528
  seen.add(engineId);
5526
- out.push(await refreshOne(engineId, input, now));
5529
+ try {
5530
+ out.push(await refreshOne(engineId, input, now));
5531
+ } catch (e) {
5532
+ const detail = e instanceof Error ? e.message : String(e);
5533
+ input.log(`Engine "${engineId}" could not be checked: ${detail}`);
5534
+ out.push({ engineId, state: "failed", detail: detail.slice(0, 300) });
5535
+ }
5527
5536
  }
5528
5537
  return out;
5529
5538
  }
@@ -5550,7 +5559,7 @@ async function refreshOne(engineId, input, now) {
5550
5559
  target: choice.version,
5551
5560
  platform: process.platform,
5552
5561
  autoInstall: input.autoInstall,
5553
- npmPresent: npmPresentOnce(),
5562
+ npmPresent: input.npmPresent ?? npmPresentOnce(),
5554
5563
  marker,
5555
5564
  locked: false,
5556
5565
  record: record ?? null,
@@ -5561,17 +5570,17 @@ async function refreshOne(engineId, input, now) {
5561
5570
  return describeRefused(engineId, input, marker, decision.reason);
5562
5571
  }
5563
5572
  input.onState?.({ engineId, state: "installing", version: choice.version });
5564
- const outcome = await installEngine({
5573
+ const outcome = await (input.install ?? installEngine)({
5565
5574
  root,
5566
5575
  engineId,
5567
5576
  version: choice.version,
5568
- healthCheck: (bin) => getDriver(engineId).healthCheck(bin),
5577
+ healthCheck: (bin) => health(input, engineId, bin),
5569
5578
  log: log2,
5570
5579
  now
5571
5580
  });
5572
5581
  if (outcome.ok) return { engineId, state: "ready", version: choice.version };
5573
5582
  log2(`Could not install "${engineId}": ${outcome.detail.slice(0, 200)}`);
5574
- const fallback = await unmanagedVersion(engineId);
5583
+ const fallback = await unmanagedVersion(input, engineId);
5575
5584
  if (fallback && !policy.blocked.includes(fallback)) {
5576
5585
  return { engineId, state: "unmanaged", version: fallback, detail: "Using a copy already on this machine \u2014 this runner cannot update or roll it back." };
5577
5586
  }
@@ -5612,11 +5621,14 @@ function npmPresentOnce() {
5612
5621
  return answer;
5613
5622
  };
5614
5623
  }
5615
- async function unmanagedVersion(engineId) {
5624
+ function health(input, engineId, bin) {
5625
+ return input.driverHealth ? input.driverHealth(engineId, bin) : getDriver(engineId).healthCheck(bin);
5626
+ }
5627
+ async function unmanagedVersion(input, engineId) {
5616
5628
  try {
5617
- const health = await getDriver(engineId).healthCheck();
5618
- if (!health.ok) return null;
5619
- const match = /(\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?)/.exec(health.detail ?? "");
5629
+ const probe2 = await health(input, engineId);
5630
+ if (!probe2.ok) return null;
5631
+ const match = /(\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?)/.exec(probe2.detail ?? "");
5620
5632
  return match?.[1] ?? null;
5621
5633
  } catch {
5622
5634
  return null;
@@ -5632,6 +5644,9 @@ function engineSet(engineIds) {
5632
5644
  if (engines.size === 0) engines.add(DEFAULT_ENGINE_ID);
5633
5645
  return engines;
5634
5646
  }
5647
+ function enginesForCredentials(credentials) {
5648
+ return [...new Set(credentials.map((c) => c.engine).filter((id) => isKnownEngine(id)))];
5649
+ }
5635
5650
 
5636
5651
  // src/service.ts
5637
5652
  import { spawnSync as spawnSync2 } from "node:child_process";
@@ -5658,6 +5673,15 @@ function run(command, args) {
5658
5673
  out: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
5659
5674
  };
5660
5675
  }
5676
+ function systemdPresent() {
5677
+ try {
5678
+ if (!fs12.existsSync("/run/systemd/system")) return false;
5679
+ const probe2 = process.platform === "win32" ? "where" : "which";
5680
+ return spawnSync2(probe2, ["systemctl"], { stdio: "ignore" }).status === 0;
5681
+ } catch {
5682
+ return false;
5683
+ }
5684
+ }
5661
5685
  function cliPath() {
5662
5686
  return fileURLToPath(import.meta.url);
5663
5687
  }
@@ -5840,6 +5864,21 @@ ${envLines}
5840
5864
  WantedBy=default.target
5841
5865
  `;
5842
5866
  }
5867
+ function linuxStatusFrom(facts) {
5868
+ if (!facts.systemdPresent) {
5869
+ return {
5870
+ state: "unsupported",
5871
+ detail: "This machine has no systemd, so there is no background service to install \u2014 it is a container, or a Linux that does not use one."
5872
+ };
5873
+ }
5874
+ if (!facts.unitExists) return { state: "not-installed", detail: "No systemd user unit installed." };
5875
+ return {
5876
+ state: facts.isActive === "active" ? "running" : "installed",
5877
+ detail: `systemd user unit is ${facts.isActive || "unknown"}.`,
5878
+ unitPath: facts.unitPath,
5879
+ ...facts.exec ?? {}
5880
+ };
5881
+ }
5843
5882
  function serviceStatus() {
5844
5883
  if (process.platform === "darwin") {
5845
5884
  const unitPath = launchAgentPath();
@@ -5859,14 +5898,16 @@ function serviceStatus() {
5859
5898
  }
5860
5899
  if (process.platform === "linux") {
5861
5900
  const unitPath = systemdUnitPath();
5862
- if (!fs12.existsSync(unitPath)) return { state: "not-installed", detail: "No systemd user unit installed." };
5863
- const active = run("systemctl", ["--user", "is-active", LINUX_UNIT]);
5864
- return {
5865
- state: active.out === "active" ? "running" : "installed",
5866
- detail: `systemd user unit is ${active.out || "unknown"}.`,
5901
+ const present = systemdPresent();
5902
+ const unitExists = fs12.existsSync(unitPath);
5903
+ const isActive = present && unitExists ? run("systemctl", ["--user", "is-active", LINUX_UNIT]).out : "";
5904
+ return linuxStatusFrom({
5905
+ systemdPresent: present,
5906
+ unitExists,
5907
+ isActive,
5867
5908
  unitPath,
5868
- ...execFacts(unitPath, parseSystemdCliPath, parseSystemdEnv)
5869
- };
5909
+ exec: unitExists ? execFacts(unitPath, parseSystemdCliPath, parseSystemdEnv) : void 0
5910
+ });
5870
5911
  }
5871
5912
  if (process.platform === "win32") {
5872
5913
  const query7 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
@@ -5880,6 +5921,11 @@ function serviceStatus() {
5880
5921
  return { state: "unsupported", detail: `No service integration for ${process.platform}.` };
5881
5922
  }
5882
5923
  function installService() {
5924
+ if (process.platform === "linux" && !systemdPresent()) {
5925
+ throw new ServiceError(
5926
+ `This machine has no systemd, so there is no background service to install. Run \`${RUNNER_BIN} start\` to keep the daemon up \u2014 in Docker, make that the container's command.`
5927
+ );
5928
+ }
5883
5929
  const notes = [];
5884
5930
  fs12.mkdirSync(logDir(), { recursive: true, mode: 448 });
5885
5931
  for (const unit of removeLegacyService()) {
@@ -6180,6 +6226,7 @@ async function startDaemon() {
6180
6226
  const shipCredentials = /* @__PURE__ */ new Map();
6181
6227
  const heldCredentialIds = /* @__PURE__ */ new Map();
6182
6228
  const loginsInFlight = /* @__PURE__ */ new Map();
6229
+ const enginesBeingEnsured = /* @__PURE__ */ new Set();
6183
6230
  const unsubsByShip = /* @__PURE__ */ new Map();
6184
6231
  const listenerError = (shipId, what) => (e) => {
6185
6232
  if (isEnrolmentGone(e)) {
@@ -6355,6 +6402,15 @@ async function startDaemon() {
6355
6402
  ensureEngine: (engineId) => ensureEngineForCaller(engineId)
6356
6403
  }).catch((e) => log2(`Sign-in for ${credential.id} ended abnormally: ${e instanceof Error ? e.message : e}`)).finally(() => loginsInFlight.delete(credential.id));
6357
6404
  }
6405
+ for (const engineId of enginesForCredentials(list)) {
6406
+ if (enginesBeingEnsured.has(engineId)) continue;
6407
+ const known = engineToolStates.get(engineId)?.state;
6408
+ if (known === "ready" || known === "installing" || known === "unmanaged") continue;
6409
+ enginesBeingEnsured.add(engineId);
6410
+ void ensureEngineForCaller(engineId).then((result) => {
6411
+ if (!result.ok) log2(`Engine "${engineId}" is named by a credential on Ship ${shipId}: ${result.detail}`);
6412
+ }).catch((e) => log2(`Could not install "${engineId}": ${e instanceof Error ? e.message : e}`)).finally(() => enginesBeingEnsured.delete(engineId));
6413
+ }
6358
6414
  poke();
6359
6415
  },
6360
6416
  // ITS OWN ERROR HANDLER, not `listenerError`, for the reason the mcp_servers listener
@@ -7319,7 +7375,10 @@ ${harder.transcript}`
7319
7375
  if (shipIds.length === 0) return;
7320
7376
  checkingEngines = true;
7321
7377
  try {
7322
- const engineIds = engineSet(agentEngines.values());
7378
+ const engineIds = engineSet([
7379
+ ...agentEngines.values(),
7380
+ ...[...shipCredentials.values()].flatMap((list) => enginesForCredentials(list))
7381
+ ]);
7323
7382
  const firstShip = shipIds[0];
7324
7383
  const policySnap = await getDoc8(
7325
7384
  doc9(sess(firstShip).fb.db, COLLECTIONS.crewConfig, CONFIG_DOCS.engines)
@@ -7707,12 +7766,12 @@ function setUpdateSetting(config2, key, value, ship2) {
7707
7766
  function setLocalMcp(config2, value, ship2) {
7708
7767
  if (!ship2) {
7709
7768
  throw new CliError(
7710
- `"${LOCAL_MCP}" is granted one Ship at a time: \`lumi-runner config set ${LOCAL_MCP} ${value} --ship <shipId>\`. There is no machine-wide form \u2014 allowing a Ship to run programs here should be a decision about that Ship. \`lumi-runner ship list\` shows the ids.`
7769
+ `"${LOCAL_MCP}" is granted one Ship at a time: \`${RUNNER_BIN} config set ${LOCAL_MCP} ${value} --ship <shipId>\`. There is no machine-wide form \u2014 allowing a Ship to run programs here should be a decision about that Ship. \`${RUNNER_BIN} ship list\` shows the ids.`
7711
7770
  );
7712
7771
  }
7713
7772
  if (!config2.ships.includes(ship2)) {
7714
7773
  throw new CliError(
7715
- `This machine does not serve Ship "${ship2}". Run \`lumi-runner ship list\` to see which Ships it serves, or \`lumi-runner ship add ${ship2}\` to add it.`
7774
+ `This machine does not serve Ship "${ship2}". Run \`${RUNNER_BIN} ship list\` to see which Ships it serves, or \`${RUNNER_BIN} ship add ${ship2}\` to add it.`
7716
7775
  );
7717
7776
  }
7718
7777
  const allowed = parseBool(value);
@@ -7732,7 +7791,7 @@ function setLocalMcp(config2, value, ship2) {
7732
7791
  " This Ship may now start MCP servers as programs on this machine, and reach servers on its loopback address. Only the connections a captain registered on that Ship, and only the ones activated on the agent running the job."
7733
7792
  );
7734
7793
  }
7735
- say.line(" Restart the daemon for this to take effect: `lumi-runner service restart`.");
7794
+ say.line(` Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
7736
7795
  return 0;
7737
7796
  }
7738
7797
  function setParallel(config2, value, ship2) {
@@ -7740,7 +7799,7 @@ function setParallel(config2, value, ship2) {
7740
7799
  if (ship2) {
7741
7800
  if (!config2.ships.includes(ship2)) {
7742
7801
  throw new CliError(
7743
- `This machine does not serve Ship "${ship2}". Run \`lumi-runner ship list\` to see which Ships it serves, or \`lumi-runner ship add ${ship2}\` to add it.`
7802
+ `This machine does not serve Ship "${ship2}". Run \`${RUNNER_BIN} ship list\` to see which Ships it serves, or \`${RUNNER_BIN} ship add ${ship2}\` to add it.`
7744
7803
  );
7745
7804
  }
7746
7805
  const overrides = { ...config2.shipParallelJobs ?? {} };
@@ -7764,10 +7823,10 @@ function setParallel(config2, value, ship2) {
7764
7823
  );
7765
7824
  if (ship2 && parsed !== null && parsed > machine) {
7766
7825
  say.line(
7767
- ` Capped at the machine-wide limit of ${machine}. Raise it with \`lumi-runner config set ${PARALLEL} ${parsed}\`.`
7826
+ ` Capped at the machine-wide limit of ${machine}. Raise it with \`${RUNNER_BIN} config set ${PARALLEL} ${parsed}\`.`
7768
7827
  );
7769
7828
  }
7770
- say.line(" Restart the daemon for this to take effect: `lumi-runner service restart`.");
7829
+ say.line(` Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
7771
7830
  return 0;
7772
7831
  }
7773
7832
 
@@ -7827,20 +7886,27 @@ function serviceCheckFrom(status) {
7827
7886
  "service",
7828
7887
  "Background service",
7829
7888
  `Installed, but the unit points at a CLI that no longer exists (${status.execPath}). It is registered and retrying forever without ever starting.`,
7830
- "Run `lumi-runner uninstall` to remove it, or `lumi-runner service install` to repoint it at this install."
7889
+ `Run \`${RUNNER_BIN} uninstall\` to remove it, or \`${RUNNER_BIN} service install\` to repoint it at this install.`
7831
7890
  );
7832
7891
  }
7833
7892
  if (status.state === "running") return ok("service", "Background service", status.detail);
7834
7893
  if (status.state === "installed") {
7835
- return warn("service", "Background service", status.detail, "Run `lumi-runner service restart`.");
7894
+ return warn("service", "Background service", status.detail, `Run \`${RUNNER_BIN} service restart\`.`);
7895
+ }
7896
+ if (status.state === "unsupported") {
7897
+ return warn(
7898
+ "service",
7899
+ "Background service",
7900
+ status.detail,
7901
+ `Run \`${RUNNER_BIN} start\` to keep this machine online \u2014 in Docker, make that the container's command.`
7902
+ );
7836
7903
  }
7837
- if (status.state === "unsupported") return warn("service", "Background service", status.detail);
7838
7904
  if (status.state === "not-installed") {
7839
7905
  return warn(
7840
7906
  "service",
7841
7907
  "Background service",
7842
7908
  "Not installed \u2014 the daemon only runs while a terminal is open.",
7843
- "Run `lumi-runner service install` to keep this Ship online whenever the machine is."
7909
+ `Run \`${RUNNER_BIN} service install\` to keep this Ship online whenever the machine is.`
7844
7910
  );
7845
7911
  }
7846
7912
  return warn("service", "Background service", `Unrecognised service state: ${status.state}.`);
@@ -7862,7 +7928,7 @@ function serviceBinaryCheckFrom(input) {
7862
7928
  id,
7863
7929
  label,
7864
7930
  `The daemon's PATH is missing ${missing.join(", ")} \u2014 it was captured when the service was installed, and jobs will fail with \`spawn ${missing[0]} ENOENT\` even though this terminal finds it.`,
7865
- "Run `lumi-runner service install` from a shell where these work, to rewrite the unit with the current PATH."
7931
+ `Run \`${RUNNER_BIN} service install\` from a shell where these work, to rewrite the unit with the current PATH.`
7866
7932
  );
7867
7933
  }
7868
7934
  function resolvesOnPath(binary, pathValue) {
@@ -7903,7 +7969,7 @@ function localMcpCheckFrom(shipId, servers, allowed) {
7903
7969
  id,
7904
7970
  label,
7905
7971
  `${local.length} connection(s) run on this machine and are not allowed here: ${names}.`,
7906
- `lumi-runner config set local-mcp on --ship ${shipId}`
7972
+ `${RUNNER_BIN} config set local-mcp on --ship ${shipId}`
7907
7973
  );
7908
7974
  }
7909
7975
  async function checkVersion(config2) {
@@ -7922,7 +7988,7 @@ async function checkShips(config2) {
7922
7988
  const manyIdentities = configRunnerIdentities(config2).length > 1;
7923
7989
  if (config2.ships.length === 0) {
7924
7990
  checks.push(
7925
- fail("ships", "Ships", "This machine serves no Ships.", "Run `lumi-runner ship add` to pick one.")
7991
+ fail("ships", "Ships", "This machine serves no Ships.", `Run \`${RUNNER_BIN} ship add\` to pick one.`)
7926
7992
  );
7927
7993
  return { checks, engines, needsGithub };
7928
7994
  }
@@ -7937,7 +8003,7 @@ async function checkShips(config2) {
7937
8003
  `key:${shipId}`,
7938
8004
  `Ship ${shipId} \u2014 key`,
7939
8005
  e instanceof Error ? e.message : String(e),
7940
- "Ask a captain to approve this machine on that Ship's Daemons page, then re-run `lumi-runner login`."
8006
+ `Ask a captain to approve this machine on that Ship's Daemons page, then re-run \`${RUNNER_BIN} login\`.`
7941
8007
  )
7942
8008
  );
7943
8009
  continue;
@@ -7952,7 +8018,7 @@ async function checkShips(config2) {
7952
8018
  `approval:${shipId}`,
7953
8019
  `Ship ${shipId} \u2014 approval`,
7954
8020
  "This machine has no record on that Ship.",
7955
- "Re-run `lumi-runner login` \u2014 approving is what enrols it."
8021
+ `Re-run \`${RUNNER_BIN} login\` \u2014 approving is what enrols it.`
7956
8022
  )
7957
8023
  );
7958
8024
  } else if (snap.data().approved !== true) {
@@ -7989,11 +8055,11 @@ async function checkShips(config2) {
7989
8055
  agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
7990
8056
  } catch {
7991
8057
  }
7992
- const shipEngines = enginesForAgents(agents);
8058
+ const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
8059
+ const shipEngines = engineSet([...enginesForAgents(agents), ...enginesForCredentials(credentials)]);
7993
8060
  for (const id of shipEngines) engines.add(id);
7994
8061
  if (agents.some((agent) => effectiveAgentTools(agent).github.enabled)) needsGithub = true;
7995
8062
  try {
7996
- const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
7997
8063
  const missing = [
7998
8064
  ...new Set(
7999
8065
  [...shipEngines].flatMap(
@@ -8032,8 +8098,8 @@ async function checkShips(config2) {
8032
8098
  );
8033
8099
  }
8034
8100
  try {
8035
- const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
8036
- for (const cred of credentials) {
8101
+ const credentials2 = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
8102
+ for (const cred of credentials2) {
8037
8103
  if (!isMachineHeldCredential(cred) || cred.runnerId !== session.runnerId) continue;
8038
8104
  if (!isMachineLoginEngine(cred.engine)) continue;
8039
8105
  const driver = getDriver(cred.engine);
@@ -8098,7 +8164,7 @@ async function runDoctor() {
8098
8164
  const config2 = loadConfig();
8099
8165
  if (!config2) {
8100
8166
  checks.push(
8101
- fail("config", "Configuration", "This machine is not connected.", "Run `lumi-runner setup`.")
8167
+ fail("config", "Configuration", "This machine is not connected.", `Run \`${RUNNER_BIN} setup\`.`)
8102
8168
  );
8103
8169
  return report2(checks);
8104
8170
  }
@@ -8122,19 +8188,26 @@ async function runDoctor() {
8122
8188
  const needsGithub = shipResults.needsGithub;
8123
8189
  const jobBinaries = [];
8124
8190
  for (const engineId of engines) {
8125
- const health = await getDriver(engineId).healthCheck();
8126
- if (health.binary) jobBinaries.push(health.binary);
8191
+ const health2 = await getDriver(engineId).healthCheck();
8192
+ if (health2.binary) jobBinaries.push(health2.binary);
8127
8193
  const managed = managedEngineBin(engineId, {});
8128
8194
  const version = managed ? readEngineMarker(configDir(), engineId)?.version : null;
8129
- const origin = managed ? ` \u2014 installed by this runner${version ? ` (${version})` : ""}` : health.binary && health.binary !== engineId ? " \u2014 from CREW_*_BIN" : " \u2014 already on this machine";
8195
+ const origin = managed ? ` \u2014 installed by this runner${version ? ` (${version})` : ""}` : health2.binary && health2.binary !== engineId ? " \u2014 from CREW_*_BIN" : " \u2014 already on this machine";
8130
8196
  checks.push(
8131
- health.ok ? ok(`engine:${engineId}`, `Engine "${engineId}"`, `${health.detail}${origin}`) : fail(
8197
+ health2.ok ? ok(`engine:${engineId}`, `Engine "${engineId}"`, `${health2.detail}${origin}`) : fail(
8132
8198
  `engine:${engineId}`,
8133
8199
  `Engine "${engineId}"`,
8134
- health.detail,
8200
+ health2.detail,
8135
8201
  // A way forward the operator can type, ahead of the vendor's download page: this
8136
8202
  // runner installs it itself, and the old message never said so.
8137
- enginePackageFor(engineId) ? `Run \`${RUNNER_BIN} engine install ${engineId}\` (a few hundred MB), or ${health.fix ?? "install the CLI yourself"}` : health.fix
8203
+ //
8204
+ // THE AUTOMATIC PATH GOES FIRST since §15.85, because on a fresh machine it is the one
8205
+ // that will actually happen and the old wording hid it. `doctor` runs BEFORE the daemon
8206
+ // exists during `setup`, so this row is guaranteed to fail there — and an operator who
8207
+ // read "N check(s) failed" walked away believing the runner does not install engines,
8208
+ // minutes before it silently did. Naming the daemon first is the difference between a
8209
+ // failure and a step that has not happened yet.
8210
+ enginePackageFor(engineId) ? `This machine installs it itself once its daemon is running (\`${RUNNER_BIN} start\`). To do it now: \`${RUNNER_BIN} engine install ${engineId}\` (a few hundred MB), or ${health2.fix ?? "install the CLI yourself"}` : health2.fix
8138
8211
  )
8139
8212
  );
8140
8213
  if (managed) jobBinaries.push("node");
@@ -8209,7 +8282,7 @@ async function runEngineList() {
8209
8282
  for (const engineId of installableEngineIds()) {
8210
8283
  const pkg = enginePackageFor(engineId);
8211
8284
  const { origin, bin, version } = originOf(engineId);
8212
- const health = await getDriver(engineId).healthCheck();
8285
+ const health2 = await getDriver(engineId).healthCheck();
8213
8286
  const record = state[engineId];
8214
8287
  rows.push({
8215
8288
  engine: engineId,
@@ -8217,10 +8290,10 @@ async function runEngineList() {
8217
8290
  package: pkg?.npmPackage ?? null,
8218
8291
  shipped: DEFAULT_ENGINE_POLICIES[engineId]?.knownGood ?? null,
8219
8292
  installed: version,
8220
- origin: origin === "none" && health.ok ? "path" : origin,
8221
- binary: bin ?? health.binary ?? null,
8222
- ok: health.ok,
8223
- detail: health.ok ? void 0 : health.detail,
8293
+ origin: origin === "none" && health2.ok ? "path" : origin,
8294
+ binary: bin ?? health2.binary ?? null,
8295
+ ok: health2.ok,
8296
+ detail: health2.ok ? void 0 : health2.detail,
8224
8297
  lastAttempt: record ? { target: record.target, outcome: record.outcome, attempts: record.attempts, detail: record.detail } : null,
8225
8298
  diskBytes: dirSize(enginePrefixDir(root, engineId))
8226
8299
  });
@@ -8428,7 +8501,7 @@ Code: ${pc.bold(start.displayCode)}`,
8428
8501
  }
8429
8502
  if (!approved) {
8430
8503
  progress.stop("");
8431
- throw new CliError("The approval window expired. Run `lumi-runner login` again.");
8504
+ throw new CliError(`The approval window expired. Run \`${RUNNER_BIN} login\` again.`);
8432
8505
  }
8433
8506
  progress.stop("Approved.");
8434
8507
  const { config: config2, approvedShips, pendingShips, occupiedShips, newIdentity, otherShips } = buildLoginResult(
@@ -8438,7 +8511,7 @@ Code: ${pc.bold(start.displayCode)}`,
8438
8511
  options.mcpUrl
8439
8512
  );
8440
8513
  if (!config2.runnerId) {
8441
- throw new CliError("The approval did not name this machine. Run `lumi-runner login` again.");
8514
+ throw new CliError(`The approval did not name this machine. Run \`${RUNNER_BIN} login\` again.`);
8442
8515
  }
8443
8516
  if (!config2.apiKey) {
8444
8517
  throw new CliError(
@@ -8541,11 +8614,11 @@ function report3(result) {
8541
8614
  }
8542
8615
  if (result.otherShips && result.otherShips.length > 0) {
8543
8616
  say.info(
8544
- `Still serving ${result.otherShips.join(", ")} \u2014 this approval did not mention them. \`lumi-runner ship remove <shipId>\` to stop.`
8617
+ `Still serving ${result.otherShips.join(", ")} \u2014 this approval did not mention them. \`${RUNNER_BIN} ship remove <shipId>\` to stop.`
8545
8618
  );
8546
8619
  }
8547
8620
  if (serviceStatus().state === "running") {
8548
- say.info("Restart the daemon for this to take effect: `lumi-runner service restart`.");
8621
+ say.info(`Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
8549
8622
  }
8550
8623
  return 0;
8551
8624
  }
@@ -8574,7 +8647,16 @@ async function runLogs(options) {
8574
8647
  // src/cli/commands/service.ts
8575
8648
  async function runServiceInstall() {
8576
8649
  const before = serviceStatus();
8577
- if (before.state !== "not-installed" && before.state !== "unsupported") {
8650
+ if (before.state === "unsupported") {
8651
+ if (isJson()) {
8652
+ emitJson({ installed: false, supported: false, ...before });
8653
+ return 1;
8654
+ }
8655
+ say.warn(before.detail);
8656
+ say.info(`Run \`${RUNNER_BIN} start\` to keep the daemon up \u2014 in Docker, make that the container's command.`);
8657
+ return 1;
8658
+ }
8659
+ if (before.state !== "not-installed") {
8578
8660
  const replace = await promptConfirm({
8579
8661
  message: "A service is already installed. Reinstall it with the current settings?",
8580
8662
  initialValue: true
@@ -8632,7 +8714,7 @@ async function runServiceStatus() {
8632
8714
  say.line(` ${status.detail}`);
8633
8715
  if (status.unitPath) say.line(` ${status.unitPath}`);
8634
8716
  if (status.execMissing) {
8635
- say.warn(`The unit runs ${status.execPath}, which no longer exists. Run \`lumi-runner uninstall\`.`);
8717
+ say.warn(`The unit runs ${status.execPath}, which no longer exists. Run \`${RUNNER_BIN} uninstall\`.`);
8636
8718
  }
8637
8719
  return 0;
8638
8720
  }
@@ -8698,14 +8780,14 @@ async function pickShips(current) {
8698
8780
  const ships = await listMyShips();
8699
8781
  if (ships.length === 0) {
8700
8782
  throw new CliError(
8701
- "This machine holds no Ship keys. Run `lumi-runner login` and have a captain approve it \u2014 a daemon can only serve Ships it was approved for."
8783
+ `This machine holds no Ship keys. Run \`${RUNNER_BIN} login\` and have a captain approve it \u2014 a daemon can only serve Ships it was approved for.`
8702
8784
  );
8703
8785
  }
8704
8786
  return promptMultiSelect({
8705
8787
  message: "Which Ships should this machine serve?",
8706
8788
  choices: ships.map((ship2) => ({ value: ship2.id, label: ship2.name || ship2.id, hint: ship2.id })),
8707
8789
  initialValues: current.filter((id) => ships.some((s) => s.id === id)),
8708
- flagHint: "lumi-runner ship add <shipId>"
8790
+ flagHint: `${RUNNER_BIN} ship add <shipId>`
8709
8791
  });
8710
8792
  }
8711
8793
  async function runShipList() {
@@ -8715,7 +8797,7 @@ async function runShipList() {
8715
8797
  return 0;
8716
8798
  }
8717
8799
  if (config2.ships.length === 0) {
8718
- say.warn("This machine serves no Ships. Run `lumi-runner ship add`.");
8800
+ say.warn(`This machine serves no Ships. Run \`${RUNNER_BIN} ship add\`.`);
8719
8801
  return 0;
8720
8802
  }
8721
8803
  for (const shipId of config2.ships) say.line(` ${shipId}`);
@@ -8746,18 +8828,36 @@ function reportAssignment(ships) {
8746
8828
  return 0;
8747
8829
  }
8748
8830
  say.success(ships.length > 0 ? `Serving: ${ships.join(", ")}` : "Serving no Ships.");
8749
- say.info("Restart the daemon for this to take effect: `lumi-runner service restart`.");
8831
+ say.info(`Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
8750
8832
  return 0;
8751
8833
  }
8752
8834
 
8753
8835
  // src/cli/commands/setup.ts
8836
+ function shipChoiceFrom(input) {
8837
+ const label = (id) => input.fetched?.find((s) => s.id === id)?.name || id;
8838
+ const serving = input.serving.map(label).join(", ");
8839
+ const question = `Serving ${serving}. Change which Ships this machine serves? (yes replaces the list)`;
8840
+ if (!input.fetched || input.fetched.length < input.keyCount) return { ask: true, line: question };
8841
+ const servingIds = new Set(input.serving);
8842
+ const others = input.fetched.filter((s) => !servingIds.has(s.id));
8843
+ const fetchedIds = new Set(input.fetched.map((s) => s.id));
8844
+ const unkeyed = input.serving.filter((id) => !fetchedIds.has(id));
8845
+ if (others.length === 0 && unkeyed.length === 0) {
8846
+ return { ask: false, line: `Serving ${serving}. This machine holds no other Ship keys.` };
8847
+ }
8848
+ if (others.length === 0) return { ask: true, line: question };
8849
+ return {
8850
+ ask: true,
8851
+ line: `Serving ${serving}. This machine also holds a key for ${others.map((s) => s.name || s.id).join(", ")}. Change which Ships it serves? (yes replaces the list)`
8852
+ };
8853
+ }
8754
8854
  async function runSetup(options) {
8755
8855
  if (!canPrompt()) {
8756
8856
  throw new CliError(
8757
- "setup is interactive. On a headless machine run `lumi-runner login`, `lumi-runner ship add <id>` and `lumi-runner service install` instead."
8857
+ `setup is interactive. On a headless machine run \`${RUNNER_BIN} login\`, \`${RUNNER_BIN} ship add <id>\` and \`${RUNNER_BIN} service install\` instead.`
8758
8858
  );
8759
8859
  }
8760
- say.intro("lumi-runner setup");
8860
+ say.intro(`${RUNNER_BIN} setup`);
8761
8861
  const moved = migrateLegacyDir();
8762
8862
  if (moved?.migrated) say.info(`Moved your runner config from ${moved.from} to ${moved.to}.`);
8763
8863
  const existing = loadConfig();
@@ -8782,7 +8882,7 @@ async function runSetup(options) {
8782
8882
  }
8783
8883
  ],
8784
8884
  initialValue: "keep",
8785
- flagHint: "lumi-runner login"
8885
+ flagHint: `${RUNNER_BIN} login`
8786
8886
  });
8787
8887
  if (action2 === "add") {
8788
8888
  await runLogin(options);
@@ -8805,14 +8905,24 @@ async function runSetup(options) {
8805
8905
  saveConfig(config2);
8806
8906
  say.success(`Serving: ${config2.ships.join(", ") || "(none)"}`);
8807
8907
  } else {
8808
- const change = await promptConfirm({
8809
- message: `Serving ${config2.ships.join(", ")}. Change that?`,
8810
- initialValue: false,
8811
- yesFlagApplies: false
8908
+ const fetched = await listMyShips().catch(() => null);
8909
+ const choice = shipChoiceFrom({
8910
+ serving: config2.ships,
8911
+ fetched,
8912
+ keyCount: Object.keys(config2.shipKeys ?? {}).length
8812
8913
  });
8813
- if (change) {
8814
- config2.ships = await pickShips(config2.ships);
8815
- saveConfig(config2);
8914
+ if (!choice.ask) {
8915
+ say.info(choice.line);
8916
+ } else {
8917
+ const change = await promptConfirm({
8918
+ message: choice.line,
8919
+ initialValue: false,
8920
+ yesFlagApplies: false
8921
+ });
8922
+ if (change) {
8923
+ config2.ships = await pickShips(config2.ships);
8924
+ saveConfig(config2);
8925
+ }
8816
8926
  }
8817
8927
  }
8818
8928
  say.step("Checking this machine\u2026");
@@ -8826,7 +8936,7 @@ async function runSetup(options) {
8826
8936
  yesFlagApplies: false
8827
8937
  });
8828
8938
  if (install) await runServiceInstall();
8829
- else say.info("Skipped. Run `lumi-runner start` manually, or `lumi-runner service install` later.");
8939
+ else say.info(`Skipped. Run \`${RUNNER_BIN} start\` manually, or \`${RUNNER_BIN} service install\` later.`);
8830
8940
  } else if (service2 === "running" || service2 === "installed") {
8831
8941
  const drift = serviceEnvDrift(serviceEnv(), status.unitEnv);
8832
8942
  if (drift.length > 0) {
@@ -8836,9 +8946,14 @@ async function runSetup(options) {
8836
8946
  say.step("Restarting the daemon so it picks up this configuration\u2026");
8837
8947
  await runServiceRestart();
8838
8948
  }
8949
+ } else if (service2 === "unsupported") {
8950
+ say.info(status.detail);
8951
+ say.info(
8952
+ `Run \`${RUNNER_BIN} start\` to keep this machine online \u2014 in Docker, make that the container's command so the daemon is the thing the container runs.`
8953
+ );
8839
8954
  }
8840
8955
  say.outro(
8841
- doctorExit === 0 ? "Ready. This machine will claim jobs for its Ships." : "Setup finished, but some checks failed \u2014 fix those and re-run `lumi-runner doctor`."
8956
+ doctorExit === 0 ? "Ready. This machine will claim jobs for its Ships." : `Setup finished, but some checks failed \u2014 fix those and re-run \`${RUNNER_BIN} doctor\`.`
8842
8957
  );
8843
8958
  return doctorExit;
8844
8959
  }
@@ -8860,7 +8975,7 @@ async function runStatus() {
8860
8975
  emitJson({ version: RUNNER_VERSION, connected: false });
8861
8976
  return 0;
8862
8977
  }
8863
- say.warn("This machine is not connected. Run `lumi-runner setup`.");
8978
+ say.warn(`This machine is not connected. Run \`${RUNNER_BIN} setup\`.`);
8864
8979
  return 0;
8865
8980
  }
8866
8981
  const progress = spinner2();
@@ -8952,7 +9067,7 @@ async function runStatus() {
8952
9067
  say.line(` ${pc.bold("Service")} ${service2.detail}`);
8953
9068
  say.line("");
8954
9069
  if (ships.length === 0) {
8955
- say.warn("This machine serves no Ships. Run `lumi-runner ship add`.");
9070
+ say.warn(`This machine serves no Ships. Run \`${RUNNER_BIN} ship add\`.`);
8956
9071
  return 0;
8957
9072
  }
8958
9073
  for (const ship2 of ships) {
@@ -9074,7 +9189,7 @@ async function runUpdate(options = {}) {
9074
9189
 
9075
9190
  // src/cli/index.ts
9076
9191
  var program = new Command();
9077
- program.name("lumi-runner").description("Lumi Crew runner daemon \u2014 runs agent jobs for your Ships on this machine.").version(RUNNER_VERSION).option("--json", "machine-readable output on stdout").option("-y, --yes", "assume yes for confirmations").option("--no-color", "disable coloured output").hook("preAction", (command) => {
9192
+ program.name(RUNNER_BIN).description("Lumi Crew runner daemon \u2014 runs agent jobs for your Ships on this machine.").version(RUNNER_VERSION).option("--json", "machine-readable output on stdout").option("-y, --yes", "assume yes for confirmations").option("--no-color", "disable coloured output").hook("preAction", (command) => {
9078
9193
  const options = command.opts();
9079
9194
  configureOutput(options);
9080
9195
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kilogent/runner",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless agent sessions (Claude Code today) on your own machine.",
6
6
  "//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",