@lunaroute/cli 0.2.0 → 0.2.1

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 +97 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // src/index.ts
4
4
  import { Command, InvalidArgumentError, Option } from "commander";
5
5
 
6
+ // package.json
7
+ var version = "0.2.1";
8
+
6
9
  // src/login.ts
7
10
  import { createServer } from "http";
8
11
  import { hostname } from "os";
@@ -588,21 +591,24 @@ function buildPlan3(ctx) {
588
591
  { name: ctx.keyEnvVar, value: "__KEY__" },
589
592
  // Point at the routing root; Claude Code appends /v1/messages itself.
590
593
  { name: "ANTHROPIC_BASE_URL", value: ctx.routingUrl },
591
- // Claude Code sends ANTHROPIC_API_KEY as the native x-api-key header. The
592
- // routing edge recognizes the lr_ prefix, authenticates it, and strips the
593
- // header before forwarding upstream — so the key never leaks and never
594
- // sits in the URL path.
595
- { name: "ANTHROPIC_API_KEY", value: `$${ctx.keyEnvVar}` },
594
+ // Claude Code sends ANTHROPIC_AUTH_TOKEN as `Authorization: Bearer`. The
595
+ // routing edge recognizes the lr_ prefix, authenticates it, and strips
596
+ // the header before forwarding upstream — so the key never leaks and
597
+ // never sits in the URL path. Bearer also outranks a stored claude.ai
598
+ // subscription OAuth login; ANTHROPIC_API_KEY does not (unapproved env
599
+ // keys are ignored and the OAuth token 401s through the gateway, kata
600
+ // 009h).
601
+ { name: "ANTHROPIC_AUTH_TOKEN", value: `$${ctx.keyEnvVar}` },
596
602
  { name: "ANTHROPIC_MODEL", value: firstModel }
597
603
  ],
598
604
  notes: [
599
605
  "\nClaude Code: add the exports above to your shell profile, then restart Claude Code.",
600
606
  `Change ANTHROPIC_MODEL to any of: ${ctx.models.map((m) => m.id).join(", ")}`,
601
- "(Auth travels in the native x-api-key header via ANTHROPIC_API_KEY.)",
602
- "If your Claude Code build requires a bearer token instead, set ANTHROPIC_AUTH_TOKEN=$LUNAROUTE_API_KEY \u2014 LunaRoute also authenticates lr_ keys sent as Authorization: Bearer.",
607
+ "(Auth travels in the Authorization: Bearer header via ANTHROPIC_AUTH_TOKEN.)",
608
+ "If your Claude Code build predates ANTHROPIC_AUTH_TOKEN support, set ANTHROPIC_API_KEY=$LUNAROUTE_API_KEY instead \u2014 LunaRoute also authenticates lr_ keys sent as x-api-key. Bearer wins whenever both are set.",
603
609
  "To set these in ~/.claude/settings.json instead, note its env values are literal \u2014 the key would be baked into the file rather than read from $LUNAROUTE_API_KEY.",
604
610
  "If Claude Code hangs or keeps retrying when connecting, your key is likely wrong \u2014 it retries auth errors silently. Debug with the curl below, which fails fast with the real error:",
605
- ` curl ${ctx.routingUrl}/v1/messages -H "x-api-key: $${ctx.keyEnvVar}" -H "content-type: application/json" -d '{"model":"${firstModel}","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}'`
611
+ ` curl ${ctx.routingUrl}/v1/messages -H "Authorization: Bearer $${ctx.keyEnvVar}" -H "content-type: application/json" -d '{"model":"${firstModel}","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}'`
606
612
  ]
607
613
  };
608
614
  }
@@ -751,8 +757,10 @@ async function runSetup(harness, opts, deps = {
751
757
  return 1;
752
758
  }
753
759
  const adapter = ADAPTERS[harness];
754
- if (!adapter) {
755
- console.error(`Unknown harness "${harness}". Choose one of: ${Object.keys(ADAPTERS).join(", ")}.`);
760
+ if (!adapter && harness !== "hermes") {
761
+ console.error(
762
+ `Unknown harness "${harness}". Choose one of: ${[...Object.keys(ADAPTERS), "hermes"].join(", ")}.`
763
+ );
756
764
  return 1;
757
765
  }
758
766
  const creds = loadProfile(opts.profile);
@@ -778,6 +786,9 @@ async function runSetup(harness, opts, deps = {
778
786
  if (harness === "opencode") {
779
787
  return runOpencodeSetup(creds, routingUrl, opts, deps);
780
788
  }
789
+ if (harness === "hermes") {
790
+ return runHermesSetup(creds, routingUrl, opts, deps);
791
+ }
781
792
  let models;
782
793
  try {
783
794
  models = await fetchModels(routingUrl);
@@ -851,6 +862,78 @@ async function runPiSetup(opts, deps) {
851
862
  return 1;
852
863
  }
853
864
  }
865
+ async function runHermesSetup(creds, routingUrl, opts, deps) {
866
+ const commands = hermesConfigSetArgs(routingUrl, creds.routing_key);
867
+ try {
868
+ if (opts.print) {
869
+ console.log("\n# would run:");
870
+ for (const args of commands) console.log(` hermes ${args.join(" ")}`);
871
+ console.log(
872
+ "\nThen start hermes and run /model custom:lunaroute:<model> to pick a LunaRoute model (models auto-discover from /v1/models)."
873
+ );
874
+ return 0;
875
+ }
876
+ if (await deps.confirm("Configure LunaRoute for Hermes via 'hermes config set'? (recommended \u2014 models auto-discover)", {
877
+ yes: opts.yes
878
+ })) {
879
+ return runHermesConfigSets(deps.spawn, routingUrl, creds.routing_key);
880
+ }
881
+ console.log("Skipped. Re-run with --yes, or configure manually:");
882
+ for (const args of commands) console.log(` hermes ${args.join(" ")}`);
883
+ console.log("\nThen start hermes and run /model custom:lunaroute:<model> to pick a LunaRoute model.");
884
+ return 0;
885
+ } catch (err) {
886
+ if (err instanceof NonInteractiveTerminalError) {
887
+ console.error("No interactive terminal available. Re-run with --yes to accept prompts, or --print to preview.");
888
+ return 1;
889
+ }
890
+ console.error(err instanceof Error ? err.message : String(err));
891
+ return 1;
892
+ }
893
+ }
894
+ function hermesConfigSetArgs(routingUrl, key) {
895
+ return [
896
+ ["config", "set", "providers.lunaroute.api", `${routingUrl}/v1`],
897
+ ["config", "set", "providers.lunaroute.key_env", "LUNAROUTE_API_KEY"],
898
+ ["config", "set", "providers.lunaroute.transport", "chat_completions"],
899
+ ["config", "set", "LUNAROUTE_API_KEY", key]
900
+ ];
901
+ }
902
+ async function runHermesConfigSets(spawn3, routingUrl, key) {
903
+ const commands = hermesConfigSetArgs(routingUrl, key);
904
+ const setKeys = commands.map((c) => c[2]);
905
+ for (let i = 0; i < commands.length; i++) {
906
+ const code = await new Promise((resolve) => {
907
+ const child = spawn3("hermes", commands[i]);
908
+ child.on("error", (err) => {
909
+ const e = err;
910
+ if (e?.code === "ENOENT") {
911
+ console.error(
912
+ `Error: "hermes" not found on PATH. Install Hermes Agent first: https://hermes-agent.nousresearch.com (exit 127)`
913
+ );
914
+ resolve(127);
915
+ return;
916
+ }
917
+ console.error(err instanceof Error ? err.message : String(err));
918
+ resolve(1);
919
+ });
920
+ child.on("exit", (code2) => {
921
+ resolve(typeof code2 === "number" ? code2 : 1);
922
+ });
923
+ });
924
+ if (code !== 0) {
925
+ const done = setKeys.slice(0, i);
926
+ console.error(
927
+ `'hermes config set ${setKeys[i]}' failed (exit ${code}). Set so far: ${done.length ? done.join(", ") : "nothing"}. Re-run 'lunaroute setup hermes' to retry (all steps are idempotent).`
928
+ );
929
+ return code || 1;
930
+ }
931
+ }
932
+ console.log("\nConfigured. Start hermes (or restart it if already running), then:");
933
+ console.log(" /model custom:lunaroute:<model> # switch to a LunaRoute model (models auto-discover from /v1/models)");
934
+ console.log(" hermes model # interactive provider/model picker");
935
+ return 0;
936
+ }
854
937
  async function runOpencodeSetup(creds, routingUrl, opts, deps) {
855
938
  try {
856
939
  if (opts.print) {
@@ -1305,7 +1388,7 @@ async function runMcp(profile) {
1305
1388
  read: (p) => readMemory(ctx, p),
1306
1389
  resolveProjectId
1307
1390
  };
1308
- const server = new McpServer({ name: "lunaroute-memory", version: "0.1.0" });
1391
+ const server = new McpServer({ name: "lunaroute-memory", version });
1309
1392
  server.registerTool(
1310
1393
  "search",
1311
1394
  {
@@ -1440,7 +1523,7 @@ function buildRunSpec(ctx) {
1440
1523
  args: [],
1441
1524
  env: {
1442
1525
  ANTHROPIC_BASE_URL: ctx.routingUrl,
1443
- ANTHROPIC_API_KEY: ctx.apiKey,
1526
+ ANTHROPIC_AUTH_TOKEN: ctx.apiKey,
1444
1527
  ANTHROPIC_MODEL: ctx.model,
1445
1528
  // Pins the Haiku/background model so Claude Code's background tasks
1446
1529
  // (titles, summaries, compaction) route through LunaRoute instead of
@@ -1538,7 +1621,7 @@ async function runRun(harness, opts, deps = realDeps) {
1538
1621
 
1539
1622
  // src/index.ts
1540
1623
  var program = new Command();
1541
- program.name("lunaroute").description("LunaRoute CLI \u2014 configure coding harnesses and manage your account.").version("0.1.0").option("-p, --profile <name>", "credential profile to use", "default");
1624
+ 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");
1542
1625
  program.command("login").description("Authorize this device via the browser and store a routing key.").action(async () => {
1543
1626
  await login(program.opts().profile);
1544
1627
  });
@@ -1548,7 +1631,7 @@ program.command("whoami").description("Show the signed-in user and organization.
1548
1631
  program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
1549
1632
  logout(program.opts().profile);
1550
1633
  });
1551
- program.command("setup <harness>").description("Configure a coding harness (opencode | pi | claude-code | copilot-cli | 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) => {
1634
+ 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) => {
1552
1635
  const code = await runSetup(harness, {
1553
1636
  profile: program.opts().profile,
1554
1637
  print: opts.print,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunaroute/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "LunaRoute CLI — configure coding harnesses and manage your LunaRoute account.",
5
5
  "repository": {
6
6
  "type": "git",