@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.
- package/dist/cli.js +301 -103
- 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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
workspace
|
|
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.
|
|
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
|
|
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 = (
|
|
3359
|
+
const done = (health2) => {
|
|
3277
3360
|
if (settled) return;
|
|
3278
3361
|
settled = true;
|
|
3279
|
-
resolve({ ...
|
|
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 = (
|
|
3892
|
+
const done = (health2) => {
|
|
3810
3893
|
if (settled) return;
|
|
3811
3894
|
settled = true;
|
|
3812
|
-
resolve({ ...
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
5253
|
-
if (!
|
|
5254
|
-
const 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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
5618
|
-
if (!
|
|
5619
|
-
const match = /(\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?)/.exec(
|
|
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
|
-
|
|
5863
|
-
const
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
8036
|
-
for (const cred of
|
|
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.",
|
|
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
|
|
8126
|
-
if (
|
|
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})` : ""}` :
|
|
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
|
-
|
|
8280
|
+
health2.ok ? ok(`engine:${engineId}`, `Engine "${engineId}"`, `${health2.detail}${origin}`) : fail(
|
|
8132
8281
|
`engine:${engineId}`,
|
|
8133
8282
|
`Engine "${engineId}"`,
|
|
8134
|
-
|
|
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
|
-
|
|
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
|
|
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" &&
|
|
8221
|
-
binary: bin ??
|
|
8222
|
-
ok:
|
|
8223
|
-
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
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 (
|
|
8814
|
-
|
|
8815
|
-
|
|
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(
|
|
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." :
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.",
|