@kilogent/runner 0.1.4 → 0.1.6

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 +301 -103
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -200,6 +200,22 @@ function effectiveAgentTools(agent) {
200
200
  };
201
201
  }
202
202
 
203
+ // ../shared/dist/agentProposal.js
204
+ var DRAFT_FIELDS = {
205
+ name: true,
206
+ emoji: true,
207
+ persona: true,
208
+ engine: true,
209
+ model: true,
210
+ credentialId: true,
211
+ contract: true,
212
+ tools: true,
213
+ autonomy: true,
214
+ riskyActions: true,
215
+ playbooks: true
216
+ };
217
+ var PROPOSAL_DRAFT_FIELDS = Object.keys(DRAFT_FIELDS);
218
+
203
219
  // ../shared/dist/assistant.js
204
220
  var ASSISTANT_ACTOR_ID = "assistant";
205
221
  var ASSISTANT_NAME = "Workspace assistant";
@@ -272,7 +288,18 @@ var CONFIG_DOCS = {
272
288
  * installs, and keeping them out of a package the app also reads keeps a surface a captain can
273
289
  * edit from ever naming a version.
274
290
  */
275
- engines: "engines"
291
+ engines: "engines",
292
+ /**
293
+ * `crewConfig/launch` — whether the product is open, and what the front gate says while it is
294
+ * not. Defaults live in `@lumi/crew-shared/launch.ts`.
295
+ *
296
+ * THE ONLY `crewConfig` DOCUMENT A SIGNED-OUT BROWSER MAY READ, and the rule is written for
297
+ * this id alone (packages/crew/firestore.rules). The login page has to say the right thing
298
+ * BEFORE the Google button is pressed and there is no session at that point — so `assistant`,
299
+ * which holds the shipped prompt, and `engines`, which holds the fleet's versions, stay closed
300
+ * beside it rather than a collection-wide read being opened for one boolean.
301
+ */
302
+ launch: "launch"
276
303
  };
277
304
  var DEFAULT_ASSISTANT_MODEL = "gemini-3.1-flash-lite";
278
305
  var DEFAULT_DAILY_CAP_USD = 0.1;
@@ -341,24 +368,46 @@ var DEFAULT_FREE_CONTRACT = `# Finish in one reply
341
368
  You get a small budget per message and only a person's message starts you. Your own reply does not
342
369
  wake you again, so finish your thought rather than promising to continue.
343
370
 
344
- # When somebody describes their business
345
- This is the most useful conversation you will have with a new workspace. Give them a real answer,
346
- not a questionnaire.
371
+ # When somebody describes their business, or asks who to hire
372
+ "Who should I hire", "what crew should I start with", "who would help here" and anyone describing
373
+ what their company does are all the SAME REQUEST, and it is the most useful conversation you will
374
+ have with a new workspace.
375
+
376
+ THEY ARE ASKING ABOUT AI AGENTS ON THIS WORKSPACE, NOT ABOUT PEOPLE. Never answer with advice on
377
+ writing a job advert, hiring a contractor or finding a person, and never suggest putting a role on
378
+ the board as a task. An agent here is a thing they can switch on this afternoon.
347
379
 
348
- Propose a crew: three to five agents, each with a name, one line on what it would own, and how work
380
+ Sketch the crew: three to five agents, each with a name, one line on what it would own, and how work
349
381
  passes between them. Use their words for their work, not ours. Be concrete enough that they can
350
382
  disagree with it.
351
383
 
352
- DO NOT CREATE THE AGENTS. Designing one properly is careful work and you are the light-weight half
353
- of this product; say what you would build and let the workspace's own crew-builder do it once it is
354
- set up. Never claim you have created something you have not.
384
+ YOU CANNOT CREATE AN AGENT HERE, AND YOU CANNOT OFFER TO. There is no tool on this path that makes
385
+ one, so both of these are false and the second is worse:
386
+
387
+ reporting \u2014 "I have created", "I have added", "I have set up", "these are now on your workspace"
388
+ offering \u2014 "I can set them up now", "shall I create them", "say the word and I will", "which one
389
+ would you like me to start with"
390
+
391
+ The first sends somebody to the Agents page to look for crew that is not there. The second gets a
392
+ "yes please" you cannot act on, which is the same lie with a delay on it.
355
393
 
356
- Write the proposal into the library with knowledge_write so it survives \u2014 this conversation scrolls
357
- out of view, and whoever builds the crew later needs to read it back. Say that you saved it, and
358
- name the slug.
394
+ NEVER SAY A SEAT, A PLAN OR A BILL HAS CHANGED OR WILL CHANGE. "This adds three paid seats" is a
395
+ statement about something happening; nothing in this conversation touches billing at all. If seats
396
+ are worth mentioning, they are what a crew WOULD cost once somebody adds it.
359
397
 
360
- Then say plainly what is still missing before any of it can run, and stop. One clear sentence beats
361
- a sales pitch, and the person is already looking at the buttons.
398
+ Speak conditionally throughout \u2014 "I would start with", "this one would own", "you could add".
399
+
400
+ THEN END BY SAYING WHO DOES BUILD IT, and end there. Once this workspace has a machine online the
401
+ assistant runs on it instead \u2014 on their own AI credential, with time to read the repositories and
402
+ the board first \u2014 and puts each agent in front of them as a card they add with one press or open and
403
+ change. That is where a crew actually gets built, and it is why the sketch above is a sketch:
404
+ designing a persona, a contract with real boundaries, a tool whitelist and an autonomy level is
405
+ careful work, and you are the light-weight half of this product.
406
+
407
+ THE CARDS APPEAR IN THIS CONVERSATION, NOT ON ANOTHER PAGE. Never send somebody to the Agents page
408
+ to find them \u2014 there is nothing waiting there, and a person who goes looking finds an empty roster
409
+ and no way to tell what went wrong. The setup facts you were given above say which step is actually
410
+ outstanding; name that one, and stop there. Do not ask which agent to start with.
362
411
 
363
412
  # What you can promise
364
413
  Nothing runs on this workspace until it has a subscription and a machine. Never imply otherwise,
@@ -372,11 +421,29 @@ Nothing you do in this session is visible to anybody until you call chat_send. T
372
421
  person hears from you. End the session with run_report so the next run knows what happened.
373
422
 
374
423
  # Building the crew
375
- You can create agents, and on this path you should. If the library holds a crew proposal, read it
376
- back and build what it describes rather than asking for the description again.
424
+ THIS IS THE PATH WHERE CREW GETS DESIGNED, and you are the half of the product that can do it. Read
425
+ what the workspace actually holds first \u2014 its repositories, its board, its knowledge library, what
426
+ it has been asking for \u2014 and let that decide the crew rather than the paragraph you were given.
427
+
428
+ When somebody asks who to hire, or describes what their company does: call chat_send with your
429
+ answer, then call propose_agents with the messageId it returns. The cards land under that reply and
430
+ a captain adds each one with a press, or opens it and changes anything first.
431
+
432
+ A PERSONA AND A CONTRACT ARE NOT OPTIONAL, and the tool refuses a proposal without them. They are
433
+ what make the thing an agent rather than a job title: the persona is the line the roster shows,
434
+ written in its own first-person voice, and the contract is the markdown it reads before every run \u2014
435
+ Mission, Responsibilities, Boundaries. There is no workspace default for either, so leaving one out
436
+ is a hole rather than an inherited setting.
377
437
 
378
- Say what each agent is for in one line, and say that every agent adds a paid seat to this
379
- workspace. Never create one nobody asked for.`;
438
+ EVERYTHING ELSE: SET ONLY WHAT YOU HAVE A REASON FOR. Tools, autonomy, model, credential and
439
+ playbooks all have a workspace default, so a field you leave out is shown as "not proposed" and
440
+ takes it \u2014 which is honest, while a whitelist or a model id you guessed at is a wrong answer wearing
441
+ a confident face. What you DID go and read is worth setting, and that reading is the whole reason a
442
+ proposal from here beats one from the included model.
443
+
444
+ If the conversation already holds cards nobody answered, do not propose the same crew again.
445
+
446
+ Say that each agent adds a paid seat, and never create one nobody asked for.`;
380
447
  function text(value, cap = MAX_ASSISTANT_CONTRACT_CHARS) {
381
448
  if (typeof value !== "string")
382
449
  return void 0;
@@ -558,6 +625,22 @@ var COLLECTIONS = {
558
625
  * `messages`, and the path is only ever composed from `chats` above it.
559
626
  */
560
627
  chatMessages: "messages",
628
+ /**
629
+ * `ships/{shipId}/chats/{chatId}/proposals/{proposalId}` — crew the assistant has proposed in
630
+ * this conversation, and what the captain decided about each (PRD §15.86).
631
+ *
632
+ * UNDER THE CHAT, not under the Ship, and that placement carries three things for free. Its
633
+ * visibility is the chat's: `chatVisible()` already answers "may this person read this thread",
634
+ * and a proposal is a paragraph of that thread. Its lifetime is the chat's: `deleteChatDeep`
635
+ * drains it with the messages, so a deleted conversation leaves no crew nobody remembers being
636
+ * offered. And the app's listener is chat-scoped, which is the only scope any reader wants.
637
+ *
638
+ * THE DECISION LIVES HERE RATHER THAN ON THE MESSAGE because `messages` is `allow update: if
639
+ * false` and must stay so — the whole thread's integrity rests on it. A proposal is the opposite
640
+ * kind of thing: it is written once by the assistant and then answered, days later, by a person.
641
+ * Those are two documents, not one.
642
+ */
643
+ chatProposals: "proposals",
561
644
  /** `ships/{shipId}/agents/{agentId}` — the crew: one doc = one configured agent. */
562
645
  agents: "agents",
563
646
  /** `ships/{shipId}/agents/{agentId}/workflows/{workflowId}` — that agent's playbooks. */
@@ -1277,14 +1360,14 @@ import os from "node:os";
1277
1360
  import path from "node:path";
1278
1361
 
1279
1362
  // src/version.ts
1280
- var RUNNER_VERSION = true ? "0.1.4" : "0.0.0-dev";
1363
+ var RUNNER_VERSION = true ? "0.1.6" : "0.0.0-dev";
1281
1364
  var RUNNER_PACKAGE = true ? "@kilogent/runner" : "@kilogent/runner-dev";
1282
1365
  var RUNNER_BIN = true ? "kilogent-runner" : "kilogent-runner-dev";
1283
1366
 
1284
1367
  // src/config.ts
1285
1368
  var IS_DEV_BUILD = RUNNER_PACKAGE.endsWith("-dev");
1286
1369
  var DEFAULT_PROJECT_ID = IS_DEV_BUILD ? "lumi-afb7d" : "kilogent-crew-prod";
1287
- var DEFAULT_MCP_URL = IS_DEV_BUILD ? "https://dev.app.kilogent.com/mcp" : "https://app.kilogent.com/mcp";
1370
+ var DEFAULT_MCP_URL = IS_DEV_BUILD ? "https://dev-app-kilogent.web.app/mcp" : "https://app.kilogent.com/mcp";
1288
1371
  var LEGACY_DIR_NAME = ".crew-runner";
1289
1372
  var DIR_NAME = `.${RUNNER_BIN}`;
1290
1373
  var LUMI_DIR_NAME = ".lumi-runner";
@@ -3273,10 +3356,10 @@ async function claudeHealthCheck(binOverride) {
3273
3356
  const bin = binOverride ?? claudeBinary();
3274
3357
  return new Promise((resolve) => {
3275
3358
  let settled = false;
3276
- const done = (health) => {
3359
+ const done = (health2) => {
3277
3360
  if (settled) return;
3278
3361
  settled = true;
3279
- resolve({ ...health, binary: bin });
3362
+ resolve({ ...health2, binary: bin });
3280
3363
  };
3281
3364
  let stdout = "";
3282
3365
  const child = spawn3(bin, ["--version"], { env: withNodeOnPath(), stdio: ["ignore", "pipe", "ignore"] });
@@ -3806,10 +3889,10 @@ async function codexHealthCheck(binOverride) {
3806
3889
  const bin = binOverride ?? codexBinary();
3807
3890
  return new Promise((resolve) => {
3808
3891
  let settled = false;
3809
- const done = (health) => {
3892
+ const done = (health2) => {
3810
3893
  if (settled) return;
3811
3894
  settled = true;
3812
- resolve({ ...health, binary: bin });
3895
+ resolve({ ...health2, binary: bin });
3813
3896
  };
3814
3897
  let stdout = "";
3815
3898
  const child = spawn4(bin, ["--version"], { env: withNodeOnPath(), stdio: ["ignore", "pipe", "ignore"] });
@@ -4026,7 +4109,7 @@ function filterLocal(servers, input) {
4026
4109
  continue;
4027
4110
  }
4028
4111
  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.`
4112
+ `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
4113
  );
4031
4114
  }
4032
4115
  return kept;
@@ -4051,7 +4134,7 @@ async function probeAndReport(input) {
4051
4134
  await report(input, {
4052
4135
  ok: false,
4053
4136
  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}`
4137
+ 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
4138
  });
4056
4139
  return;
4057
4140
  }
@@ -4108,7 +4191,7 @@ async function probeStdio(server) {
4108
4191
  send(child, 1, "initialize", {
4109
4192
  protocolVersion: PROTOCOL_VERSION,
4110
4193
  capabilities: {},
4111
- clientInfo: { name: "lumi-runner", version: RUNNER_VERSION }
4194
+ clientInfo: { name: RUNNER_BIN, version: RUNNER_VERSION }
4112
4195
  });
4113
4196
  await replies.waitFor(1, () => stderr);
4114
4197
  child.stdin.write(
@@ -4148,7 +4231,7 @@ async function probeHttp(server) {
4148
4231
  await post(1, "initialize", {
4149
4232
  protocolVersion: PROTOCOL_VERSION,
4150
4233
  capabilities: {},
4151
- clientInfo: { name: "lumi-runner", version: RUNNER_VERSION }
4234
+ clientInfo: { name: RUNNER_BIN, version: RUNNER_VERSION }
4152
4235
  });
4153
4236
  return toolNamesFrom(await post(2, "tools/list", {}));
4154
4237
  }
@@ -5249,9 +5332,9 @@ async function installEngine(input) {
5249
5332
  return { ok: false, ran: true, detail: installed.detail };
5250
5333
  }
5251
5334
  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;
5335
+ const health2 = await input.healthCheck(bin);
5336
+ if (!health2.ok || health2.binary !== bin) {
5337
+ 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
5338
  recordEngineInstall(root, engineId, { outcome: "failed", at: Date.now(), detail: detail.slice(0, 400) });
5256
5339
  return { ok: false, ran: true, detail };
5257
5340
  }
@@ -5378,6 +5461,7 @@ function decideUpdate(f) {
5378
5461
  if (f.current === DEV_VERSION) return { update: false, reason: "dev-build" };
5379
5462
  if (!f.autoUpdate) return { update: false, reason: "disabled" };
5380
5463
  const service2 = f.service();
5464
+ if (service2.state === "unsupported") return { update: false, reason: "service-unsupported" };
5381
5465
  if (service2.state !== "running") return { update: false, reason: "service-not-running" };
5382
5466
  if (!service2.execPath || service2.execPath !== f.cliPath) {
5383
5467
  return { update: false, reason: "foreign-unit" };
@@ -5401,6 +5485,8 @@ function describeRefusal(reason, f) {
5401
5485
  return `Auto-update is off (\`${RUNNER_BIN} config set autoUpdate on\` to enable).`;
5402
5486
  case "service-not-running":
5403
5487
  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.`;
5488
+ case "service-unsupported":
5489
+ 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
5490
  case "foreign-unit":
5405
5491
  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
5492
  case "no-npm":
@@ -5523,7 +5609,13 @@ async function refreshEngineTools(input) {
5523
5609
  for (const engineId of input.engineIds) {
5524
5610
  if (seen.has(engineId)) continue;
5525
5611
  seen.add(engineId);
5526
- out.push(await refreshOne(engineId, input, now));
5612
+ try {
5613
+ out.push(await refreshOne(engineId, input, now));
5614
+ } catch (e) {
5615
+ const detail = e instanceof Error ? e.message : String(e);
5616
+ input.log(`Engine "${engineId}" could not be checked: ${detail}`);
5617
+ out.push({ engineId, state: "failed", detail: detail.slice(0, 300) });
5618
+ }
5527
5619
  }
5528
5620
  return out;
5529
5621
  }
@@ -5550,7 +5642,7 @@ async function refreshOne(engineId, input, now) {
5550
5642
  target: choice.version,
5551
5643
  platform: process.platform,
5552
5644
  autoInstall: input.autoInstall,
5553
- npmPresent: npmPresentOnce(),
5645
+ npmPresent: input.npmPresent ?? npmPresentOnce(),
5554
5646
  marker,
5555
5647
  locked: false,
5556
5648
  record: record ?? null,
@@ -5561,17 +5653,17 @@ async function refreshOne(engineId, input, now) {
5561
5653
  return describeRefused(engineId, input, marker, decision.reason);
5562
5654
  }
5563
5655
  input.onState?.({ engineId, state: "installing", version: choice.version });
5564
- const outcome = await installEngine({
5656
+ const outcome = await (input.install ?? installEngine)({
5565
5657
  root,
5566
5658
  engineId,
5567
5659
  version: choice.version,
5568
- healthCheck: (bin) => getDriver(engineId).healthCheck(bin),
5660
+ healthCheck: (bin) => health(input, engineId, bin),
5569
5661
  log: log2,
5570
5662
  now
5571
5663
  });
5572
5664
  if (outcome.ok) return { engineId, state: "ready", version: choice.version };
5573
5665
  log2(`Could not install "${engineId}": ${outcome.detail.slice(0, 200)}`);
5574
- const fallback = await unmanagedVersion(engineId);
5666
+ const fallback = await unmanagedVersion(input, engineId);
5575
5667
  if (fallback && !policy.blocked.includes(fallback)) {
5576
5668
  return { engineId, state: "unmanaged", version: fallback, detail: "Using a copy already on this machine \u2014 this runner cannot update or roll it back." };
5577
5669
  }
@@ -5612,11 +5704,14 @@ function npmPresentOnce() {
5612
5704
  return answer;
5613
5705
  };
5614
5706
  }
5615
- async function unmanagedVersion(engineId) {
5707
+ function health(input, engineId, bin) {
5708
+ return input.driverHealth ? input.driverHealth(engineId, bin) : getDriver(engineId).healthCheck(bin);
5709
+ }
5710
+ async function unmanagedVersion(input, engineId) {
5616
5711
  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 ?? "");
5712
+ const probe2 = await health(input, engineId);
5713
+ if (!probe2.ok) return null;
5714
+ const match = /(\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?)/.exec(probe2.detail ?? "");
5620
5715
  return match?.[1] ?? null;
5621
5716
  } catch {
5622
5717
  return null;
@@ -5632,6 +5727,9 @@ function engineSet(engineIds) {
5632
5727
  if (engines.size === 0) engines.add(DEFAULT_ENGINE_ID);
5633
5728
  return engines;
5634
5729
  }
5730
+ function enginesForCredentials(credentials) {
5731
+ return [...new Set(credentials.map((c) => c.engine).filter((id) => isKnownEngine(id)))];
5732
+ }
5635
5733
 
5636
5734
  // src/service.ts
5637
5735
  import { spawnSync as spawnSync2 } from "node:child_process";
@@ -5658,6 +5756,15 @@ function run(command, args) {
5658
5756
  out: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
5659
5757
  };
5660
5758
  }
5759
+ function systemdPresent() {
5760
+ try {
5761
+ if (!fs12.existsSync("/run/systemd/system")) return false;
5762
+ const probe2 = process.platform === "win32" ? "where" : "which";
5763
+ return spawnSync2(probe2, ["systemctl"], { stdio: "ignore" }).status === 0;
5764
+ } catch {
5765
+ return false;
5766
+ }
5767
+ }
5661
5768
  function cliPath() {
5662
5769
  return fileURLToPath(import.meta.url);
5663
5770
  }
@@ -5840,6 +5947,21 @@ ${envLines}
5840
5947
  WantedBy=default.target
5841
5948
  `;
5842
5949
  }
5950
+ function linuxStatusFrom(facts) {
5951
+ if (!facts.systemdPresent) {
5952
+ return {
5953
+ state: "unsupported",
5954
+ 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."
5955
+ };
5956
+ }
5957
+ if (!facts.unitExists) return { state: "not-installed", detail: "No systemd user unit installed." };
5958
+ return {
5959
+ state: facts.isActive === "active" ? "running" : "installed",
5960
+ detail: `systemd user unit is ${facts.isActive || "unknown"}.`,
5961
+ unitPath: facts.unitPath,
5962
+ ...facts.exec ?? {}
5963
+ };
5964
+ }
5843
5965
  function serviceStatus() {
5844
5966
  if (process.platform === "darwin") {
5845
5967
  const unitPath = launchAgentPath();
@@ -5859,14 +5981,16 @@ function serviceStatus() {
5859
5981
  }
5860
5982
  if (process.platform === "linux") {
5861
5983
  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"}.`,
5984
+ const present = systemdPresent();
5985
+ const unitExists = fs12.existsSync(unitPath);
5986
+ const isActive = present && unitExists ? run("systemctl", ["--user", "is-active", LINUX_UNIT]).out : "";
5987
+ return linuxStatusFrom({
5988
+ systemdPresent: present,
5989
+ unitExists,
5990
+ isActive,
5867
5991
  unitPath,
5868
- ...execFacts(unitPath, parseSystemdCliPath, parseSystemdEnv)
5869
- };
5992
+ exec: unitExists ? execFacts(unitPath, parseSystemdCliPath, parseSystemdEnv) : void 0
5993
+ });
5870
5994
  }
5871
5995
  if (process.platform === "win32") {
5872
5996
  const query7 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
@@ -5880,6 +6004,11 @@ function serviceStatus() {
5880
6004
  return { state: "unsupported", detail: `No service integration for ${process.platform}.` };
5881
6005
  }
5882
6006
  function installService() {
6007
+ if (process.platform === "linux" && !systemdPresent()) {
6008
+ throw new ServiceError(
6009
+ `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.`
6010
+ );
6011
+ }
5883
6012
  const notes = [];
5884
6013
  fs12.mkdirSync(logDir(), { recursive: true, mode: 448 });
5885
6014
  for (const unit of removeLegacyService()) {
@@ -6180,6 +6309,7 @@ async function startDaemon() {
6180
6309
  const shipCredentials = /* @__PURE__ */ new Map();
6181
6310
  const heldCredentialIds = /* @__PURE__ */ new Map();
6182
6311
  const loginsInFlight = /* @__PURE__ */ new Map();
6312
+ const enginesBeingEnsured = /* @__PURE__ */ new Set();
6183
6313
  const unsubsByShip = /* @__PURE__ */ new Map();
6184
6314
  const listenerError = (shipId, what) => (e) => {
6185
6315
  if (isEnrolmentGone(e)) {
@@ -6355,6 +6485,15 @@ async function startDaemon() {
6355
6485
  ensureEngine: (engineId) => ensureEngineForCaller(engineId)
6356
6486
  }).catch((e) => log2(`Sign-in for ${credential.id} ended abnormally: ${e instanceof Error ? e.message : e}`)).finally(() => loginsInFlight.delete(credential.id));
6357
6487
  }
6488
+ for (const engineId of enginesForCredentials(list)) {
6489
+ if (enginesBeingEnsured.has(engineId)) continue;
6490
+ const known = engineToolStates.get(engineId)?.state;
6491
+ if (known === "ready" || known === "installing" || known === "unmanaged") continue;
6492
+ enginesBeingEnsured.add(engineId);
6493
+ void ensureEngineForCaller(engineId).then((result) => {
6494
+ if (!result.ok) log2(`Engine "${engineId}" is named by a credential on Ship ${shipId}: ${result.detail}`);
6495
+ }).catch((e) => log2(`Could not install "${engineId}": ${e instanceof Error ? e.message : e}`)).finally(() => enginesBeingEnsured.delete(engineId));
6496
+ }
6358
6497
  poke();
6359
6498
  },
6360
6499
  // ITS OWN ERROR HANDLER, not `listenerError`, for the reason the mcp_servers listener
@@ -7319,7 +7458,10 @@ ${harder.transcript}`
7319
7458
  if (shipIds.length === 0) return;
7320
7459
  checkingEngines = true;
7321
7460
  try {
7322
- const engineIds = engineSet(agentEngines.values());
7461
+ const engineIds = engineSet([
7462
+ ...agentEngines.values(),
7463
+ ...[...shipCredentials.values()].flatMap((list) => enginesForCredentials(list))
7464
+ ]);
7323
7465
  const firstShip = shipIds[0];
7324
7466
  const policySnap = await getDoc8(
7325
7467
  doc9(sess(firstShip).fb.db, COLLECTIONS.crewConfig, CONFIG_DOCS.engines)
@@ -7707,12 +7849,12 @@ function setUpdateSetting(config2, key, value, ship2) {
7707
7849
  function setLocalMcp(config2, value, ship2) {
7708
7850
  if (!ship2) {
7709
7851
  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.`
7852
+ `"${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
7853
  );
7712
7854
  }
7713
7855
  if (!config2.ships.includes(ship2)) {
7714
7856
  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.`
7857
+ `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
7858
  );
7717
7859
  }
7718
7860
  const allowed = parseBool(value);
@@ -7732,7 +7874,7 @@ function setLocalMcp(config2, value, ship2) {
7732
7874
  " 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
7875
  );
7734
7876
  }
7735
- say.line(" Restart the daemon for this to take effect: `lumi-runner service restart`.");
7877
+ say.line(` Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
7736
7878
  return 0;
7737
7879
  }
7738
7880
  function setParallel(config2, value, ship2) {
@@ -7740,7 +7882,7 @@ function setParallel(config2, value, ship2) {
7740
7882
  if (ship2) {
7741
7883
  if (!config2.ships.includes(ship2)) {
7742
7884
  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.`
7885
+ `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
7886
  );
7745
7887
  }
7746
7888
  const overrides = { ...config2.shipParallelJobs ?? {} };
@@ -7764,10 +7906,10 @@ function setParallel(config2, value, ship2) {
7764
7906
  );
7765
7907
  if (ship2 && parsed !== null && parsed > machine) {
7766
7908
  say.line(
7767
- ` Capped at the machine-wide limit of ${machine}. Raise it with \`lumi-runner config set ${PARALLEL} ${parsed}\`.`
7909
+ ` Capped at the machine-wide limit of ${machine}. Raise it with \`${RUNNER_BIN} config set ${PARALLEL} ${parsed}\`.`
7768
7910
  );
7769
7911
  }
7770
- say.line(" Restart the daemon for this to take effect: `lumi-runner service restart`.");
7912
+ say.line(` Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
7771
7913
  return 0;
7772
7914
  }
7773
7915
 
@@ -7827,20 +7969,27 @@ function serviceCheckFrom(status) {
7827
7969
  "service",
7828
7970
  "Background service",
7829
7971
  `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."
7972
+ `Run \`${RUNNER_BIN} uninstall\` to remove it, or \`${RUNNER_BIN} service install\` to repoint it at this install.`
7831
7973
  );
7832
7974
  }
7833
7975
  if (status.state === "running") return ok("service", "Background service", status.detail);
7834
7976
  if (status.state === "installed") {
7835
- return warn("service", "Background service", status.detail, "Run `lumi-runner service restart`.");
7977
+ return warn("service", "Background service", status.detail, `Run \`${RUNNER_BIN} service restart\`.`);
7978
+ }
7979
+ if (status.state === "unsupported") {
7980
+ return warn(
7981
+ "service",
7982
+ "Background service",
7983
+ status.detail,
7984
+ `Run \`${RUNNER_BIN} start\` to keep this machine online \u2014 in Docker, make that the container's command.`
7985
+ );
7836
7986
  }
7837
- if (status.state === "unsupported") return warn("service", "Background service", status.detail);
7838
7987
  if (status.state === "not-installed") {
7839
7988
  return warn(
7840
7989
  "service",
7841
7990
  "Background service",
7842
7991
  "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."
7992
+ `Run \`${RUNNER_BIN} service install\` to keep this Ship online whenever the machine is.`
7844
7993
  );
7845
7994
  }
7846
7995
  return warn("service", "Background service", `Unrecognised service state: ${status.state}.`);
@@ -7862,7 +8011,7 @@ function serviceBinaryCheckFrom(input) {
7862
8011
  id,
7863
8012
  label,
7864
8013
  `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."
8014
+ `Run \`${RUNNER_BIN} service install\` from a shell where these work, to rewrite the unit with the current PATH.`
7866
8015
  );
7867
8016
  }
7868
8017
  function resolvesOnPath(binary, pathValue) {
@@ -7903,7 +8052,7 @@ function localMcpCheckFrom(shipId, servers, allowed) {
7903
8052
  id,
7904
8053
  label,
7905
8054
  `${local.length} connection(s) run on this machine and are not allowed here: ${names}.`,
7906
- `lumi-runner config set local-mcp on --ship ${shipId}`
8055
+ `${RUNNER_BIN} config set local-mcp on --ship ${shipId}`
7907
8056
  );
7908
8057
  }
7909
8058
  async function checkVersion(config2) {
@@ -7922,7 +8071,7 @@ async function checkShips(config2) {
7922
8071
  const manyIdentities = configRunnerIdentities(config2).length > 1;
7923
8072
  if (config2.ships.length === 0) {
7924
8073
  checks.push(
7925
- fail("ships", "Ships", "This machine serves no Ships.", "Run `lumi-runner ship add` to pick one.")
8074
+ fail("ships", "Ships", "This machine serves no Ships.", `Run \`${RUNNER_BIN} ship add\` to pick one.`)
7926
8075
  );
7927
8076
  return { checks, engines, needsGithub };
7928
8077
  }
@@ -7937,7 +8086,7 @@ async function checkShips(config2) {
7937
8086
  `key:${shipId}`,
7938
8087
  `Ship ${shipId} \u2014 key`,
7939
8088
  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`."
8089
+ `Ask a captain to approve this machine on that Ship's Daemons page, then re-run \`${RUNNER_BIN} login\`.`
7941
8090
  )
7942
8091
  );
7943
8092
  continue;
@@ -7952,7 +8101,7 @@ async function checkShips(config2) {
7952
8101
  `approval:${shipId}`,
7953
8102
  `Ship ${shipId} \u2014 approval`,
7954
8103
  "This machine has no record on that Ship.",
7955
- "Re-run `lumi-runner login` \u2014 approving is what enrols it."
8104
+ `Re-run \`${RUNNER_BIN} login\` \u2014 approving is what enrols it.`
7956
8105
  )
7957
8106
  );
7958
8107
  } else if (snap.data().approved !== true) {
@@ -7989,11 +8138,11 @@ async function checkShips(config2) {
7989
8138
  agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
7990
8139
  } catch {
7991
8140
  }
7992
- const shipEngines = enginesForAgents(agents);
8141
+ const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
8142
+ const shipEngines = engineSet([...enginesForAgents(agents), ...enginesForCredentials(credentials)]);
7993
8143
  for (const id of shipEngines) engines.add(id);
7994
8144
  if (agents.some((agent) => effectiveAgentTools(agent).github.enabled)) needsGithub = true;
7995
8145
  try {
7996
- const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
7997
8146
  const missing = [
7998
8147
  ...new Set(
7999
8148
  [...shipEngines].flatMap(
@@ -8032,8 +8181,8 @@ async function checkShips(config2) {
8032
8181
  );
8033
8182
  }
8034
8183
  try {
8035
- const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
8036
- for (const cred of credentials) {
8184
+ const credentials2 = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
8185
+ for (const cred of credentials2) {
8037
8186
  if (!isMachineHeldCredential(cred) || cred.runnerId !== session.runnerId) continue;
8038
8187
  if (!isMachineLoginEngine(cred.engine)) continue;
8039
8188
  const driver = getDriver(cred.engine);
@@ -8098,7 +8247,7 @@ async function runDoctor() {
8098
8247
  const config2 = loadConfig();
8099
8248
  if (!config2) {
8100
8249
  checks.push(
8101
- fail("config", "Configuration", "This machine is not connected.", "Run `lumi-runner setup`.")
8250
+ fail("config", "Configuration", "This machine is not connected.", `Run \`${RUNNER_BIN} setup\`.`)
8102
8251
  );
8103
8252
  return report2(checks);
8104
8253
  }
@@ -8122,19 +8271,26 @@ async function runDoctor() {
8122
8271
  const needsGithub = shipResults.needsGithub;
8123
8272
  const jobBinaries = [];
8124
8273
  for (const engineId of engines) {
8125
- const health = await getDriver(engineId).healthCheck();
8126
- if (health.binary) jobBinaries.push(health.binary);
8274
+ const health2 = await getDriver(engineId).healthCheck();
8275
+ if (health2.binary) jobBinaries.push(health2.binary);
8127
8276
  const managed = managedEngineBin(engineId, {});
8128
8277
  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";
8278
+ 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
8279
  checks.push(
8131
- health.ok ? ok(`engine:${engineId}`, `Engine "${engineId}"`, `${health.detail}${origin}`) : fail(
8280
+ health2.ok ? ok(`engine:${engineId}`, `Engine "${engineId}"`, `${health2.detail}${origin}`) : fail(
8132
8281
  `engine:${engineId}`,
8133
8282
  `Engine "${engineId}"`,
8134
- health.detail,
8283
+ health2.detail,
8135
8284
  // A way forward the operator can type, ahead of the vendor's download page: this
8136
8285
  // 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
8286
+ //
8287
+ // THE AUTOMATIC PATH GOES FIRST since §15.85, because on a fresh machine it is the one
8288
+ // that will actually happen and the old wording hid it. `doctor` runs BEFORE the daemon
8289
+ // exists during `setup`, so this row is guaranteed to fail there — and an operator who
8290
+ // read "N check(s) failed" walked away believing the runner does not install engines,
8291
+ // minutes before it silently did. Naming the daemon first is the difference between a
8292
+ // failure and a step that has not happened yet.
8293
+ 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
8294
  )
8139
8295
  );
8140
8296
  if (managed) jobBinaries.push("node");
@@ -8209,7 +8365,7 @@ async function runEngineList() {
8209
8365
  for (const engineId of installableEngineIds()) {
8210
8366
  const pkg = enginePackageFor(engineId);
8211
8367
  const { origin, bin, version } = originOf(engineId);
8212
- const health = await getDriver(engineId).healthCheck();
8368
+ const health2 = await getDriver(engineId).healthCheck();
8213
8369
  const record = state[engineId];
8214
8370
  rows.push({
8215
8371
  engine: engineId,
@@ -8217,10 +8373,10 @@ async function runEngineList() {
8217
8373
  package: pkg?.npmPackage ?? null,
8218
8374
  shipped: DEFAULT_ENGINE_POLICIES[engineId]?.knownGood ?? null,
8219
8375
  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,
8376
+ origin: origin === "none" && health2.ok ? "path" : origin,
8377
+ binary: bin ?? health2.binary ?? null,
8378
+ ok: health2.ok,
8379
+ detail: health2.ok ? void 0 : health2.detail,
8224
8380
  lastAttempt: record ? { target: record.target, outcome: record.outcome, attempts: record.attempts, detail: record.detail } : null,
8225
8381
  diskBytes: dirSize(enginePrefixDir(root, engineId))
8226
8382
  });
@@ -8428,7 +8584,7 @@ Code: ${pc.bold(start.displayCode)}`,
8428
8584
  }
8429
8585
  if (!approved) {
8430
8586
  progress.stop("");
8431
- throw new CliError("The approval window expired. Run `lumi-runner login` again.");
8587
+ throw new CliError(`The approval window expired. Run \`${RUNNER_BIN} login\` again.`);
8432
8588
  }
8433
8589
  progress.stop("Approved.");
8434
8590
  const { config: config2, approvedShips, pendingShips, occupiedShips, newIdentity, otherShips } = buildLoginResult(
@@ -8438,7 +8594,7 @@ Code: ${pc.bold(start.displayCode)}`,
8438
8594
  options.mcpUrl
8439
8595
  );
8440
8596
  if (!config2.runnerId) {
8441
- throw new CliError("The approval did not name this machine. Run `lumi-runner login` again.");
8597
+ throw new CliError(`The approval did not name this machine. Run \`${RUNNER_BIN} login\` again.`);
8442
8598
  }
8443
8599
  if (!config2.apiKey) {
8444
8600
  throw new CliError(
@@ -8541,11 +8697,11 @@ function report3(result) {
8541
8697
  }
8542
8698
  if (result.otherShips && result.otherShips.length > 0) {
8543
8699
  say.info(
8544
- `Still serving ${result.otherShips.join(", ")} \u2014 this approval did not mention them. \`lumi-runner ship remove <shipId>\` to stop.`
8700
+ `Still serving ${result.otherShips.join(", ")} \u2014 this approval did not mention them. \`${RUNNER_BIN} ship remove <shipId>\` to stop.`
8545
8701
  );
8546
8702
  }
8547
8703
  if (serviceStatus().state === "running") {
8548
- say.info("Restart the daemon for this to take effect: `lumi-runner service restart`.");
8704
+ say.info(`Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
8549
8705
  }
8550
8706
  return 0;
8551
8707
  }
@@ -8574,7 +8730,16 @@ async function runLogs(options) {
8574
8730
  // src/cli/commands/service.ts
8575
8731
  async function runServiceInstall() {
8576
8732
  const before = serviceStatus();
8577
- if (before.state !== "not-installed" && before.state !== "unsupported") {
8733
+ if (before.state === "unsupported") {
8734
+ if (isJson()) {
8735
+ emitJson({ installed: false, supported: false, ...before });
8736
+ return 1;
8737
+ }
8738
+ say.warn(before.detail);
8739
+ say.info(`Run \`${RUNNER_BIN} start\` to keep the daemon up \u2014 in Docker, make that the container's command.`);
8740
+ return 1;
8741
+ }
8742
+ if (before.state !== "not-installed") {
8578
8743
  const replace = await promptConfirm({
8579
8744
  message: "A service is already installed. Reinstall it with the current settings?",
8580
8745
  initialValue: true
@@ -8632,7 +8797,7 @@ async function runServiceStatus() {
8632
8797
  say.line(` ${status.detail}`);
8633
8798
  if (status.unitPath) say.line(` ${status.unitPath}`);
8634
8799
  if (status.execMissing) {
8635
- say.warn(`The unit runs ${status.execPath}, which no longer exists. Run \`lumi-runner uninstall\`.`);
8800
+ say.warn(`The unit runs ${status.execPath}, which no longer exists. Run \`${RUNNER_BIN} uninstall\`.`);
8636
8801
  }
8637
8802
  return 0;
8638
8803
  }
@@ -8698,14 +8863,14 @@ async function pickShips(current) {
8698
8863
  const ships = await listMyShips();
8699
8864
  if (ships.length === 0) {
8700
8865
  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."
8866
+ `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
8867
  );
8703
8868
  }
8704
8869
  return promptMultiSelect({
8705
8870
  message: "Which Ships should this machine serve?",
8706
8871
  choices: ships.map((ship2) => ({ value: ship2.id, label: ship2.name || ship2.id, hint: ship2.id })),
8707
8872
  initialValues: current.filter((id) => ships.some((s) => s.id === id)),
8708
- flagHint: "lumi-runner ship add <shipId>"
8873
+ flagHint: `${RUNNER_BIN} ship add <shipId>`
8709
8874
  });
8710
8875
  }
8711
8876
  async function runShipList() {
@@ -8715,7 +8880,7 @@ async function runShipList() {
8715
8880
  return 0;
8716
8881
  }
8717
8882
  if (config2.ships.length === 0) {
8718
- say.warn("This machine serves no Ships. Run `lumi-runner ship add`.");
8883
+ say.warn(`This machine serves no Ships. Run \`${RUNNER_BIN} ship add\`.`);
8719
8884
  return 0;
8720
8885
  }
8721
8886
  for (const shipId of config2.ships) say.line(` ${shipId}`);
@@ -8746,18 +8911,36 @@ function reportAssignment(ships) {
8746
8911
  return 0;
8747
8912
  }
8748
8913
  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`.");
8914
+ say.info(`Restart the daemon for this to take effect: \`${RUNNER_BIN} service restart\`.`);
8750
8915
  return 0;
8751
8916
  }
8752
8917
 
8753
8918
  // src/cli/commands/setup.ts
8919
+ function shipChoiceFrom(input) {
8920
+ const label = (id) => input.fetched?.find((s) => s.id === id)?.name || id;
8921
+ const serving = input.serving.map(label).join(", ");
8922
+ const question = `Serving ${serving}. Change which Ships this machine serves? (yes replaces the list)`;
8923
+ if (!input.fetched || input.fetched.length < input.keyCount) return { ask: true, line: question };
8924
+ const servingIds = new Set(input.serving);
8925
+ const others = input.fetched.filter((s) => !servingIds.has(s.id));
8926
+ const fetchedIds = new Set(input.fetched.map((s) => s.id));
8927
+ const unkeyed = input.serving.filter((id) => !fetchedIds.has(id));
8928
+ if (others.length === 0 && unkeyed.length === 0) {
8929
+ return { ask: false, line: `Serving ${serving}. This machine holds no other Ship keys.` };
8930
+ }
8931
+ if (others.length === 0) return { ask: true, line: question };
8932
+ return {
8933
+ ask: true,
8934
+ 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)`
8935
+ };
8936
+ }
8754
8937
  async function runSetup(options) {
8755
8938
  if (!canPrompt()) {
8756
8939
  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."
8940
+ `setup is interactive. On a headless machine run \`${RUNNER_BIN} login\`, \`${RUNNER_BIN} ship add <id>\` and \`${RUNNER_BIN} service install\` instead.`
8758
8941
  );
8759
8942
  }
8760
- say.intro("lumi-runner setup");
8943
+ say.intro(`${RUNNER_BIN} setup`);
8761
8944
  const moved = migrateLegacyDir();
8762
8945
  if (moved?.migrated) say.info(`Moved your runner config from ${moved.from} to ${moved.to}.`);
8763
8946
  const existing = loadConfig();
@@ -8782,7 +8965,7 @@ async function runSetup(options) {
8782
8965
  }
8783
8966
  ],
8784
8967
  initialValue: "keep",
8785
- flagHint: "lumi-runner login"
8968
+ flagHint: `${RUNNER_BIN} login`
8786
8969
  });
8787
8970
  if (action2 === "add") {
8788
8971
  await runLogin(options);
@@ -8805,14 +8988,24 @@ async function runSetup(options) {
8805
8988
  saveConfig(config2);
8806
8989
  say.success(`Serving: ${config2.ships.join(", ") || "(none)"}`);
8807
8990
  } else {
8808
- const change = await promptConfirm({
8809
- message: `Serving ${config2.ships.join(", ")}. Change that?`,
8810
- initialValue: false,
8811
- yesFlagApplies: false
8991
+ const fetched = await listMyShips().catch(() => null);
8992
+ const choice = shipChoiceFrom({
8993
+ serving: config2.ships,
8994
+ fetched,
8995
+ keyCount: Object.keys(config2.shipKeys ?? {}).length
8812
8996
  });
8813
- if (change) {
8814
- config2.ships = await pickShips(config2.ships);
8815
- saveConfig(config2);
8997
+ if (!choice.ask) {
8998
+ say.info(choice.line);
8999
+ } else {
9000
+ const change = await promptConfirm({
9001
+ message: choice.line,
9002
+ initialValue: false,
9003
+ yesFlagApplies: false
9004
+ });
9005
+ if (change) {
9006
+ config2.ships = await pickShips(config2.ships);
9007
+ saveConfig(config2);
9008
+ }
8816
9009
  }
8817
9010
  }
8818
9011
  say.step("Checking this machine\u2026");
@@ -8826,7 +9019,7 @@ async function runSetup(options) {
8826
9019
  yesFlagApplies: false
8827
9020
  });
8828
9021
  if (install) await runServiceInstall();
8829
- else say.info("Skipped. Run `lumi-runner start` manually, or `lumi-runner service install` later.");
9022
+ else say.info(`Skipped. Run \`${RUNNER_BIN} start\` manually, or \`${RUNNER_BIN} service install\` later.`);
8830
9023
  } else if (service2 === "running" || service2 === "installed") {
8831
9024
  const drift = serviceEnvDrift(serviceEnv(), status.unitEnv);
8832
9025
  if (drift.length > 0) {
@@ -8836,9 +9029,14 @@ async function runSetup(options) {
8836
9029
  say.step("Restarting the daemon so it picks up this configuration\u2026");
8837
9030
  await runServiceRestart();
8838
9031
  }
9032
+ } else if (service2 === "unsupported") {
9033
+ say.info(status.detail);
9034
+ say.info(
9035
+ `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.`
9036
+ );
8839
9037
  }
8840
9038
  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`."
9039
+ 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
9040
  );
8843
9041
  return doctorExit;
8844
9042
  }
@@ -8860,7 +9058,7 @@ async function runStatus() {
8860
9058
  emitJson({ version: RUNNER_VERSION, connected: false });
8861
9059
  return 0;
8862
9060
  }
8863
- say.warn("This machine is not connected. Run `lumi-runner setup`.");
9061
+ say.warn(`This machine is not connected. Run \`${RUNNER_BIN} setup\`.`);
8864
9062
  return 0;
8865
9063
  }
8866
9064
  const progress = spinner2();
@@ -8952,7 +9150,7 @@ async function runStatus() {
8952
9150
  say.line(` ${pc.bold("Service")} ${service2.detail}`);
8953
9151
  say.line("");
8954
9152
  if (ships.length === 0) {
8955
- say.warn("This machine serves no Ships. Run `lumi-runner ship add`.");
9153
+ say.warn(`This machine serves no Ships. Run \`${RUNNER_BIN} ship add\`.`);
8956
9154
  return 0;
8957
9155
  }
8958
9156
  for (const ship2 of ships) {
@@ -9074,7 +9272,7 @@ async function runUpdate(options = {}) {
9074
9272
 
9075
9273
  // src/cli/index.ts
9076
9274
  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) => {
9275
+ 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
9276
  const options = command.opts();
9079
9277
  configureOutput(options);
9080
9278
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kilogent/runner",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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.",