@lunaroute/cli 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +152 -69
  2. package/package.json +3 -1
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.2";
7
+ var version = "0.2.4";
8
8
 
9
9
  // src/login.ts
10
10
  import { createServer } from "http";
@@ -359,10 +359,16 @@ function logout(profile) {
359
359
  }
360
360
 
361
361
  // src/catalog.ts
362
- async function fetchModels(routingUrl) {
362
+ async function fetchModels(routingUrl, key) {
363
363
  const url = `${routingUrl}/v1/models`;
364
- const res = await fetch(url, { method: "GET" });
364
+ const headers = key ? { Authorization: `Bearer ${key}` } : {};
365
+ const res = await fetch(url, { method: "GET", headers });
365
366
  if (!res.ok) {
367
+ if (res.status === 401) {
368
+ throw new Error(
369
+ 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
+ );
371
+ }
366
372
  throw new Error(`failed to fetch models: HTTP ${res.status} from ${url}`);
367
373
  }
368
374
  const body = await res.json();
@@ -536,8 +542,31 @@ async function promptInput(question, opts = {}) {
536
542
  return value === "" && opts.default !== void 0 ? opts.default : value;
537
543
  }
538
544
 
539
- // src/commands/setup.ts
540
- import { spawn } from "child_process";
545
+ // src/spawn.ts
546
+ import { spawn as nodeSpawn } from "child_process";
547
+ import { spawn as crossSpawn } from "cross-spawn";
548
+ function makeRealSpawn(platform = process.platform) {
549
+ if (platform === "win32") {
550
+ return (command, args, env) => crossSpawn(command, args, { stdio: "inherit", env });
551
+ }
552
+ return (command, args, env) => nodeSpawn(command, args, { stdio: "inherit", env });
553
+ }
554
+ function describeSpawnError(command, installHint, err) {
555
+ const code = err?.code;
556
+ if (code === "ENOENT") {
557
+ return {
558
+ message: `Error: "${command}" not found on PATH. ${installHint} (exit 127)`,
559
+ exitCode: 127
560
+ };
561
+ }
562
+ if (code === "EINVAL") {
563
+ return {
564
+ message: `Error: "${command}" could not be launched \u2014 Node on Windows refuses to execute npm .cmd shims directly (EINVAL). The tool is likely installed; try running it manually. (exit 126)`,
565
+ exitCode: 126
566
+ };
567
+ }
568
+ return { message: err instanceof Error ? err.message : String(err), exitCode: 1 };
569
+ }
541
570
 
542
571
  // src/setup/paths.ts
543
572
  import { execSync } from "child_process";
@@ -885,7 +914,7 @@ var ADAPTERS = {
885
914
  var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension", "npm:pi-mcp-adapter"];
886
915
  async function runSetup(harness, opts, deps = {
887
916
  confirm,
888
- spawn: (command, args) => spawn(command, args, { stdio: "inherit" }),
917
+ spawn: makeRealSpawn(),
889
918
  runLogin
890
919
  }) {
891
920
  if (harness === "pi" && opts.extension && opts.models) {
@@ -899,8 +928,25 @@ async function runSetup(harness, opts, deps = {
899
928
  );
900
929
  return 1;
901
930
  }
902
- const stored = loadProfile(opts.profile);
931
+ let stored = loadProfile(opts.profile);
903
932
  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);
949
+ }
904
950
  const routingUrlRaw = opts.routingUrl || stored?.routing_url || settings.routing_url;
905
951
  if (!routingUrlRaw) {
906
952
  console.error("No routing URL in profile; pass --routing-url.");
@@ -926,10 +972,12 @@ async function runSetup(harness, opts, deps = {
926
972
  }
927
973
  let models;
928
974
  try {
929
- models = await fetchModels(routingUrl);
975
+ models = await fetchModels(routingUrl, creds.routing_key);
930
976
  } catch (err) {
931
- console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
932
- return 1;
977
+ console.error(
978
+ `Could not fetch the model catalog (${err instanceof Error ? err.message : err}) \u2014 continuing; model ids show as <model> until it answers.`
979
+ );
980
+ models = [];
933
981
  }
934
982
  const ctx = {
935
983
  routingUrl,
@@ -958,6 +1006,7 @@ Wrote: ${summary.written.join(", ")}`);
958
1006
  }
959
1007
  }
960
1008
  var NOT_LOGGED_IN = 'Not logged in. Run "lunaroute login" first.';
1009
+ var LR_KEY_RE = /^lr_[A-Za-z0-9_-]{8,128}$/;
961
1010
  var OPENCODE_LOGIN_HINT = "Or re-run and choose the extension \u2014 it logs in via /connect inside opencode.";
962
1011
  var PI_LOGIN_HINT = "Or re-run and choose the extension \u2014 login happens via /login lunaroute inside pi.";
963
1012
  async function requireCreds(opts, deps, stored, hint) {
@@ -991,6 +1040,38 @@ async function requireCreds(opts, deps, stored, hint) {
991
1040
  }
992
1041
  async function runPiSetup(opts, stored, routingUrl, deps) {
993
1042
  try {
1043
+ if (opts.key) {
1044
+ const creds2 = await requireCreds(opts, deps, stored);
1045
+ 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."
1057
+ );
1058
+ }
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)?",
1067
+ { yes: opts.yes }
1068
+ );
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.");
1072
+ }
1073
+ return code;
1074
+ }
994
1075
  if (opts.extension) {
995
1076
  const ok = await deps.confirm(
996
1077
  "Install the LunaRoute Pi extension + MCP adapter via 'pi install'?",
@@ -1075,23 +1156,20 @@ function hermesConfigSetArgs(routingUrl, key) {
1075
1156
  ["config", "set", "LUNAROUTE_API_KEY", key]
1076
1157
  ];
1077
1158
  }
1078
- async function runHermesConfigSets(spawn3, routingUrl, key) {
1159
+ async function runHermesConfigSets(spawn, routingUrl, key) {
1079
1160
  const commands = hermesConfigSetArgs(routingUrl, key);
1080
1161
  const setKeys = commands.map((c) => c[2]);
1081
1162
  for (let i = 0; i < commands.length; i++) {
1082
1163
  const code = await new Promise((resolve) => {
1083
- const child = spawn3("hermes", commands[i]);
1164
+ const child = spawn("hermes", commands[i]);
1084
1165
  child.on("error", (err) => {
1085
- const e = err;
1086
- if (e?.code === "ENOENT") {
1087
- console.error(
1088
- `Error: "hermes" not found on PATH. Install Hermes Agent first: https://hermes-agent.nousresearch.com (exit 127)`
1089
- );
1090
- resolve(127);
1091
- return;
1092
- }
1093
- console.error(err instanceof Error ? err.message : String(err));
1094
- resolve(1);
1166
+ const { message, exitCode } = describeSpawnError(
1167
+ "hermes",
1168
+ "Install Hermes Agent first: https://hermes-agent.nousresearch.com",
1169
+ err
1170
+ );
1171
+ console.error(message);
1172
+ resolve(exitCode);
1095
1173
  });
1096
1174
  child.on("exit", (code2) => {
1097
1175
  resolve(typeof code2 === "number" ? code2 : 1);
@@ -1158,7 +1236,7 @@ async function runOpencodeSetup(stored, settings, routingUrl, opts, deps) {
1158
1236
  async function applyOpencodeProvider(creds, routingUrl, write) {
1159
1237
  let models;
1160
1238
  try {
1161
- models = await fetchModels(routingUrl);
1239
+ models = await fetchModels(routingUrl, creds.routing_key);
1162
1240
  } catch (err) {
1163
1241
  console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
1164
1242
  return 1;
@@ -1185,18 +1263,17 @@ Wrote: ${summary.written.join(", ")}`);
1185
1263
  return 1;
1186
1264
  }
1187
1265
  }
1188
- async function launchOpencode(spawn3) {
1266
+ async function launchOpencode(spawn) {
1189
1267
  return new Promise((resolve) => {
1190
- const child = spawn3("opencode", []);
1268
+ const child = spawn("opencode", []);
1191
1269
  child.on("error", (err) => {
1192
- const e = err;
1193
- if (e?.code === "ENOENT") {
1194
- console.error(`Error: "opencode" not found on PATH. Install OpenCode first: https://opencode.ai (exit 127)`);
1195
- resolve(127);
1196
- return;
1197
- }
1198
- console.error(err instanceof Error ? err.message : String(err));
1199
- resolve(1);
1270
+ const { message, exitCode } = describeSpawnError(
1271
+ "opencode",
1272
+ "Install OpenCode first: https://opencode.ai",
1273
+ err
1274
+ );
1275
+ console.error(message);
1276
+ resolve(exitCode);
1200
1277
  });
1201
1278
  child.on("exit", (code) => {
1202
1279
  resolve(typeof code === "number" ? code : 1);
@@ -1208,14 +1285,13 @@ async function installPiExtension(deps, opts) {
1208
1285
  const code = await new Promise((resolve) => {
1209
1286
  const child = deps.spawn("pi", ["install", pkg]);
1210
1287
  child.on("error", (err) => {
1211
- const e = err;
1212
- if (e?.code === "ENOENT") {
1213
- console.error(`Error: "pi" not found on PATH. Install pi first: https://pi.dev (exit 127)`);
1214
- resolve(127);
1215
- return;
1216
- }
1217
- console.error(err instanceof Error ? err.message : String(err));
1218
- resolve(1);
1288
+ const { message, exitCode } = describeSpawnError(
1289
+ "pi",
1290
+ "Install pi first: https://pi.dev",
1291
+ err
1292
+ );
1293
+ console.error(message);
1294
+ resolve(exitCode);
1219
1295
  });
1220
1296
  child.on("exit", (code2) => {
1221
1297
  resolve(typeof code2 === "number" ? code2 : 1);
@@ -1237,18 +1313,17 @@ async function installPiExtension(deps, opts) {
1237
1313
  console.log(" 2. /model \u2014 pick a lunaroute/* model (models auto-sync from /v1/models).");
1238
1314
  return 0;
1239
1315
  }
1240
- async function launchPi(spawn3) {
1316
+ async function launchPi(spawn) {
1241
1317
  return new Promise((resolve) => {
1242
- const child = spawn3("pi", []);
1318
+ const child = spawn("pi", []);
1243
1319
  child.on("error", (err) => {
1244
- const e = err;
1245
- if (e?.code === "ENOENT") {
1246
- console.error(`Error: "pi" not found on PATH. Install pi first: https://pi.dev (exit 127)`);
1247
- resolve(127);
1248
- return;
1249
- }
1250
- console.error(err instanceof Error ? err.message : String(err));
1251
- resolve(1);
1320
+ const { message, exitCode } = describeSpawnError(
1321
+ "pi",
1322
+ "Install pi first: https://pi.dev",
1323
+ err
1324
+ );
1325
+ console.error(message);
1326
+ resolve(exitCode);
1252
1327
  });
1253
1328
  child.on("exit", (code) => {
1254
1329
  resolve(typeof code === "number" ? code : 1);
@@ -1258,7 +1333,7 @@ async function launchPi(spawn3) {
1258
1333
  async function applyPiModels(creds, routingUrl, write) {
1259
1334
  let models;
1260
1335
  try {
1261
- models = await fetchModels(routingUrl);
1336
+ models = await fetchModels(routingUrl, creds.routing_key);
1262
1337
  } catch (err) {
1263
1338
  console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
1264
1339
  return 1;
@@ -1299,6 +1374,8 @@ async function runModels(profile, opts) {
1299
1374
  }
1300
1375
  if (opts.json) {
1301
1376
  console.log(JSON.stringify(ids, null, 2));
1377
+ } else if (ids.length === 0) {
1378
+ console.log("// no models enabled for your org yet \u2014 contact hello@lunaroute.com to get models enabled");
1302
1379
  } else {
1303
1380
  for (const id of ids) console.log(id);
1304
1381
  }
@@ -1333,6 +1410,10 @@ async function runPricing(profile, opts) {
1333
1410
  console.log(JSON.stringify(rows, null, 2));
1334
1411
  return 0;
1335
1412
  }
1413
+ if (rows.length === 0) {
1414
+ console.log("// no models enabled for your org yet \u2014 contact hello@lunaroute.com to get models enabled");
1415
+ return 0;
1416
+ }
1336
1417
  const cell = (n) => n === void 0 ? "\u2014" : String(n);
1337
1418
  const table = renderTable(
1338
1419
  ["MODEL", "INPUT (cr/M)", "OUTPUT (cr/M)", "CACHED (cr/M)"],
@@ -1367,6 +1448,9 @@ async function runUsage(profile, opts) {
1367
1448
  console.log(
1368
1449
  `Wallet: available ${w.available_credits} (balance ${w.balance_credits}, reserved ${w.reserved_credits})`
1369
1450
  );
1451
+ if (w.available_credits === 0) {
1452
+ console.log("// wallet is empty \u2014 add credits in the dashboard \u2192 Billing");
1453
+ }
1370
1454
  console.log("");
1371
1455
  const table = renderTable(
1372
1456
  ["TIME", "MODEL", "IN", "OUT", "CACHED", "\u0394CREDITS", "BALANCE"],
@@ -1791,9 +1875,6 @@ async function runSkillInstall(profile, opts) {
1791
1875
  return 0;
1792
1876
  }
1793
1877
 
1794
- // src/commands/run.ts
1795
- import { spawn as spawn2 } from "child_process";
1796
-
1797
1878
  // src/run/adapters/claudeCode.ts
1798
1879
  function buildRunSpec(ctx) {
1799
1880
  return {
@@ -1840,7 +1921,7 @@ var INSTALL_HINT = {
1840
1921
  codex: "Install Codex: https://github.com/openai/codex#install"
1841
1922
  };
1842
1923
  var realDeps = {
1843
- spawn: (command, args, env) => spawn2(command, args, { env, stdio: "inherit" }),
1924
+ spawn: makeRealSpawn(),
1844
1925
  fetchModels
1845
1926
  };
1846
1927
  async function runRun(harness, opts, deps = realDeps) {
@@ -1858,7 +1939,7 @@ async function runRun(harness, opts, deps = realDeps) {
1858
1939
  if (!model) {
1859
1940
  let models;
1860
1941
  try {
1861
- models = await deps.fetchModels(creds.routing_url);
1942
+ models = await deps.fetchModels(creds.routing_url, creds.routing_key);
1862
1943
  } catch (err) {
1863
1944
  console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
1864
1945
  return 1;
@@ -1880,16 +1961,9 @@ async function runRun(harness, opts, deps = realDeps) {
1880
1961
  const child = deps.spawn(spec.command, args, env);
1881
1962
  return new Promise((resolve) => {
1882
1963
  child.on("error", (err) => {
1883
- const e = err;
1884
- if (e?.code === "ENOENT") {
1885
- console.error(
1886
- `Error: "${spec.command}" not found on PATH. ${INSTALL_HINT[harness]} (exit 127)`
1887
- );
1888
- resolve(127);
1889
- return;
1890
- }
1891
- console.error(err instanceof Error ? err.message : String(err));
1892
- resolve(1);
1964
+ const { message, exitCode } = describeSpawnError(spec.command, INSTALL_HINT[harness], err);
1965
+ console.error(message);
1966
+ resolve(exitCode);
1893
1967
  });
1894
1968
  child.on("exit", (code) => {
1895
1969
  resolve(typeof code === "number" ? code : 1);
@@ -1898,6 +1972,14 @@ async function runRun(harness, opts, deps = realDeps) {
1898
1972
  }
1899
1973
 
1900
1974
  // src/index.ts
1975
+ var MIN_NODE_MAJOR = 20;
1976
+ var nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
1977
+ if (nodeMajor < MIN_NODE_MAJOR) {
1978
+ console.error(
1979
+ `LunaRoute CLI requires Node.js ${MIN_NODE_MAJOR}+ \u2014 you're running ${process.versions.node}. Upgrade from https://nodejs.org/en/download and try again.`
1980
+ );
1981
+ process.exit(1);
1982
+ }
1901
1983
  var program = new Command();
1902
1984
  program.name("lunaroute").description("LunaRoute CLI \u2014 configure coding harnesses and manage your account.").version(version).option("-p, --profile <name>", "credential profile to use", "default");
1903
1985
  program.command("login").description("Authorize this device via the browser and store a routing key.").action(async () => {
@@ -1909,14 +1991,15 @@ program.command("whoami").description("Show the signed-in user and organization.
1909
1991
  program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
1910
1992
  logout(program.opts().profile);
1911
1993
  });
1912
- 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").action(async (harness, opts) => {
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) => {
1913
1995
  const code = await runSetup(harness, {
1914
1996
  profile: program.opts().profile,
1915
1997
  print: opts.print,
1916
1998
  routingUrl: opts.routingUrl,
1917
1999
  yes: opts.yes,
1918
2000
  extension: opts.extension,
1919
- models: opts.models
2001
+ models: opts.models,
2002
+ key: opts.key
1920
2003
  });
1921
2004
  if (code !== 0) process.exit(code);
1922
2005
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunaroute/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "LunaRoute CLI — configure coding harnesses and manage your LunaRoute account.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,10 +30,12 @@
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
32
  "commander": "^15.0.0",
33
+ "cross-spawn": "^7.0.6",
33
34
  "open": "^11.0.1",
34
35
  "zod": "^4.4.3"
35
36
  },
36
37
  "devDependencies": {
38
+ "@types/cross-spawn": "^6.0.6",
37
39
  "@types/node": "^26.2.0",
38
40
  "tsup": "^8.3.0",
39
41
  "tsx": "^4.23.12",