llm-relay 0.61.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts CHANGED
@@ -31,7 +31,59 @@ export declare function firstRunPending(configDir?: string): boolean;
31
31
  * nothing in the file says this. A notice beside the JSON reaches a human and an agent both,
32
32
  * and breaks no parser.
33
33
  */
34
- export declare function printFirstRunNotice(configDir?: string): void;
34
+ export declare function printFirstRunNotice(configDir?: string, cfg?: Config): void;
35
+ /**
36
+ * The environment half of the first-run notice: what is installed, and what already has a key.
37
+ *
38
+ * ⚠ **Why this is here and not in `onboard`.** Everything it reports already existed —
39
+ * `presets.ts` has carried a `signupUrl` for every free provider since the beginning, and
40
+ * `printOnboardingGuide()` has printed them. What did not exist was any moment at which a user
41
+ * SAW it: onboarding is pull-only, so it fires when somebody types `llm-relay onboard`, and a
42
+ * first-time user has no reason to guess that command exists. The first-run notice is the one
43
+ * place the relay already speaks unprompted, so the report belongs beside the routing question.
44
+ *
45
+ * ⚠ It reports, and never acts. No key is read aloud, nothing is written, and no provider is
46
+ * added — a notice that changed configuration would be exactly the silent behaviour the first-run
47
+ * marker exists to avoid. `llm-relay onboard` remains the only thing that writes credentials.
48
+ *
49
+ * ⚠ Absence is reported as "not detected", never as "not installed". `installed-hosts.ts` requires
50
+ * positive evidence, so a false there means no evidence was found — the `key-checker.ts`
51
+ * `unverified` rule, applied to tools instead of credentials.
52
+ *
53
+ * Never throws: the notice is advisory, and a detection fault must not break the command it rides
54
+ * on. A caller with no `Config` still gets the host half.
55
+ */
56
+ /** The inputs the report renders. Injected so the rendering is testable without a real machine. */
57
+ export interface FirstRunEnvironmentInputs {
58
+ /** Hosts for which POSITIVE evidence was found. Never a claim about what is absent. */
59
+ detected: readonly {
60
+ readonly label: string;
61
+ }[];
62
+ /** Provider credential status, as `getOnboardingStatusList` reports it. */
63
+ statuses: readonly {
64
+ readonly displayName: string;
65
+ readonly hasKey: boolean;
66
+ readonly signupUrl?: string | undefined;
67
+ }[];
68
+ }
69
+ /**
70
+ * PURE renderer for the environment half of the first-run notice.
71
+ *
72
+ * ⚠ Separated from the IO so it can be tested at all. Adversarial review found the first version
73
+ * had no seam and zero coverage, unlike every sibling in this file — and it was the only part of
74
+ * the notice that makes factual claims about the operator's machine, so it was exactly the part
75
+ * that needed pinning.
76
+ *
77
+ * ⚠ **It makes no claim about ladder membership**, and that is a deliberate correction. The first
78
+ * version printed "none is configured as a lane yet" unconditionally whenever any host was
79
+ * detected — false the moment an operator adds a rung, which is a supported edit that does not
80
+ * clear the first-run marker. Checking properly is not available either: `laneOfCommand`'s closed
81
+ * vocabulary recognises only `agy` and `codex`, so Claude Code and OpenCode would always look
82
+ * unconfigured. Rather than duplicate ladder logic in a notice, or assert what it cannot verify,
83
+ * this points at the surface that DOES know. That is the same "ask the relay, don't guess" rule
84
+ * the skill states for dispatch order.
85
+ */
86
+ export declare function renderFirstRunEnvironment(inputs: FirstRunEnvironmentInputs): string;
35
87
  /** Record that the operator has been asked and has answered. Idempotent; never throws. */
36
88
  export declare function clearFirstRun(configDir?: string): boolean;
37
89
  /**
package/dist/cli.js CHANGED
@@ -501,7 +501,7 @@ export function firstRunPending(configDir) {
501
501
  * nothing in the file says this. A notice beside the JSON reaches a human and an agent both,
502
502
  * and breaks no parser.
503
503
  */
504
- export function printFirstRunNotice(configDir) {
504
+ export function printFirstRunNotice(configDir, cfg) {
505
505
  if (!firstRunPending(configDir))
506
506
  return;
507
507
  process.stderr.write("\nllm-relay: first run — this install has never been steered.\n" +
@@ -510,7 +510,76 @@ export function printFirstRunNotice(configDir) {
510
510
  " llm-relay offload claude on # marked subagents to the free pools\n" +
511
511
  " llm-relay routing subagent <tier> <spec> # choose where each tier lands\n" +
512
512
  " llm-relay routing default <spec> # move the MAIN conversation (deliberate)\n" +
513
- " Then run `llm-relay routing answered` to stop showing this.\n");
513
+ " Then run `llm-relay routing answered` to stop showing this.\n" +
514
+ firstRunEnvironmentReport(cfg));
515
+ }
516
+ /** How many missing providers the first-run notice names before it summarises the rest. */
517
+ const FIRST_RUN_MISSING_SHOWN = 3;
518
+ /**
519
+ * PURE renderer for the environment half of the first-run notice.
520
+ *
521
+ * ⚠ Separated from the IO so it can be tested at all. Adversarial review found the first version
522
+ * had no seam and zero coverage, unlike every sibling in this file — and it was the only part of
523
+ * the notice that makes factual claims about the operator's machine, so it was exactly the part
524
+ * that needed pinning.
525
+ *
526
+ * ⚠ **It makes no claim about ladder membership**, and that is a deliberate correction. The first
527
+ * version printed "none is configured as a lane yet" unconditionally whenever any host was
528
+ * detected — false the moment an operator adds a rung, which is a supported edit that does not
529
+ * clear the first-run marker. Checking properly is not available either: `laneOfCommand`'s closed
530
+ * vocabulary recognises only `agy` and `codex`, so Claude Code and OpenCode would always look
531
+ * unconfigured. Rather than duplicate ladder logic in a notice, or assert what it cannot verify,
532
+ * this points at the surface that DOES know. That is the same "ask the relay, don't guess" rule
533
+ * the skill states for dispatch order.
534
+ */
535
+ export function renderFirstRunEnvironment(inputs) {
536
+ const lines = [];
537
+ if (inputs.detected.length > 0) {
538
+ lines.push(` Detected on this machine: ${inputs.detected.map((h) => h.label).join(", ")}.`);
539
+ lines.push(" `llm-relay dispatch` shows which of these are configured as lanes, and in what order.");
540
+ }
541
+ const statuses = inputs.statuses;
542
+ if (statuses.length > 0) {
543
+ const ready = statuses.filter((s) => s.hasKey);
544
+ const missing = statuses.filter((s) => !s.hasKey && s.signupUrl !== undefined);
545
+ lines.push(` Credentials: ${ready.length} of ${statuses.length} configured providers have a key.`);
546
+ for (const s of missing.slice(0, FIRST_RUN_MISSING_SHOWN)) {
547
+ lines.push(` ${s.displayName} — sign up free: ${s.signupUrl}`);
548
+ }
549
+ if (missing.length > FIRST_RUN_MISSING_SHOWN) {
550
+ lines.push(` …and ${missing.length - FIRST_RUN_MISSING_SHOWN} more.`);
551
+ }
552
+ if (missing.length > 0) {
553
+ lines.push(" `llm-relay onboard` walks through them and saves the keys.");
554
+ }
555
+ }
556
+ return lines.length > 0 ? `\n${lines.join("\n")}\n` : "";
557
+ }
558
+ /**
559
+ * Gather the real inputs and render them.
560
+ *
561
+ * ⚠ **Why this exists at all.** Everything it reports already existed — `presets.ts` has carried a
562
+ * `signupUrl` for every free provider since the beginning, and `printOnboardingGuide()` has printed
563
+ * them. What did not exist was any moment at which a user SAW it: onboarding is pull-only, so it
564
+ * fires when somebody types `llm-relay onboard`, and a first-time user has no reason to guess that
565
+ * command exists. The first-run notice is the one place the relay already speaks unprompted.
566
+ *
567
+ * ⚠ It reports, and never acts. No key is read aloud, nothing is written, and no provider is added.
568
+ * `llm-relay onboard` remains the only thing that writes credentials.
569
+ *
570
+ * Never throws: the notice is advisory, and a detection fault must not break the command it rides on.
571
+ */
572
+ function firstRunEnvironmentReport(cfg) {
573
+ try {
574
+ return renderFirstRunEnvironment({
575
+ detected: detectHosts().filter((h) => h.installed),
576
+ statuses: getOnboardingStatusList(cfg),
577
+ });
578
+ }
579
+ catch {
580
+ // Advisory only. A broken detection must never cost the caller its command.
581
+ return "";
582
+ }
514
583
  }
515
584
  /** Record that the operator has been asked and has answered. Idempotent; never throws. */
516
585
  export function clearFirstRun(configDir) {
@@ -2094,7 +2163,7 @@ export async function runOffload(arg, nextArg) {
2094
2163
  // The other surface an agent reaches for when it is about to steer traffic. Stderr, so the
2095
2164
  // stdout report stays exactly what it was for anything parsing it.
2096
2165
  if (want === null)
2097
- printFirstRunNotice();
2166
+ printFirstRunNotice(undefined, cfg);
2098
2167
  const hostRouting = detectHostRouting();
2099
2168
  if (hostRouting.state === "bypassed" && state.enabled) {
2100
2169
  process.stdout.write(` ⚠ ${hostRouting.reason}\n`);
@@ -2780,7 +2849,7 @@ export function runRoutingCommand() {
2780
2849
  const action = positionals[1] ?? "show";
2781
2850
  if (action === "show" || action === "get") {
2782
2851
  outputJson(cfg.routing);
2783
- printFirstRunNotice();
2852
+ printFirstRunNotice(undefined, cfg);
2784
2853
  return;
2785
2854
  }
2786
2855
  if (action === "default") {
@@ -3055,7 +3124,8 @@ export async function runPools(deps = {}) {
3055
3124
  }
3056
3125
  }
3057
3126
  import { probeAllPools } from "./pool-health.js";
3058
- import { runInteractiveOnboarding } from "./onboarding.js";
3127
+ import { runInteractiveOnboarding, getOnboardingStatusList } from "./onboarding.js";
3128
+ import { detectHosts } from "./installed-hosts.js";
3059
3129
  import { importKeysFromFile } from "./key-import.js";
3060
3130
  import { setupClaudeCli, setupClaudeDesktop } from "./setup-claude.js";
3061
3131
  import { getTelemetryReport } from "./telemetry.js";