@lunaroute/cli 0.2.4 → 0.2.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 (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +241 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,7 +18,9 @@ Requires Node.js 20+.
18
18
  lunaroute setup pi # or: opencode | claude-code | copilot-cli | generic
19
19
  ```
20
20
 
21
- One command from a fresh machine: `setup` asks before touching anything — for `pi` it first offers to install the LunaRoute Pi extension + MCP adapter (recommended — models then auto-sync, and you sign in with `/login lunaroute` inside pi), then offers to start pi for you. `opencode` works the same way with its extension (`/connect` to sign in). Decline an extension — or pick a harness whose config needs a key — and `setup` offers to run `lunaroute login` right there (it opens your browser) before writing. The fallback config finishes with an `export LUNAROUTE_API_KEY=…` line — run it in your shell, then start your agent and it routes through LunaRoute.
21
+ One command from a fresh machine: `setup` asks before touching anything — for `pi` it first offers to install the LunaRoute Pi extension (recommended — models then auto-sync, and you sign in with `/login lunaroute` inside pi), then offers to start pi for you. `opencode` works the same way with its extension (`/connect` to sign in). Decline an extension — or pick a harness whose config needs a key — and `setup` offers to run `lunaroute login` right there (it opens your browser) before writing. The fallback config finishes with an `export LUNAROUTE_API_KEY=…` line — run it in your shell, then start your agent and it routes through LunaRoute.
22
+
23
+ Already have a key from the dashboard? `lunaroute setup pi --key lr_… --yes` is the whole onboarding: it checks the key against the catalog, installs the Pi extension, and stores the key in `~/.pi/agent/auth.json`, so pi starts signed in with models syncing from `/v1/models`. No models.json edit and no shell export. A stale `providers.lunaroute` block from an older CLI is removed in the same run (backup kept). `lunaroute setup opencode --key lr_… --yes` works the same way: it installs the OpenCode extension and stores the key in `~/.local/share/opencode/auth.json`, so opencode starts signed in — no `/connect` needed.
22
24
 
23
25
  Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accepts all prompts, and `--print` previews without writing.
24
26
 
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command, InvalidArgumentError, Option } from "commander";
5
5
 
6
6
  // package.json
7
- var version = "0.2.4";
7
+ var version = "0.2.6";
8
8
 
9
9
  // src/login.ts
10
10
  import { createServer } from "http";
@@ -359,13 +359,26 @@ function logout(profile) {
359
359
  }
360
360
 
361
361
  // src/catalog.ts
362
+ var KeyRejectedError = class extends Error {
363
+ };
364
+ var NON_CHAT_CAPABILITIES = [
365
+ "image_generation",
366
+ "embeddings",
367
+ "rerank"
368
+ ];
369
+ function isChatModel(m) {
370
+ for (const c of NON_CHAT_CAPABILITIES) {
371
+ if (m.capabilities?.[c] === true) return false;
372
+ }
373
+ return true;
374
+ }
362
375
  async function fetchModels(routingUrl, key) {
363
376
  const url = `${routingUrl}/v1/models`;
364
377
  const headers = key ? { Authorization: `Bearer ${key}` } : {};
365
378
  const res = await fetch(url, { method: "GET", headers });
366
379
  if (!res.ok) {
367
380
  if (res.status === 401) {
368
- throw new Error(
381
+ throw new KeyRejectedError(
369
382
  key ? `the routing service rejected that key (HTTP 401) \u2014 check that you copied the whole lr_ key` : `the routing service requires a key for /v1/models (HTTP 401) \u2014 run "lunaroute login" or pass --key`
370
383
  );
371
384
  }
@@ -379,9 +392,9 @@ async function fetchModels(routingUrl, key) {
379
392
  max_output_tokens: m.max_output_tokens,
380
393
  capabilities: m.capabilities,
381
394
  client_compat: m.client_compat
382
- })).filter((m) => m.id.length > 0);
395
+ })).filter((m) => m.id.length > 0 && isChatModel(m));
383
396
  if (models.length === 0) {
384
- throw new Error(`no models available from ${url}`);
397
+ throw new Error(`no chat models available from ${url}`);
385
398
  }
386
399
  return models.sort((a, b) => a.id.localeCompare(b.id));
387
400
  }
@@ -578,12 +591,19 @@ function configHome() {
578
591
  function opencodeConfigPath() {
579
592
  return join2(configHome(), "opencode", "opencode.json");
580
593
  }
594
+ function opencodeAuthPath() {
595
+ const dataHome = process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share");
596
+ return join2(dataHome, "opencode", "auth.json");
597
+ }
581
598
  function openclawConfigPath() {
582
599
  return join2(homedir2(), ".openclaw", "openclaw.json");
583
600
  }
584
601
  function piModelsPath() {
585
602
  return join2(homedir2(), ".pi", "agent", "models.json");
586
603
  }
604
+ function piAuthPath() {
605
+ return join2(homedir2(), ".pi", "agent", "auth.json");
606
+ }
587
607
  var LUNAROUTE_SKILL_NAME = "lunaroute-memory";
588
608
  function claudeUserConfigPath() {
589
609
  return join2(homedir2(), ".claude.json");
@@ -692,17 +712,38 @@ function providerPlan(ctx) {
692
712
  ]
693
713
  };
694
714
  }
715
+ function buildKeyPlan(key) {
716
+ return {
717
+ fileWrites: [
718
+ {
719
+ kind: "json",
720
+ path: opencodeAuthPath(),
721
+ merge: (existing) => {
722
+ const obj = existing ?? {};
723
+ delete obj["lunaroute/"];
724
+ return { ...obj, lunaroute: { type: "api", key } };
725
+ }
726
+ }
727
+ ],
728
+ exports: [],
729
+ notes: [
730
+ "opencode: key stored in ~/.local/share/opencode/auth.json \u2014 start opencode (or restart it), then /models to pick a lunaroute/* model."
731
+ ]
732
+ };
733
+ }
695
734
 
696
735
  // src/setup/adapters/pi.ts
736
+ import { readFileSync as readFileSync3 } from "fs";
697
737
  function buildPiModelEntry(m) {
738
+ if (!m.context_window_tokens || !m.max_output_tokens) return null;
698
739
  const reasoning = m.capabilities?.reasoning === true;
699
740
  const entry = {
700
741
  id: m.id,
701
742
  name: m.display_name ?? m.id,
702
743
  reasoning,
703
744
  input: m.capabilities?.vision ? ["text", "image"] : ["text"],
704
- contextWindow: m.context_window_tokens ?? 0,
705
- maxTokens: m.max_output_tokens ?? 0
745
+ contextWindow: m.context_window_tokens,
746
+ maxTokens: m.max_output_tokens
706
747
  };
707
748
  if (!reasoning) return entry;
708
749
  const pi = m.client_compat?.pi;
@@ -718,13 +759,32 @@ function buildPiModelEntry(m) {
718
759
  return entry;
719
760
  }
720
761
  function buildPlan2(ctx) {
762
+ const skipped = [];
763
+ const models = [];
764
+ for (const m of ctx.models) {
765
+ const entry = buildPiModelEntry(m);
766
+ if (entry) models.push(entry);
767
+ else skipped.push(m.id);
768
+ }
769
+ if (models.length === 0) {
770
+ throw new Error(
771
+ `no LunaRoute model has a context window in the catalog${skipped.length ? ` (skipped: ${skipped.join(", ")})` : ""}`
772
+ );
773
+ }
721
774
  const block = {
722
775
  baseUrl: `${ctx.routingUrl}/v1`,
723
776
  api: "openai-completions",
724
777
  // Real key; rides the native Authorization: Bearer header.
725
778
  apiKey: `$${ctx.keyEnvVar}`,
726
- models: ctx.models.map(buildPiModelEntry)
779
+ models
727
780
  };
781
+ const notes = [
782
+ "\npi: open /model to pick a LunaRoute model (models.json hot-reloads).",
783
+ "Tip: the lunaroute-pi-extension auto-registers models from /v1/models so you can skip this file \u2014 `pi install npm:@lunaroute/pi-extension` then `/login lunaroute`."
784
+ ];
785
+ if (skipped.length > 0) {
786
+ notes.push(`Skipped (no context window in the catalog): ${skipped.join(", ")}`);
787
+ }
728
788
  return {
729
789
  fileWrites: [
730
790
  {
@@ -739,12 +799,60 @@ function buildPlan2(ctx) {
739
799
  }
740
800
  ],
741
801
  exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
802
+ notes
803
+ };
804
+ }
805
+ function stalePiProviderExists() {
806
+ try {
807
+ const obj = JSON.parse(readFileSync3(piModelsPath(), "utf8"));
808
+ return Boolean(obj.providers?.lunaroute);
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
813
+ function buildStaleProviderCleanupPlan() {
814
+ return {
815
+ fileWrites: [
816
+ {
817
+ kind: "json",
818
+ path: piModelsPath(),
819
+ merge: (existing) => {
820
+ const obj = existing ?? {};
821
+ const providers = { ...obj.providers };
822
+ delete providers.lunaroute;
823
+ const next = { ...obj };
824
+ if (Object.keys(providers).length > 0) next.providers = providers;
825
+ else delete next.providers;
826
+ return next;
827
+ }
828
+ }
829
+ ],
830
+ exports: [],
742
831
  notes: [
743
- "\npi: open /model to pick a LunaRoute model (models.json hot-reloads).",
744
- "Tip: the lunaroute-pi-extension auto-registers models from /v1/models so you can skip this file \u2014 `pi install npm:@lunaroute/pi-extension` then `/login lunaroute`."
832
+ "pi: removed the stale providers.lunaroute block from ~/.pi/agent/models.json (backup kept) \u2014 the extension syncs models live."
745
833
  ]
746
834
  };
747
835
  }
836
+ var KEY_EXPIRES_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
837
+ function buildKeyPlan2(key) {
838
+ return {
839
+ fileWrites: [
840
+ {
841
+ kind: "json",
842
+ path: piAuthPath(),
843
+ merge: (existing) => {
844
+ const obj = existing ?? {};
845
+ return {
846
+ ...obj,
847
+ lunaroute: { type: "oauth", access: key, refresh: "", expires: Date.now() + KEY_EXPIRES_MS }
848
+ };
849
+ }
850
+ }
851
+ ],
852
+ exports: [],
853
+ notes: ["pi: key stored in ~/.pi/agent/auth.json \u2014 start pi, then /model to pick a lunaroute/* model."]
854
+ };
855
+ }
748
856
 
749
857
  // src/setup/adapters/claudeCode.ts
750
858
  function buildPlan3(ctx) {
@@ -911,7 +1019,8 @@ var ADAPTERS = {
911
1019
  "copilot-cli": buildPlan4,
912
1020
  generic: buildPlan5
913
1021
  };
914
- var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension", "npm:pi-mcp-adapter"];
1022
+ var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension"];
1023
+ var PI_INSTALL_HINT = "Install pi first: npm install -g --ignore-scripts @earendil-works/pi-coding-agent (https://pi.dev)";
915
1024
  async function runSetup(harness, opts, deps = {
916
1025
  confirm,
917
1026
  spawn: makeRealSpawn(),
@@ -921,6 +1030,10 @@ async function runSetup(harness, opts, deps = {
921
1030
  console.error("Choose one of --extension or --models.");
922
1031
  return 1;
923
1032
  }
1033
+ if (opts.key && (opts.extension || opts.models)) {
1034
+ console.error("--key runs the full pi or opencode flow; drop --extension/--models.");
1035
+ return 1;
1036
+ }
924
1037
  const adapter = ADAPTERS[harness];
925
1038
  if (!adapter && harness !== "hermes") {
926
1039
  console.error(
@@ -930,22 +1043,11 @@ async function runSetup(harness, opts, deps = {
930
1043
  }
931
1044
  let stored = loadProfile(opts.profile);
932
1045
  const settings = resolveSettings(opts.profile);
933
- if (opts.key) {
934
- if (!LR_KEY_RE.test(opts.key)) {
935
- console.error(
936
- "Invalid key: a LunaRoute routing key starts with lr_ (copy the whole key from the dashboard)."
937
- );
938
- return 1;
939
- }
940
- saveProfile(opts.profile, {
941
- api_url: settings.api_url,
942
- routing_url: opts.routingUrl || settings.routing_url,
943
- front_url: settings.front_url,
944
- org_id: stored?.org_id ?? "",
945
- user_email: stored?.user_email ?? "",
946
- routing_key: opts.key
947
- });
948
- stored = loadProfile(opts.profile);
1046
+ if (opts.key && !LR_KEY_RE.test(opts.key)) {
1047
+ console.error(
1048
+ "Invalid key: a LunaRoute routing key starts with lr_ (copy the whole key from the dashboard)."
1049
+ );
1050
+ return 1;
949
1051
  }
950
1052
  const routingUrlRaw = opts.routingUrl || stored?.routing_url || settings.routing_url;
951
1053
  if (!routingUrlRaw) {
@@ -959,6 +1061,29 @@ async function runSetup(harness, opts, deps = {
959
1061
  console.error(err instanceof Error ? err.message : err);
960
1062
  return 1;
961
1063
  }
1064
+ if (opts.key) {
1065
+ try {
1066
+ await fetchModels(routingUrl, opts.key);
1067
+ } catch (err) {
1068
+ if (err instanceof KeyRejectedError) {
1069
+ console.error(err.message);
1070
+ return 1;
1071
+ }
1072
+ console.warn(`Note: ${err instanceof Error ? err.message : err}`);
1073
+ console.warn(
1074
+ "Continuing without a synced catalog. If your organization has no chat models enabled yet, contact hello@lunaroute.com \u2014 models appear on the next refresh once they are on."
1075
+ );
1076
+ }
1077
+ saveProfile(opts.profile, {
1078
+ api_url: settings.api_url,
1079
+ routing_url: opts.routingUrl || settings.routing_url,
1080
+ front_url: settings.front_url,
1081
+ org_id: stored?.org_id ?? "",
1082
+ user_email: stored?.user_email ?? "",
1083
+ routing_key: opts.key
1084
+ });
1085
+ stored = loadProfile(opts.profile);
1086
+ }
962
1087
  if (harness === "pi" && !opts.print) {
963
1088
  return runPiSetup(opts, stored, routingUrl, deps);
964
1089
  }
@@ -987,7 +1112,7 @@ async function runSetup(harness, opts, deps = {
987
1112
  };
988
1113
  let plan;
989
1114
  try {
990
- plan = adapter(ctx);
1115
+ plan = harness === "pi" && opts.key ? buildKeyPlan2("<your-api-key>") : adapter(ctx);
991
1116
  } catch (err) {
992
1117
  console.error(err instanceof Error ? err.message : err);
993
1118
  return 1;
@@ -1043,38 +1168,36 @@ async function runPiSetup(opts, stored, routingUrl, deps) {
1043
1168
  if (opts.key) {
1044
1169
  const creds2 = await requireCreds(opts, deps, stored);
1045
1170
  if (!creds2) return 1;
1046
- let models = [];
1047
- let modelsSynced = true;
1048
- try {
1049
- models = await fetchModels(routingUrl, creds2.routing_key);
1050
- } catch (err) {
1051
- modelsSynced = false;
1052
- console.warn(
1053
- `Note: ${err instanceof Error ? err.message : err}`
1054
- );
1055
- console.warn(
1056
- "Writing the provider block with no models. Your organization has no models enabled yet \u2014 contact hello@lunaroute.com to get them turned on, then re-run this command to sync them."
1171
+ const install = await deps.confirm(
1172
+ "Install the LunaRoute Pi extension via 'pi install'?",
1173
+ { yes: opts.yes }
1174
+ );
1175
+ if (!install) {
1176
+ return applyPiModels(
1177
+ creds2,
1178
+ routingUrl,
1179
+ await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", { yes: opts.yes })
1057
1180
  );
1058
1181
  }
1059
- const plan = buildPlan2({
1060
- routingUrl,
1061
- orgId: creds2.org_id,
1062
- models,
1063
- keyEnvVar: "LUNAROUTE_API_KEY"
1064
- });
1065
- const write = await deps.confirm(
1066
- "Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?",
1182
+ const code = await installPiPackages(deps);
1183
+ if (code !== 0) return code;
1184
+ const store = await deps.confirm(
1185
+ "Store the key in ~/.pi/agent/auth.json (replaces any existing LunaRoute login; other credentials preserved)?",
1067
1186
  { yes: opts.yes }
1068
1187
  );
1069
- const code = await applyAndReport(plan, creds2.routing_key, write);
1070
- if (code === 0 && modelsSynced) {
1071
- console.log("\nDone. Start pi \u2014 it is signed in. Pick a model with /model.");
1188
+ if (!store) {
1189
+ console.log("\nSkipped. Inside pi, run /login lunaroute to sign in.");
1190
+ return 0;
1072
1191
  }
1073
- return code;
1192
+ const plan = combinePlans(buildKeyPlan2(creds2.routing_key), stalePiProviderExists() ? buildStaleProviderCleanupPlan() : null);
1193
+ const wcode = await applyAndReport(plan, creds2.routing_key, true);
1194
+ if (wcode !== 0) return wcode;
1195
+ console.log("\nDone. Start pi \u2014 it is signed in. Pick a model with /model.");
1196
+ return 0;
1074
1197
  }
1075
1198
  if (opts.extension) {
1076
1199
  const ok = await deps.confirm(
1077
- "Install the LunaRoute Pi extension + MCP adapter via 'pi install'?",
1200
+ "Install the LunaRoute Pi extension via 'pi install'?",
1078
1201
  { yes: opts.yes }
1079
1202
  );
1080
1203
  if (!ok) {
@@ -1094,7 +1217,7 @@ async function runPiSetup(opts, stored, routingUrl, deps) {
1094
1217
  );
1095
1218
  }
1096
1219
  if (await deps.confirm(
1097
- "Install the LunaRoute Pi extension + MCP adapter via 'pi install' (recommended \u2014 auto-registers models)?",
1220
+ "Install the LunaRoute Pi extension via 'pi install' (recommended \u2014 auto-registers models)?",
1098
1221
  { yes: opts.yes }
1099
1222
  )) {
1100
1223
  return installPiExtension(deps, opts);
@@ -1191,11 +1314,48 @@ async function runHermesConfigSets(spawn, routingUrl, key) {
1191
1314
  async function runOpencodeSetup(stored, settings, routingUrl, opts, deps) {
1192
1315
  try {
1193
1316
  if (opts.print) {
1317
+ const plan = combinePlans(
1318
+ buildPlan(
1319
+ { routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
1320
+ { extension: true }
1321
+ ),
1322
+ opts.key ? buildKeyPlan("<your-api-key>") : null
1323
+ );
1324
+ return applyAndReport(plan, stored?.routing_key ?? "", false);
1325
+ }
1326
+ if (opts.key) {
1327
+ const creds2 = await requireCreds(opts, deps, stored);
1328
+ if (!creds2) return 1;
1329
+ const install = await deps.confirm("Install the LunaRoute OpenCode extension?", {
1330
+ yes: opts.yes
1331
+ });
1332
+ if (!install) {
1333
+ return applyOpencodeProvider(
1334
+ creds2,
1335
+ routingUrl,
1336
+ await deps.confirm("Merge LunaRoute provider into opencode.json (backup kept, other providers preserved)?", {
1337
+ yes: opts.yes
1338
+ })
1339
+ );
1340
+ }
1194
1341
  const plan = buildPlan(
1195
1342
  { routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
1196
1343
  { extension: true }
1197
1344
  );
1198
- return applyAndReport(plan, stored?.routing_key ?? "", false);
1345
+ const code = await applyAndReport(plan, creds2.routing_key, true);
1346
+ if (code !== 0) return code;
1347
+ const store = await deps.confirm(
1348
+ "Store the key in ~/.local/share/opencode/auth.json (replaces any existing LunaRoute login; other credentials preserved)?",
1349
+ { yes: opts.yes }
1350
+ );
1351
+ if (!store) {
1352
+ console.log("\nSkipped. Inside opencode, run /connect to sign in.");
1353
+ return 0;
1354
+ }
1355
+ const wcode = await applyAndReport(buildKeyPlan(creds2.routing_key), creds2.routing_key, true);
1356
+ if (wcode !== 0) return wcode;
1357
+ console.log("\nDone. Start opencode \u2014 it is signed in. Pick a model with /models.");
1358
+ return 0;
1199
1359
  }
1200
1360
  if (await deps.confirm(
1201
1361
  "Install the LunaRoute OpenCode extension (recommended \u2014 /connect login, models auto-sync)?",
@@ -1249,6 +1409,14 @@ async function applyOpencodeProvider(creds, routingUrl, write) {
1249
1409
  });
1250
1410
  return applyAndReport(plan, creds.routing_key, write);
1251
1411
  }
1412
+ function combinePlans(a, b) {
1413
+ if (!b) return a;
1414
+ return {
1415
+ fileWrites: [...a.fileWrites, ...b.fileWrites],
1416
+ exports: [...a.exports, ...b.exports],
1417
+ notes: [...a.notes, ...b.notes]
1418
+ };
1419
+ }
1252
1420
  async function applyAndReport(plan, key, write) {
1253
1421
  try {
1254
1422
  const summary = await applyPlan(plan, { print: !write, key });
@@ -1280,16 +1448,12 @@ async function launchOpencode(spawn) {
1280
1448
  });
1281
1449
  });
1282
1450
  }
1283
- async function installPiExtension(deps, opts) {
1451
+ async function installPiPackages(deps) {
1284
1452
  for (const pkg of PI_INSTALL_PACKAGES) {
1285
1453
  const code = await new Promise((resolve) => {
1286
1454
  const child = deps.spawn("pi", ["install", pkg]);
1287
1455
  child.on("error", (err) => {
1288
- const { message, exitCode } = describeSpawnError(
1289
- "pi",
1290
- "Install pi first: https://pi.dev",
1291
- err
1292
- );
1456
+ const { message, exitCode } = describeSpawnError("pi", PI_INSTALL_HINT, err);
1293
1457
  console.error(message);
1294
1458
  resolve(exitCode);
1295
1459
  });
@@ -1300,11 +1464,26 @@ async function installPiExtension(deps, opts) {
1300
1464
  if (code !== 0) {
1301
1465
  const done = PI_INSTALL_PACKAGES.slice(0, PI_INSTALL_PACKAGES.indexOf(pkg));
1302
1466
  console.error(
1303
- `'pi install ${pkg}' failed (exit ${code}). Installed so far: ${done.length ? done.join(", ") : "nothing"}. Re-run 'lunaroute setup pi --extension' to retry.`
1467
+ `'pi install ${pkg}' failed (exit ${code}). Installed so far: ${done.length ? done.join(", ") : "nothing"}. Re-run the same command to retry.`
1304
1468
  );
1305
1469
  return code || 1;
1306
1470
  }
1307
1471
  }
1472
+ return 0;
1473
+ }
1474
+ async function installPiExtension(deps, opts) {
1475
+ const code = await installPiPackages(deps);
1476
+ if (code !== 0) return code;
1477
+ if (stalePiProviderExists()) {
1478
+ const remove = await deps.confirm(
1479
+ "Remove the stale LunaRoute block from ~/.pi/agent/models.json (backup kept, other providers preserved)?",
1480
+ { yes: opts.yes }
1481
+ );
1482
+ if (remove) {
1483
+ const ccode = await applyAndReport(buildStaleProviderCleanupPlan(), "", true);
1484
+ if (ccode !== 0) return ccode;
1485
+ }
1486
+ }
1308
1487
  if (await deps.confirm("Start pi now? (then run /login lunaroute to sign in)", { yes: opts.yes })) {
1309
1488
  return launchPi(deps.spawn);
1310
1489
  }
@@ -1317,11 +1496,7 @@ async function launchPi(spawn) {
1317
1496
  return new Promise((resolve) => {
1318
1497
  const child = spawn("pi", []);
1319
1498
  child.on("error", (err) => {
1320
- const { message, exitCode } = describeSpawnError(
1321
- "pi",
1322
- "Install pi first: https://pi.dev",
1323
- err
1324
- );
1499
+ const { message, exitCode } = describeSpawnError("pi", PI_INSTALL_HINT, err);
1325
1500
  console.error(message);
1326
1501
  resolve(exitCode);
1327
1502
  });
@@ -1991,7 +2166,7 @@ program.command("whoami").description("Show the signed-in user and organization.
1991
2166
  program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
1992
2167
  logout(program.opts().profile);
1993
2168
  });
1994
- program.command("setup <harness>").description("Configure a coding harness (opencode | pi | hermes | claude-code | copilot-cli | openclaw | generic).").option("--print", "print the config instead of writing files", false).option("--routing-url <url>", "override the routing base URL").option("--yes", "accept all confirmation prompts without a TTY (for scripts)", false).option("--extension", "pi only: jump straight to installing the Pi extension + MCP adapter").option("--models", "pi only: jump straight to writing the models.json provider block").option("--key <key>", "a LunaRoute routing key (lr_\u2026), e.g. from the dashboard's onboarding page \u2014 stores it and skips the browser login").action(async (harness, opts) => {
2169
+ program.command("setup <harness>").description("Configure a coding harness (opencode | pi | hermes | claude-code | copilot-cli | openclaw | generic).").option("--print", "print the config instead of writing files", false).option("--routing-url <url>", "override the routing base URL").option("--yes", "accept all confirmation prompts without a TTY (for scripts)", false).option("--extension", "pi only: jump straight to installing the Pi extension").option("--models", "pi only: jump straight to writing the models.json provider block").option("--key <key>", "a LunaRoute routing key (lr_\u2026), e.g. from the dashboard's onboarding page \u2014 stores it and skips the browser login").action(async (harness, opts) => {
1995
2170
  const code = await runSetup(harness, {
1996
2171
  profile: program.opts().profile,
1997
2172
  print: opts.print,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunaroute/cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "LunaRoute CLI — configure coding harnesses and manage your LunaRoute account.",
5
5
  "repository": {
6
6
  "type": "git",