@lunaroute/cli 0.1.1 → 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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +345 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@ Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accept
37
37
  | `lunaroute login` | Authorize this device via the browser and store a routing key |
38
38
  | `lunaroute whoami` | Show the signed-in user and organization |
39
39
  | `lunaroute logout` | Remove stored credentials |
40
- | `lunaroute setup <harness>` | Configure `opencode` \| `pi` \| `claude-code` \| `copilot-cli` \| `generic`. pi setup is interactive (asks before writing); `--extension` / `--models` pick a flow, `--yes` accepts prompts, `--print` previews |
40
+ | `lunaroute setup <harness>` | Configure `opencode` \| `pi` \| `openclaw` \| `claude-code` \| `copilot-cli` \| `generic`. pi setup is interactive (asks before writing); `--extension` / `--models` pick a flow, `--yes` accepts prompts, `--print` previews |
41
41
  | `lunaroute run <harness>` | Launch `claude` \| `claude-code` \| `codex` on LunaRoute for this session |
42
42
  | `lunaroute models` | List available LunaRoute models |
43
43
  | `lunaroute pricing` | Per-model pricing (credits per million tokens) |
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";
@@ -411,6 +414,9 @@ function configHome() {
411
414
  function opencodeConfigPath() {
412
415
  return join2(configHome(), "opencode", "opencode.json");
413
416
  }
417
+ function openclawConfigPath() {
418
+ return join2(homedir2(), ".openclaw", "openclaw.json");
419
+ }
414
420
  function piModelsPath() {
415
421
  return join2(homedir2(), ".pi", "agent", "models.json");
416
422
  }
@@ -441,7 +447,55 @@ function gitRepoRoot(cwd = process.cwd()) {
441
447
  }
442
448
 
443
449
  // src/setup/adapters/opencode.ts
444
- function buildPlan(ctx) {
450
+ var EXTENSION = "@lunaroute/opencode-extension";
451
+ var PROD_GATEWAY_HOST = "gw.lunaroute.com";
452
+ function buildPlan(ctx, opts = {}) {
453
+ if (opts.extension) return extensionPlan(ctx);
454
+ return providerPlan(ctx);
455
+ }
456
+ function extensionPlan(ctx) {
457
+ return {
458
+ fileWrites: [
459
+ {
460
+ kind: "json",
461
+ path: opencodeConfigPath(),
462
+ merge: (existing) => {
463
+ const obj = existing ?? {};
464
+ const raw = obj.plugin;
465
+ const plugins = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
466
+ if (!plugins.includes(EXTENSION)) plugins.push(EXTENSION);
467
+ const next = { ...obj, plugin: plugins };
468
+ if (!isProdGateway(ctx.routingUrl)) {
469
+ const providers = objectOrEmpty(obj.provider);
470
+ const lr = objectOrEmpty(providers.lunaroute);
471
+ next.provider = {
472
+ ...providers,
473
+ lunaroute: { ...lr, options: { ...objectOrEmpty(lr.options), baseURL: `${ctx.routingUrl}/v1` } }
474
+ };
475
+ }
476
+ return next;
477
+ }
478
+ }
479
+ ],
480
+ // No env key: login happens in-app via /connect.
481
+ exports: [],
482
+ notes: [
483
+ "\nopencode: the extension installs on the next opencode start (npm plugins auto-install at startup).",
484
+ "Inside opencode: run /connect to log in, then /models to pick a LunaRoute model."
485
+ ]
486
+ };
487
+ }
488
+ function isProdGateway(url) {
489
+ try {
490
+ return new URL(url).host === PROD_GATEWAY_HOST;
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+ function objectOrEmpty(v) {
496
+ return typeof v === "object" && v !== null ? v : {};
497
+ }
498
+ function providerPlan(ctx) {
445
499
  const models = {};
446
500
  for (const m of ctx.models) models[m.id] = { name: m.id };
447
501
  const block = {
@@ -462,7 +516,7 @@ function buildPlan(ctx) {
462
516
  path: opencodeConfigPath(),
463
517
  merge: (existing) => {
464
518
  const obj = existing ?? {};
465
- const existingProviders = typeof obj.provider === "object" && obj.provider !== null ? obj.provider : {};
519
+ const existingProviders = objectOrEmpty(obj.provider);
466
520
  const provider = { ...existingProviders, lunaroute: block };
467
521
  return { ...obj, provider };
468
522
  }
@@ -537,21 +591,24 @@ function buildPlan3(ctx) {
537
591
  { name: ctx.keyEnvVar, value: "__KEY__" },
538
592
  // Point at the routing root; Claude Code appends /v1/messages itself.
539
593
  { name: "ANTHROPIC_BASE_URL", value: ctx.routingUrl },
540
- // Claude Code sends ANTHROPIC_API_KEY as the native x-api-key header. The
541
- // routing edge recognizes the lr_ prefix, authenticates it, and strips the
542
- // header before forwarding upstream — so the key never leaks and never
543
- // sits in the URL path.
544
- { 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}` },
545
602
  { name: "ANTHROPIC_MODEL", value: firstModel }
546
603
  ],
547
604
  notes: [
548
605
  "\nClaude Code: add the exports above to your shell profile, then restart Claude Code.",
549
606
  `Change ANTHROPIC_MODEL to any of: ${ctx.models.map((m) => m.id).join(", ")}`,
550
- "(Auth travels in the native x-api-key header via ANTHROPIC_API_KEY.)",
551
- "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.",
552
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.",
553
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:",
554
- ` 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"}]}'`
555
612
  ]
556
613
  };
557
614
  }
@@ -598,10 +655,94 @@ function buildPlan5(ctx) {
598
655
  };
599
656
  }
600
657
 
658
+ // src/setup/routing-url.ts
659
+ function validatedRoutingUrl(routingUrl) {
660
+ function fail(reason) {
661
+ throw new Error(
662
+ `routingUrl rejected (reason: ${reason}; <redacted URL>) \u2014 check --routing-url or the profile's routing URL`
663
+ );
664
+ }
665
+ if (!/^https?:\/\/[^/?#]/.test(routingUrl)) fail("invalid-url");
666
+ if (/[?#]/.test(routingUrl)) fail("query-or-fragment");
667
+ if (routingUrl !== routingUrl.trim()) fail("whitespace");
668
+ if (routingUrl.endsWith("/")) fail("trailing-slash");
669
+ let parsed;
670
+ try {
671
+ parsed = new URL(routingUrl);
672
+ } catch {
673
+ fail("invalid-url");
674
+ }
675
+ if (parsed.username !== "" || parsed.password !== "") fail("userinfo");
676
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail("invalid-url");
677
+ if (parsed.hostname === "") fail("invalid-url");
678
+ return routingUrl;
679
+ }
680
+
681
+ // src/setup/adapters/openclaw.ts
682
+ function asRecord(v) {
683
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
684
+ }
685
+ function buildPlan6(ctx) {
686
+ const base = validatedRoutingUrl(ctx.routingUrl);
687
+ const models = ctx.models.map((m) => ({ id: m.id, name: m.id }));
688
+ return {
689
+ fileWrites: [
690
+ {
691
+ kind: "json",
692
+ path: openclawConfigPath(),
693
+ merge: (existing) => {
694
+ const obj = asRecord(existing);
695
+ const modelsCfg = asRecord(obj.models);
696
+ const providers = asRecord(modelsCfg.providers);
697
+ const agents = asRecord(obj.agents);
698
+ const defaults = asRecord(agents.defaults);
699
+ const model = asRecord(defaults.model);
700
+ return {
701
+ ...obj,
702
+ models: {
703
+ ...modelsCfg,
704
+ mode: modelsCfg.mode ?? "merge",
705
+ providers: {
706
+ ...providers,
707
+ lunaroute: {
708
+ baseUrl: `${base}/v1`,
709
+ // OpenClaw interpolates the env var and sends it as
710
+ // Authorization: Bearer lr_…; the edge authenticates the lr_
711
+ // key and strips the header before forwarding upstream.
712
+ apiKey: `\${${ctx.keyEnvVar}}`,
713
+ api: "openai-completions",
714
+ models
715
+ }
716
+ }
717
+ },
718
+ agents: {
719
+ ...agents,
720
+ defaults: {
721
+ ...defaults,
722
+ model: {
723
+ ...model,
724
+ // Preserve an existing primary model, like OpenClaw onboarding.
725
+ primary: model.primary ?? `lunaroute/${ctx.models[0]?.id ?? "<model>"}`
726
+ }
727
+ }
728
+ }
729
+ };
730
+ }
731
+ }
732
+ ],
733
+ exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
734
+ notes: [
735
+ `
736
+ openclaw: restart the gateway, then \`openclaw models list\` and \`openclaw models set lunaroute/${ctx.models[0]?.id ?? "<model>"}\` (only if you have no default model already). If lunaroute/* models do not appear in \`openclaw models list\`, check that models.mode is merge \u2014 setup preserves whatever mode you have.`
737
+ ]
738
+ };
739
+ }
740
+
601
741
  // src/commands/setup.ts
602
742
  var ADAPTERS = {
603
743
  opencode: buildPlan,
604
744
  pi: buildPlan2,
745
+ openclaw: buildPlan6,
605
746
  "claude-code": buildPlan3,
606
747
  "copilot-cli": buildPlan4,
607
748
  generic: buildPlan5
@@ -616,8 +757,10 @@ async function runSetup(harness, opts, deps = {
616
757
  return 1;
617
758
  }
618
759
  const adapter = ADAPTERS[harness];
619
- if (!adapter) {
620
- 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
+ );
621
764
  return 1;
622
765
  }
623
766
  const creds = loadProfile(opts.profile);
@@ -625,14 +768,27 @@ async function runSetup(harness, opts, deps = {
625
768
  console.error('Not logged in. Run "lunaroute login" first.');
626
769
  return 1;
627
770
  }
628
- const routingUrl = opts.routingUrl || creds.routing_url;
629
- if (!routingUrl) {
771
+ const routingUrlRaw = opts.routingUrl || creds.routing_url;
772
+ if (!routingUrlRaw) {
630
773
  console.error("No routing URL in profile; pass --routing-url.");
631
774
  return 1;
632
775
  }
776
+ let routingUrl;
777
+ try {
778
+ routingUrl = validatedRoutingUrl(routingUrlRaw.replace(/\/+$/, "").trim());
779
+ } catch (err) {
780
+ console.error(err instanceof Error ? err.message : err);
781
+ return 1;
782
+ }
633
783
  if (harness === "pi" && !opts.print) {
634
784
  return runPiSetup(opts, deps);
635
785
  }
786
+ if (harness === "opencode") {
787
+ return runOpencodeSetup(creds, routingUrl, opts, deps);
788
+ }
789
+ if (harness === "hermes") {
790
+ return runHermesSetup(creds, routingUrl, opts, deps);
791
+ }
636
792
  let models;
637
793
  try {
638
794
  models = await fetchModels(routingUrl);
@@ -646,8 +802,15 @@ async function runSetup(harness, opts, deps = {
646
802
  models,
647
803
  keyEnvVar: "LUNAROUTE_API_KEY"
648
804
  };
805
+ let plan;
806
+ try {
807
+ plan = adapter(ctx);
808
+ } catch (err) {
809
+ console.error(err instanceof Error ? err.message : err);
810
+ return 1;
811
+ }
649
812
  try {
650
- const summary = await applyPlan(adapter(ctx), { print: opts.print, key: creds.routing_key });
813
+ const summary = await applyPlan(plan, { print: opts.print, key: creds.routing_key });
651
814
  if (!opts.print && summary.written.length > 0) {
652
815
  console.log(`
653
816
  Wrote: ${summary.written.join(", ")}`);
@@ -699,6 +862,169 @@ async function runPiSetup(opts, deps) {
699
862
  return 1;
700
863
  }
701
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
+ }
937
+ async function runOpencodeSetup(creds, routingUrl, opts, deps) {
938
+ try {
939
+ if (opts.print) {
940
+ const plan = buildPlan(
941
+ { routingUrl, orgId: creds.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
942
+ { extension: true }
943
+ );
944
+ return applyAndReport(plan, creds.routing_key, false);
945
+ }
946
+ if (await deps.confirm(
947
+ "Install the LunaRoute OpenCode extension (recommended \u2014 /connect login, models auto-sync)?",
948
+ { yes: opts.yes }
949
+ )) {
950
+ const plan = buildPlan(
951
+ { routingUrl, orgId: creds.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
952
+ { extension: true }
953
+ );
954
+ const code = await applyAndReport(plan, creds.routing_key, !opts.print);
955
+ if (code !== 0) return code;
956
+ if (await deps.confirm("Start opencode now? (if it's already open, quit and reopen it to load the extension)", {
957
+ yes: opts.yes
958
+ })) {
959
+ return launchOpencode(deps.spawn);
960
+ }
961
+ console.log("\nStart opencode when ready \u2014 then run /connect to log in and /models to pick a LunaRoute model.");
962
+ return 0;
963
+ }
964
+ const write = await deps.confirm(
965
+ "Merge LunaRoute provider into opencode.json instead? (backup kept, other providers preserved)",
966
+ { yes: opts.yes }
967
+ );
968
+ return applyOpencodeProvider(creds, routingUrl, write);
969
+ } catch (err) {
970
+ if (err instanceof NonInteractiveTerminalError) {
971
+ console.error(
972
+ "No interactive terminal available. Re-run with --yes to accept prompts, or --print to preview without writing."
973
+ );
974
+ return 1;
975
+ }
976
+ console.error(err instanceof Error ? err.message : String(err));
977
+ return 1;
978
+ }
979
+ }
980
+ async function applyOpencodeProvider(creds, routingUrl, write) {
981
+ let models;
982
+ try {
983
+ models = await fetchModels(routingUrl);
984
+ } catch (err) {
985
+ console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
986
+ return 1;
987
+ }
988
+ const plan = buildPlan({
989
+ routingUrl,
990
+ orgId: creds.org_id,
991
+ models,
992
+ keyEnvVar: "LUNAROUTE_API_KEY"
993
+ });
994
+ return applyAndReport(plan, creds.routing_key, write);
995
+ }
996
+ async function applyAndReport(plan, key, write) {
997
+ try {
998
+ const summary = await applyPlan(plan, { print: !write, key });
999
+ if (write && summary.written.length > 0) {
1000
+ console.log(`
1001
+ Wrote: ${summary.written.join(", ")}`);
1002
+ if (summary.backedUp.length > 0) console.log(` Backups: ${summary.backedUp.join(", ")}`);
1003
+ }
1004
+ return 0;
1005
+ } catch (err) {
1006
+ console.error(err instanceof Error ? err.message : String(err));
1007
+ return 1;
1008
+ }
1009
+ }
1010
+ async function launchOpencode(spawn3) {
1011
+ return new Promise((resolve) => {
1012
+ const child = spawn3("opencode", []);
1013
+ child.on("error", (err) => {
1014
+ const e = err;
1015
+ if (e?.code === "ENOENT") {
1016
+ console.error(`Error: "opencode" not found on PATH. Install OpenCode first: https://opencode.ai (exit 127)`);
1017
+ resolve(127);
1018
+ return;
1019
+ }
1020
+ console.error(err instanceof Error ? err.message : String(err));
1021
+ resolve(1);
1022
+ });
1023
+ child.on("exit", (code) => {
1024
+ resolve(typeof code === "number" ? code : 1);
1025
+ });
1026
+ });
1027
+ }
702
1028
  async function installPiExtension(spawn3) {
703
1029
  for (const pkg of PI_INSTALL_PACKAGES) {
704
1030
  const code = await new Promise((resolve) => {
@@ -1062,7 +1388,7 @@ async function runMcp(profile) {
1062
1388
  read: (p) => readMemory(ctx, p),
1063
1389
  resolveProjectId
1064
1390
  };
1065
- const server = new McpServer({ name: "lunaroute-memory", version: "0.1.0" });
1391
+ const server = new McpServer({ name: "lunaroute-memory", version });
1066
1392
  server.registerTool(
1067
1393
  "search",
1068
1394
  {
@@ -1197,7 +1523,7 @@ function buildRunSpec(ctx) {
1197
1523
  args: [],
1198
1524
  env: {
1199
1525
  ANTHROPIC_BASE_URL: ctx.routingUrl,
1200
- ANTHROPIC_API_KEY: ctx.apiKey,
1526
+ ANTHROPIC_AUTH_TOKEN: ctx.apiKey,
1201
1527
  ANTHROPIC_MODEL: ctx.model,
1202
1528
  // Pins the Haiku/background model so Claude Code's background tasks
1203
1529
  // (titles, summaries, compaction) route through LunaRoute instead of
@@ -1295,7 +1621,7 @@ async function runRun(harness, opts, deps = realDeps) {
1295
1621
 
1296
1622
  // src/index.ts
1297
1623
  var program = new Command();
1298
- 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");
1299
1625
  program.command("login").description("Authorize this device via the browser and store a routing key.").action(async () => {
1300
1626
  await login(program.opts().profile);
1301
1627
  });
@@ -1305,7 +1631,7 @@ program.command("whoami").description("Show the signed-in user and organization.
1305
1631
  program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
1306
1632
  logout(program.opts().profile);
1307
1633
  });
1308
- 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) => {
1309
1635
  const code = await runSetup(harness, {
1310
1636
  profile: program.opts().profile,
1311
1637
  print: opts.print,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunaroute/cli",
3
- "version": "0.1.1",
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",