byollm 0.1.0-alpha.63 → 0.1.0-alpha.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!WARNING]
2
- > **Alpha (`0.1.0-alpha.63`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.65`) — under active development. Don't use this yet.**
3
3
  >
4
4
  > Install it deliberately: `npx byollm@alpha`, or `npm install byollm@alpha`.
5
5
  >
@@ -85,7 +85,7 @@
85
85
  > packages published and `@byollm/server` did not: a Sigstore
86
86
  > transparency-log 409 on its provenance attestation. The workflow's
87
87
  > "already published" guard correctly refuses to resume a partial publish,
88
- > so `0.1.0-alpha.63` is that release, whole.
88
+ > so `0.1.0-alpha.65` is that release, whole.
89
89
  >
90
90
  > If you run the Supabase adapter, `alpha.21` needs
91
91
  > `20260819010000_completed_by_lease_id.sql`: alpha.19 shipped §3.6's
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-J4GXTVXX.js";
4
+ } from "./chunk-HM5G6ZUG.js";
5
5
 
6
6
  // src/bin.ts
7
7
  process.exitCode = await main(process.argv.slice(2));
@@ -51,7 +51,7 @@ function asHttpOrigin(candidate) {
51
51
  }
52
52
 
53
53
  // src/cli.ts
54
- import { mkdir as mkdir9, readFile as readFile9, rm as rm5, stat as stat3, writeFile as writeFile9 } from "fs/promises";
54
+ import { mkdir as mkdir9, readFile as readFile10, rm as rm5, stat as stat3, writeFile as writeFile10 } from "fs/promises";
55
55
  import { hostname, userInfo } from "os";
56
56
  import { dirname as dirname10 } from "path";
57
57
  import { createInterface as createInterface2 } from "readline/promises";
@@ -229,7 +229,15 @@ ${stderr}`);
229
229
  finish({
230
230
  ok: false,
231
231
  code: authFailed ? "unauthorized" : "backend-error",
232
- message: authFailed ? `${displayName} is not signed in` : stderr.trim() === "" ? `${displayName} exited with status ${String(code)}` : `${displayName} failed: ${firstLine(stderr)}`,
232
+ message: authFailed ? `${displayName} is not signed in` : stderr.trim() !== "" ? `${displayName} failed: ${firstLine(stderr)}` : (
233
+ /* Nothing on stderr does not mean nothing was said. An
234
+ agentic CLI writes its errors to stdout — that is where
235
+ "Failed to authenticate" arrived — and reporting a bare
236
+ exit code while holding the explanation is the one thing a
237
+ diagnostic must not do. No new exposure: stdout already
238
+ goes to the site on success. */
239
+ stdout.trim() !== "" ? `${displayName} failed: ${firstLine(stdout)}` : `${displayName} exited with status ${String(code)}`
240
+ ),
233
241
  retryable: false,
234
242
  durationMs
235
243
  });
@@ -256,7 +264,31 @@ var AUTH_FAILURE = [
256
264
  /\bnot authenticated\b/,
257
265
  /\bauthentication failed\b/,
258
266
  /\binvalid api key\b/,
259
- /\b401 unauthorized\b/
267
+ /\b401 unauthorized\b/,
268
+ /**
269
+ * The expiry family — Todd's Mac, 2026-08-31, and the reason this list grew.
270
+ *
271
+ * The CLI said "Failed to authenticate. API Error: 401 OAuth access token
272
+ * has expired. Re-authenticate to continue." and not one pattern above
273
+ * matched it: the list had "authentication failed" and the CLI wrote the
274
+ * same two words in the other order, and it had "401 unauthorized" against
275
+ * a 401 that named OAuth instead. So a signed-out backend was reported as
276
+ * `backend-error`, the service was not withdrawn, and the sentence that
277
+ * reached the person was "the claude CLI exited with status 1".
278
+ *
279
+ * An expired token is the *common* case in a subscription CLI — it is what
280
+ * happens to everybody eventually, on a machine that was working
281
+ * yesterday — and it was the one the corpus lacked.
282
+ *
283
+ * "failed to authenticate" was in this list for about a minute. The test
284
+ * below refused it: "the guard had failed to authenticate the visitor's
285
+ * papers" is a sentence a model can write, and matching it would withdraw a
286
+ * service that works. The two that remain are auth machinery talking about
287
+ * itself, which prose has little reason to imitate — and Todd's message
288
+ * contained both, so nothing was lost by dropping the loose one.
289
+ */
290
+ /\baccess token has expired\b/,
291
+ /\bre-?authenticate\b/
260
292
  ];
261
293
  function isAuthFailure(output) {
262
294
  const text = output.toLowerCase();
@@ -368,6 +400,7 @@ function resolveUncached(binary, platform2, source, npmPackage) {
368
400
  var ClaudeCliBackend = class {
369
401
  id = "claude-cli";
370
402
  class = "process";
403
+ signIn = "run `claude` in a terminal";
371
404
  #binary;
372
405
  /**
373
406
  * @param binary - which executable to run. Defaults to `claude` and is
@@ -479,6 +512,7 @@ function codexArgv(model) {
479
512
  var CodexCliBackend = class {
480
513
  id = "codex-cli";
481
514
  class = "process";
515
+ signIn = "run `codex login`";
482
516
  #binary;
483
517
  /**
484
518
  * @param binary - which executable to run. Defaults to `codex` and is **not
@@ -1235,6 +1269,30 @@ var BOTH_KINDS = Object.freeze([
1235
1269
  "llm.generate",
1236
1270
  "llm.chat"
1237
1271
  ]);
1272
+ async function detect(id, model) {
1273
+ try {
1274
+ const backend = createBackend(id, {});
1275
+ const health = await backend.health();
1276
+ if (!health.healthy) {
1277
+ return {
1278
+ installed: false,
1279
+ answers: void 0,
1280
+ ...health.detail === void 0 ? {} : { detail: health.detail }
1281
+ };
1282
+ }
1283
+ if (backend.canary === void 0) {
1284
+ return { installed: true, answers: void 0 };
1285
+ }
1286
+ const proof = await backend.canary(model);
1287
+ return {
1288
+ installed: true,
1289
+ answers: proof.healthy,
1290
+ ...proof.detail === void 0 ? {} : { detail: proof.detail }
1291
+ };
1292
+ } catch {
1293
+ return { installed: false, answers: void 0 };
1294
+ }
1295
+ }
1238
1296
  async function detectInstalled(id) {
1239
1297
  try {
1240
1298
  const backend = createBackend(id, {});
@@ -1249,7 +1307,7 @@ var yes = (answer, fallback) => {
1249
1307
  if (text === "") return fallback;
1250
1308
  return /^y(es)?$/.test(text);
1251
1309
  };
1252
- async function runSetup(paths, io, detector = detectInstalled, probe = () => probeLocalServers()) {
1310
+ async function runSetup(paths, io, detector = detectInstalled, probe = () => probeLocalServers(), verifier = detect) {
1253
1311
  if (!io.interactive) {
1254
1312
  io.err(
1255
1313
  "byollm setup needs a terminal it can ask questions in.\nWrite ~/.byollm/config.json by hand instead: https://docs.byollm.cloud/guides/models\n"
@@ -1258,13 +1316,27 @@ async function runSetup(paths, io, detector = detectInstalled, probe = () => pro
1258
1316
  }
1259
1317
  const existing = await readExisting(paths.config);
1260
1318
  if (existing !== void 0) {
1261
- io.out(
1262
- `You already have a config at ${paths.config}.
1263
- It has ${String(Object.keys(existing.services).length)} service(s). Setup will not change it.
1319
+ const count = Object.keys(existing.services).length;
1320
+ if (count > 0) {
1321
+ io.out(
1322
+ `You already have a config at ${paths.config}.
1323
+ It has ${String(count)} service(s). Setup will not change it.
1264
1324
  Run \`byollm services\` to see what it does, or edit that file.
1325
+ `
1326
+ );
1327
+ return { wrote: false, services: [] };
1328
+ }
1329
+ io.out(
1330
+ `
1331
+ Your config at ${paths.config} has no services in it, so nothing
1332
+ can run yet. That is how versions before alpha.44 left it.
1265
1333
  `
1266
1334
  );
1267
- return { wrote: false, services: [] };
1335
+ const go = await io.ask(" Set it up now? [Y/n] ");
1336
+ if (!yes(go, true)) {
1337
+ io.out(" Left alone. Nothing was changed.\n");
1338
+ return { wrote: false, services: [] };
1339
+ }
1268
1340
  }
1269
1341
  io.out(`
1270
1342
  Setting up byollm. Change any of it later in ${paths.config}.
@@ -1283,6 +1355,16 @@ Setting up byollm. Change any of it later in ${paths.config}.
1283
1355
  io.out(`
1284
1356
  Found the \`${cli.binary}\` CLI.
1285
1357
  `);
1358
+ const proof = await verifier(cli.id, cli.model);
1359
+ if (proof.answers === false) {
1360
+ io.out(
1361
+ ` It is installed but cannot answer yet \u2014 it needs signing in.
1362
+ Run \`${cli.binary}\` in a terminal, sign in, then rerun this.
1363
+ Setting it up anyway; nothing will route to it until it can
1364
+ answer, and \`byollm status\` will keep saying so.
1365
+ `
1366
+ );
1367
+ }
1286
1368
  io.out(
1287
1369
  ` Uses your ${cli.plan}, for YOUR OWN jobs only \u2014 never shared with a
1288
1370
  team, whatever the config says. Someone else's terms are not yours
@@ -2705,6 +2787,7 @@ function daemonPaths(root = defaultRoot()) {
2705
2787
  ingressLog: join3(root, "ingress.log"),
2706
2788
  budgets: join3(root, "budgets.json"),
2707
2789
  spend: join3(root, "spend.json"),
2790
+ serviceStates: join3(root, "services.json"),
2708
2791
  spentGrants: join3(root, "spent-grants.json"),
2709
2792
  keys: join3(root, "keys.json"),
2710
2793
  pauseFlag: join3(root, "paused"),
@@ -2717,6 +2800,39 @@ function defaultRoot() {
2717
2800
  return process.env["BYOLLM_HOME"] ?? join3(homedir(), ".byollm");
2718
2801
  }
2719
2802
 
2803
+ // src/site-outcome.ts
2804
+ var SERVICE_UNAVAILABLE = "a device service is unavailable \u2014 its owner has been told";
2805
+ var FOR_SITE = Object.freeze({
2806
+ "backend-unreachable": {
2807
+ code: "service_unavailable",
2808
+ message: SERVICE_UNAVAILABLE
2809
+ },
2810
+ "backend-error": {
2811
+ code: "service_unavailable",
2812
+ message: SERVICE_UNAVAILABLE
2813
+ },
2814
+ "model-not-found": {
2815
+ code: "service_unavailable",
2816
+ message: SERVICE_UNAVAILABLE
2817
+ },
2818
+ unauthorized: {
2819
+ code: "service_unavailable",
2820
+ message: SERVICE_UNAVAILABLE
2821
+ },
2822
+ timeout: {
2823
+ code: "timeout",
2824
+ message: "the device did not answer in time"
2825
+ },
2826
+ "output-too-large": {
2827
+ code: "output-too-large",
2828
+ message: "the answer was too large to return"
2829
+ },
2830
+ canceled: { code: "canceled", message: "the job was canceled" }
2831
+ });
2832
+ function outcomeForSite(code) {
2833
+ return FOR_SITE[code];
2834
+ }
2835
+
2720
2836
  // src/runner.ts
2721
2837
  import {
2722
2838
  RETIREMENT_WINDOW_MS,
@@ -2915,7 +3031,15 @@ var Runner = class {
2915
3031
  * daemon then receives no work for it, which is the correct outcome and one
2916
3032
  * the owner can see in `byollm status`.
2917
3033
  */
3034
+ /**
3035
+ * What the last probe learned about each service, for the owner's surfaces.
3036
+ *
3037
+ * Read by `byollm status`, by `byollm connect`'s report and by the daemon's
3038
+ * own output — one answer, three renderings, through `serviceLine`.
3039
+ */
3040
+ serviceStates = /* @__PURE__ */ new Map();
2918
3041
  async detectCapabilities(options = {}) {
3042
+ this.serviceStates = /* @__PURE__ */ new Map();
2919
3043
  const capabilities = [];
2920
3044
  const probed = /* @__PURE__ */ new Map();
2921
3045
  for (const route of this.#options.loaded.routes) {
@@ -2925,9 +3049,33 @@ var Runner = class {
2925
3049
  const backend = this.#backendFor(route);
2926
3050
  const health = await backend.health();
2927
3051
  usable = health.healthy && (health.models.length === 0 || modelPresent(health.models, route.model));
3052
+ const remedy = backend.signIn === void 0 ? {} : { signIn: backend.signIn };
3053
+ if (!health.healthy) {
3054
+ this.serviceStates.set(route.service, {
3055
+ state: { kind: "missing" },
3056
+ ...remedy
3057
+ });
3058
+ } else if (options.canary !== true || backend.canary === void 0) {
3059
+ this.serviceStates.set(route.service, {
3060
+ state: { kind: "unknown", model: route.model },
3061
+ ...remedy
3062
+ });
3063
+ } else {
3064
+ this.serviceStates.set(route.service, {
3065
+ state: { kind: "answers", model: route.model },
3066
+ ...remedy
3067
+ });
3068
+ }
2928
3069
  if (usable && options.canary === true && backend.canary !== void 0) {
2929
3070
  const proof = await backend.canary(route.model);
2930
3071
  if (!proof.healthy) {
3072
+ this.serviceStates.set(route.service, {
3073
+ state: {
3074
+ kind: "signed-out",
3075
+ ...proof.detail === void 0 ? {} : { detail: proof.detail }
3076
+ },
3077
+ ...remedy
3078
+ });
2931
3079
  usable = false;
2932
3080
  this.#unauthenticated.add(route.service);
2933
3081
  this.#options.onEvent?.({
@@ -3320,12 +3468,34 @@ var Runner = class {
3320
3468
  detail: result.message
3321
3469
  });
3322
3470
  }
3323
- const outcome = result.ok ? { outcome: "ok", text: result.text } : result.code === "canceled" ? { outcome: "canceled" } : {
3324
- outcome: "error",
3325
- code: result.code,
3326
- message: result.message,
3327
- retryable: result.retryable
3328
- };
3471
+ const outcome = result.ok ? { outcome: "ok", text: result.text } : result.code === "canceled" ? { outcome: "canceled" } : (
3472
+ /**
3473
+ * The class, never the text — a8137b5.
3474
+ *
3475
+ * `result.message` is the CLI's own words and stays with the
3476
+ * owner: the event above, the ingress log below, `byollm status`
3477
+ * and Your Devices. It does not travel, for two reasons.
3478
+ *
3479
+ * It quotes the owner's machine — paths, usernames, config
3480
+ * locations, account emails all live in CLI errors, and a
3481
+ * stranger's page is not where those go.
3482
+ *
3483
+ * And it names the service. "the claude CLI is not signed in"
3484
+ * tells a site which model answered, which is the one thing the
3485
+ * disclosure fence exists to prevent — arriving through the error
3486
+ * path because nobody was watching the error path. Every message
3487
+ * named its backend, so every failure leaked what every success
3488
+ * is careful to hide.
3489
+ *
3490
+ * `retryable` still travels: whether to try again is the site's
3491
+ * decision and says nothing about whose machine it was.
3492
+ */
3493
+ {
3494
+ outcome: "error",
3495
+ ...outcomeForSite(result.code),
3496
+ retryable: result.retryable
3497
+ }
3498
+ );
3329
3499
  await this.#options.ingress.recordOutcome({
3330
3500
  at: this.#now(),
3331
3501
  jobId: job.id,
@@ -4154,7 +4324,37 @@ WantedBy=default.target
4154
4324
  deactivate: [["schtasks", "/delete", "/tn", SERVICE_LABEL, "/f"]],
4155
4325
  query: ["schtasks", "/query", "/tn", SERVICE_LABEL],
4156
4326
  logPath,
4157
- supervisor: "Task Scheduler"
4327
+ supervisor: "Task Scheduler",
4328
+ /**
4329
+ * The Startup folder, for a machine that will not register a task.
4330
+ *
4331
+ * A `.cmd` here runs at logon for this user, needs no elevation and no
4332
+ * policy exemption, and cannot be refused. What it does not do is restart
4333
+ * byollm if it stops — which is the whole reason the task XML exists — so
4334
+ * that is said out loud rather than left for somebody to discover when a
4335
+ * crash goes unnoticed.
4336
+ *
4337
+ * `start ""` so the shim exits immediately and the logon does not wait on
4338
+ * a long-running process; the empty title is required because `start`
4339
+ * reads a first quoted argument as the window title.
4340
+ */
4341
+ fallback: {
4342
+ unitPath: join4(
4343
+ process.env["APPDATA"] ?? root,
4344
+ "Microsoft",
4345
+ "Windows",
4346
+ "Start Menu",
4347
+ "Programs",
4348
+ "Startup",
4349
+ "byollm.cmd"
4350
+ ),
4351
+ unitContents: `@echo off\r
4352
+ rem byollm \u2014 run an app's LLM jobs on your own models\r
4353
+ start "" /b "${execPath}" "${scriptPath}" run >> "${logPath}" 2>&1\r
4354
+ `,
4355
+ supervisor: "the Startup folder",
4356
+ caveat: "It starts byollm when you log in. It does not restart it if it stops \u2014 Task Scheduler would have, and this machine would not register the task."
4357
+ }
4158
4358
  };
4159
4359
  }
4160
4360
  async function serviceIsInstalled(target) {
@@ -4201,6 +4401,11 @@ async function serviceState(plan, run2) {
4201
4401
  if (/\bRunning\b/.test(result.output)) return { state: "running" };
4202
4402
  return { state: "installed", detail: "registered, not running" };
4203
4403
  }
4404
+ function refusalOf(command, result) {
4405
+ const said = result.output.trim();
4406
+ const denied = /access is denied|requires elevation|0x80070005/i.test(said);
4407
+ return denied ? `${command.join(" ")} \u2014 this machine will not let you register a scheduled task (no administrator rights, or IT policy).` : `${command.join(" ")} \u2014 exit ${String(result.code)}` + (said === "" ? "" : `: ${said}`);
4408
+ }
4204
4409
  async function installService(target, run2) {
4205
4410
  const plan = servicePlan(target);
4206
4411
  const refusal = refuseToSupervise(target.scriptPath);
@@ -4213,16 +4418,46 @@ async function installService(target, run2) {
4213
4418
  const result = await run2(command);
4214
4419
  const mayFail = plan.platform === "darwin" && index === 0;
4215
4420
  if (result.code !== 0 && !mayFail) {
4421
+ const fallback = plan.fallback;
4422
+ if (fallback !== void 0) {
4423
+ const fell = await mkdir8(dirname9(fallback.unitPath), {
4424
+ recursive: true
4425
+ }).then(
4426
+ () => writeFile8(fallback.unitPath, fallback.unitContents, "utf8")
4427
+ ).then(
4428
+ () => true,
4429
+ () => false
4430
+ );
4431
+ if (fell) {
4432
+ return {
4433
+ ok: true,
4434
+ plan,
4435
+ lines: [
4436
+ `${plan.supervisor} would not register the task, so byollm is`,
4437
+ `set to start from ${fallback.supervisor} instead.`,
4438
+ "",
4439
+ ` ${fallback.caveat}`,
4440
+ "",
4441
+ ` ${refusalOf(command, result)}`,
4442
+ "",
4443
+ ` startup: ${fallback.unitPath}`,
4444
+ ` log: ${plan.logPath}`,
4445
+ ` check: byollm status`,
4446
+ ` remove: byollm uninstall`
4447
+ ]
4448
+ };
4449
+ }
4450
+ }
4216
4451
  return {
4217
4452
  ok: false,
4218
4453
  plan,
4219
4454
  lines: [
4220
4455
  `wrote ${plan.unitPath}, but ${plan.supervisor} refused it:`,
4221
4456
  "",
4222
- ` ${command.join(" ")}`,
4223
- ` exit ${String(result.code)}${result.output.trim() === "" ? "" : `: ${result.output.trim()}`}`,
4457
+ ` ${refusalOf(command, result)}`,
4224
4458
  "",
4225
- `The daemon is not supervised. \`byollm run\` still works in a terminal.`
4459
+ `The daemon is not supervised. \`byollm run\` still works in a terminal,`,
4460
+ `and is the way to keep serving until this is sorted.`
4226
4461
  ]
4227
4462
  };
4228
4463
  }
@@ -4290,6 +4525,95 @@ var spawnCommand = async (command) => {
4290
4525
  });
4291
4526
  };
4292
4527
 
4528
+ // src/service-line.ts
4529
+ function serviceLine(input) {
4530
+ const { service, device, state } = input;
4531
+ switch (state.kind) {
4532
+ case "answers":
4533
+ return { line: `${service} \u2014 ${state.model}` };
4534
+ case "unknown":
4535
+ return { line: `${service} \u2014 ${state.model}` };
4536
+ case "signed-out": {
4537
+ const remedy = input.signIn ?? "sign it in";
4538
+ return {
4539
+ line: `${service} \u2014 needs sign-in on ${device}: ${remedy}`,
4540
+ ...state.detail === void 0 ? {} : { detail: state.detail }
4541
+ };
4542
+ }
4543
+ case "missing": {
4544
+ const remove = input.removeWith ?? `remove it from ~/.byollm/config.json`;
4545
+ return {
4546
+ line: `${service} \u2014 not found on ${device}: install it, or ${remove}`
4547
+ };
4548
+ }
4549
+ }
4550
+ }
4551
+ function renderServices(states, device) {
4552
+ const lines = [];
4553
+ for (const [service, report2] of states) {
4554
+ const said = serviceLine({
4555
+ service,
4556
+ device,
4557
+ state: report2.state,
4558
+ ...report2.signIn === void 0 ? {} : { signIn: report2.signIn }
4559
+ });
4560
+ lines.push(` ${said.line}`);
4561
+ if (said.detail !== void 0) lines.push(` ${said.detail}`);
4562
+ }
4563
+ return lines;
4564
+ }
4565
+ function authNote(input) {
4566
+ const { report: report2 } = input;
4567
+ if (report2 === void 0) return void 0;
4568
+ if (report2.state.kind === "answers") return void 0;
4569
+ if (report2.state.kind === "unknown") return void 0;
4570
+ return serviceLine({
4571
+ service: input.service,
4572
+ device: input.device,
4573
+ state: report2.state,
4574
+ ...report2.signIn === void 0 ? {} : { signIn: report2.signIn }
4575
+ });
4576
+ }
4577
+
4578
+ // src/service-states.ts
4579
+ import { readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
4580
+ import { z as z7 } from "zod";
4581
+ var Stored = z7.record(
4582
+ z7.string(),
4583
+ z7.object({
4584
+ state: z7.discriminatedUnion("kind", [
4585
+ z7.object({ kind: z7.literal("answers"), model: z7.string() }),
4586
+ z7.object({
4587
+ kind: z7.literal("signed-out"),
4588
+ detail: z7.string().optional()
4589
+ }),
4590
+ z7.object({ kind: z7.literal("missing") }),
4591
+ z7.object({ kind: z7.literal("unknown"), model: z7.string() })
4592
+ ]),
4593
+ signIn: z7.string().optional()
4594
+ })
4595
+ );
4596
+ async function writeServiceStates(path, states) {
4597
+ const out = {};
4598
+ for (const [service, report2] of states) out[service] = report2;
4599
+ try {
4600
+ await writeFile9(path, `${JSON.stringify(out, null, 2)}
4601
+ `, "utf8");
4602
+ } catch {
4603
+ }
4604
+ }
4605
+ async function readServiceStates(path) {
4606
+ try {
4607
+ const parsed = Stored.safeParse(
4608
+ JSON.parse(await readFile9(path, "utf8"))
4609
+ );
4610
+ if (!parsed.success) return /* @__PURE__ */ new Map();
4611
+ return new Map(Object.entries(parsed.data));
4612
+ } catch {
4613
+ return /* @__PURE__ */ new Map();
4614
+ }
4615
+ }
4616
+
4293
4617
  // src/cli.ts
4294
4618
  var USAGE = `byollm \u2014 run an app's LLM jobs on your own models.
4295
4619
 
@@ -4435,7 +4759,7 @@ async function commandName(paths, args, io) {
4435
4759
  return 0;
4436
4760
  }
4437
4761
  await mkdir9(dirname10(paths.label), { recursive: true });
4438
- await writeFile9(paths.label, `${next.slice(0, 120)}
4762
+ await writeFile10(paths.label, `${next.slice(0, 120)}
4439
4763
  `, { mode: 384 });
4440
4764
  io.out(
4441
4765
  `This device will pair as ${next.slice(0, 120)}.
@@ -4455,7 +4779,7 @@ function connectTarget(args) {
4455
4779
  async function labelFor(paths, flag) {
4456
4780
  if (flag !== void 0 && flag !== "") return flag.slice(0, 120);
4457
4781
  try {
4458
- const saved = (await readFile9(paths.label, "utf8")).trim();
4782
+ const saved = (await readFile10(paths.label, "utf8")).trim();
4459
4783
  if (saved !== "") return saved.slice(0, 120);
4460
4784
  } catch {
4461
4785
  }
@@ -4524,9 +4848,21 @@ ${wrap(
4524
4848
  ingress
4525
4849
  });
4526
4850
  const capabilities = await runner.detectCapabilities({ canary: true });
4851
+ await writeServiceStates(paths.serviceStates, runner.serviceStates);
4852
+ {
4853
+ const lines = renderServices(
4854
+ runner.serviceStates,
4855
+ await labelFor(paths, name)
4856
+ );
4857
+ if (lines.length > 0) io.out(`
4858
+ services
4859
+ ${lines.join("\n")}
4860
+ `);
4861
+ }
4862
+ await writeServiceStates(paths.serviceStates, runner.serviceStates);
4527
4863
  if (capabilities.length === 0) {
4528
4864
  io.err(
4529
- "\n0 backends are healthy, so nothing will route to this device yet.\nPairing anyway \u2014 set a model server up (docs.byollm.cloud/guides/models),\nthen check `byollm services`. Work starts arriving on its own once one is healthy.\n"
4865
+ "\nNothing will route to this device yet.\nPairing anyway \u2014 work starts arriving on its own once a service can\nanswer. `byollm status` says where each one stands.\n"
4530
4866
  );
4531
4867
  }
4532
4868
  io.out(`
@@ -4634,7 +4970,7 @@ function reportSkipped(pairings, io) {
4634
4970
  async function retireAllowlist(paths, io) {
4635
4971
  let raw;
4636
4972
  try {
4637
- raw = await readFile9(paths.allowlist, "utf8");
4973
+ raw = await readFile10(paths.allowlist, "utf8");
4638
4974
  } catch {
4639
4975
  return;
4640
4976
  }
@@ -4931,6 +5267,8 @@ async function commandStatus(paths, io, service) {
4931
5267
  byService.set(route.service, entry);
4932
5268
  }
4933
5269
  io.out("\nservices\n");
5270
+ const recorded = await readServiceStates(paths.serviceStates);
5271
+ const deviceName = await labelFor(paths, void 0);
4934
5272
  const declared = Object.entries(loaded.config.services);
4935
5273
  if (declared.length === 0) {
4936
5274
  io.out(" (none configured)\n");
@@ -4945,6 +5283,11 @@ async function commandStatus(paths, io, service) {
4945
5283
  hasRelay
4946
5284
  });
4947
5285
  const scope = `${summary.scope} (${summary.audience})` + (summary.narrowedBy === void 0 ? "" : ` \u2014 ${summary.narrowedBy}`);
5286
+ const authLine = authNote({
5287
+ service: name,
5288
+ device: deviceName,
5289
+ report: recorded.get(name)
5290
+ });
4948
5291
  io.out(` ${name}
4949
5292
  `);
4950
5293
  io.out(` ${shown}
@@ -4960,6 +5303,14 @@ async function commandStatus(paths, io, service) {
4960
5303
  ` ${scope} \xB7 ${says.length === 0 ? "not offered \u2014 see the problems below" : says.join(" \xB7 ")}
4961
5304
  `
4962
5305
  );
5306
+ if (authLine !== void 0) {
5307
+ io.out(` ${authLine.line}
5308
+ `);
5309
+ if (authLine.detail !== void 0) {
5310
+ io.out(` ${authLine.detail}
5311
+ `);
5312
+ }
5313
+ }
4963
5314
  }
4964
5315
  for (const held of loaded.withheld) {
4965
5316
  io.out(
@@ -5082,7 +5433,7 @@ function indent(text) {
5082
5433
  async function commandPause(paths, pause, io) {
5083
5434
  await mkdir9(paths.root, { recursive: true });
5084
5435
  if (pause) {
5085
- await writeFile9(paths.pauseFlag, `${(/* @__PURE__ */ new Date()).toISOString()}
5436
+ await writeFile10(paths.pauseFlag, `${(/* @__PURE__ */ new Date()).toISOString()}
5086
5437
  `);
5087
5438
  io.out(
5088
5439
  "paused \u2014 no new work will be claimed. `byollm resume` to start again.\n"
@@ -5178,7 +5529,7 @@ async function commandOffer(paths, args, io) {
5178
5529
  }
5179
5530
  let raw;
5180
5531
  try {
5181
- raw = await readFile9(paths.config, "utf8");
5532
+ raw = await readFile10(paths.config, "utf8");
5182
5533
  } catch {
5183
5534
  io.err(
5184
5535
  `no config at ${paths.config} \u2014 nothing to offer yet.
@@ -5281,7 +5632,7 @@ Offer ${serviceKey} to your team?`
5281
5632
  };
5282
5633
  }
5283
5634
  await mkdir9(dirname10(paths.config), { recursive: true });
5284
- await writeFile9(paths.config, `${JSON.stringify(result.data, null, 2)}
5635
+ await writeFile10(paths.config, `${JSON.stringify(result.data, null, 2)}
5285
5636
  `);
5286
5637
  const written = result.data.services[serviceKey].spend?.dailyCapCents;
5287
5638
  io.out(
@@ -5491,7 +5842,7 @@ Addresses look like https://app.example.com or localhost:8080.
5491
5842
  }
5492
5843
 
5493
5844
  // src/index.ts
5494
- var DAEMON_VERSION = "0.1.0-alpha.63";
5845
+ var DAEMON_VERSION = "0.1.0-alpha.65";
5495
5846
  function daemonVersion() {
5496
5847
  return {
5497
5848
  daemon: DAEMON_VERSION,
@@ -5543,4 +5894,4 @@ export {
5543
5894
  runCli,
5544
5895
  main
5545
5896
  };
5546
- //# sourceMappingURL=chunk-J4GXTVXX.js.map
5897
+ //# sourceMappingURL=chunk-HM5G6ZUG.js.map