@lunaroute/cli 0.2.5 → 0.2.7

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 +4 -4
  2. package/dist/index.js +148 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,18 +15,18 @@ Requires Node.js 20+.
15
15
  ## Quickstart
16
16
 
17
17
  ```sh
18
- lunaroute setup pi # or: opencode | claude-code | copilot-cli | generic
18
+ lunaroute setup pi # or: opencode | claude-code | codex | copilot-cli | generic
19
19
  ```
20
20
 
21
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
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).
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.
24
24
 
25
25
  Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accepts all prompts, and `--print` previews without writing.
26
26
 
27
27
  ## What it enables
28
28
 
29
- - **One-command setup** — `lunaroute setup` writes the right config for `pi`, `claude-code`, `opencode`, `copilot-cli`, or `generic` (any OpenAI-compatible base URL swap).
29
+ - **One-command setup** — `lunaroute setup` writes the right config for `pi`, `claude-code`, `codex`, `opencode`, `copilot-cli`, or `generic` (any OpenAI-compatible base URL swap).
30
30
  - **Session launches** — `lunaroute run claude` or `lunaroute run codex` starts a session wired to LunaRoute; `--model` picks from the catalog, everything after `--` passes through to the harness.
31
31
  - **The catalog, in your terminal** — `models`, `pricing`, and `usage` show what's available, what it costs (credits per million tokens), and your wallet balance plus recent ledger entries.
32
32
  - **Project memory** — `memory search` and `memory read` query your past agent conversations for the current project. `lunaroute mcp` exposes the same tools as a stdio MCP server, and `skill install` wires the memory skill + MCP into Claude Code.
@@ -38,7 +38,7 @@ Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accept
38
38
  | `lunaroute login` | Authorize this device via the browser and store a routing key |
39
39
  | `lunaroute whoami` | Show the signed-in user and organization |
40
40
  | `lunaroute logout` | Remove stored credentials |
41
- | `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
+ | `lunaroute setup <harness>` | Configure `opencode` \| `pi` \| `openclaw` \| `claude-code` \| `codex` \| `copilot-cli` \| `generic`. pi setup is interactive (asks before writing); `--extension` / `--models` pick a flow, `--yes` accepts prompts, `--print` previews |
42
42
  | `lunaroute run <harness>` | Launch `claude` \| `claude-code` \| `codex` on LunaRoute for this session |
43
43
  | `lunaroute models` | List available LunaRoute models |
44
44
  | `lunaroute pricing` | Per-model pricing (credits per million tokens) |
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.5";
7
+ var version = "0.2.7";
8
8
 
9
9
  // src/login.ts
10
10
  import { createServer } from "http";
@@ -591,9 +591,17 @@ function configHome() {
591
591
  function opencodeConfigPath() {
592
592
  return join2(configHome(), "opencode", "opencode.json");
593
593
  }
594
+ function opencodeAuthPath() {
595
+ const dataHome = process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share");
596
+ return join2(dataHome, "opencode", "auth.json");
597
+ }
594
598
  function openclawConfigPath() {
595
599
  return join2(homedir2(), ".openclaw", "openclaw.json");
596
600
  }
601
+ function codexProfilePath() {
602
+ const home = process.env.CODEX_HOME || join2(homedir2(), ".codex");
603
+ return join2(home, "lunaroute.config.toml");
604
+ }
597
605
  function piModelsPath() {
598
606
  return join2(homedir2(), ".pi", "agent", "models.json");
599
607
  }
@@ -708,6 +716,25 @@ function providerPlan(ctx) {
708
716
  ]
709
717
  };
710
718
  }
719
+ function buildKeyPlan(key) {
720
+ return {
721
+ fileWrites: [
722
+ {
723
+ kind: "json",
724
+ path: opencodeAuthPath(),
725
+ merge: (existing) => {
726
+ const obj = existing ?? {};
727
+ delete obj["lunaroute/"];
728
+ return { ...obj, lunaroute: { type: "api", key } };
729
+ }
730
+ }
731
+ ],
732
+ exports: [],
733
+ notes: [
734
+ "opencode: key stored in ~/.local/share/opencode/auth.json \u2014 start opencode (or restart it), then /models to pick a lunaroute/* model."
735
+ ]
736
+ };
737
+ }
711
738
 
712
739
  // src/setup/adapters/pi.ts
713
740
  import { readFileSync as readFileSync3 } from "fs";
@@ -811,7 +838,7 @@ function buildStaleProviderCleanupPlan() {
811
838
  };
812
839
  }
813
840
  var KEY_EXPIRES_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
814
- function buildKeyPlan(key) {
841
+ function buildKeyPlan2(key) {
815
842
  return {
816
843
  fileWrites: [
817
844
  {
@@ -862,8 +889,57 @@ function buildPlan3(ctx) {
862
889
  };
863
890
  }
864
891
 
865
- // src/setup/adapters/copilotCli.ts
892
+ // src/setup/routing-url.ts
893
+ function validatedRoutingUrl(routingUrl) {
894
+ function fail2(reason) {
895
+ throw new Error(
896
+ `routingUrl rejected (reason: ${reason}; <redacted URL>) \u2014 check --routing-url or the profile's routing URL`
897
+ );
898
+ }
899
+ if (!/^https?:\/\/[^/?#]/.test(routingUrl)) fail2("invalid-url");
900
+ if (/[?#]/.test(routingUrl)) fail2("query-or-fragment");
901
+ if (routingUrl !== routingUrl.trim()) fail2("whitespace");
902
+ if (routingUrl.endsWith("/")) fail2("trailing-slash");
903
+ let parsed;
904
+ try {
905
+ parsed = new URL(routingUrl);
906
+ } catch {
907
+ fail2("invalid-url");
908
+ }
909
+ if (parsed.username !== "" || parsed.password !== "") fail2("userinfo");
910
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail2("invalid-url");
911
+ if (parsed.hostname === "") fail2("invalid-url");
912
+ return routingUrl;
913
+ }
914
+
915
+ // src/setup/adapters/codex.ts
866
916
  function buildPlan4(ctx) {
917
+ const base = validatedRoutingUrl(ctx.routingUrl);
918
+ const model = ctx.models[0]?.id ?? "<model>";
919
+ const content = [
920
+ `model = "${model}"`,
921
+ 'model_provider = "lunaroute"',
922
+ "",
923
+ "[model_providers.lunaroute]",
924
+ 'name = "LunaRoute"',
925
+ `base_url = "${base}/v1"`,
926
+ 'env_key = "LUNAROUTE_API_KEY"',
927
+ 'wire_api = "responses"'
928
+ ].join("\n") + "\n";
929
+ return {
930
+ fileWrites: [{ kind: "text", path: codexProfilePath(), content }],
931
+ exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
932
+ notes: [
933
+ "\nCodex: add the export above to your shell profile, then start Codex with `codex --profile lunaroute`.",
934
+ `Change model to any of: ${ctx.models.map((m) => m.id).join(", ")}, or pass \`--model <id>\` for one session.`,
935
+ "The key rides the Authorization: Bearer header via env_key; LunaRoute authenticates the lr_ key at the edge and never forwards it upstream.",
936
+ "One file: the model and the named provider live in the profile file \u2014 your ~/.codex/config.toml is untouched."
937
+ ]
938
+ };
939
+ }
940
+
941
+ // src/setup/adapters/copilotCli.ts
942
+ function buildPlan5(ctx) {
867
943
  const firstModel = ctx.models[0]?.id ?? "<model>";
868
944
  return {
869
945
  fileWrites: [],
@@ -889,7 +965,7 @@ function buildPlan4(ctx) {
889
965
  }
890
966
 
891
967
  // src/setup/adapters/generic.ts
892
- function buildPlan5(ctx) {
968
+ function buildPlan6(ctx) {
893
969
  return {
894
970
  fileWrites: [],
895
971
  exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
@@ -904,34 +980,11 @@ function buildPlan5(ctx) {
904
980
  };
905
981
  }
906
982
 
907
- // src/setup/routing-url.ts
908
- function validatedRoutingUrl(routingUrl) {
909
- function fail2(reason) {
910
- throw new Error(
911
- `routingUrl rejected (reason: ${reason}; <redacted URL>) \u2014 check --routing-url or the profile's routing URL`
912
- );
913
- }
914
- if (!/^https?:\/\/[^/?#]/.test(routingUrl)) fail2("invalid-url");
915
- if (/[?#]/.test(routingUrl)) fail2("query-or-fragment");
916
- if (routingUrl !== routingUrl.trim()) fail2("whitespace");
917
- if (routingUrl.endsWith("/")) fail2("trailing-slash");
918
- let parsed;
919
- try {
920
- parsed = new URL(routingUrl);
921
- } catch {
922
- fail2("invalid-url");
923
- }
924
- if (parsed.username !== "" || parsed.password !== "") fail2("userinfo");
925
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail2("invalid-url");
926
- if (parsed.hostname === "") fail2("invalid-url");
927
- return routingUrl;
928
- }
929
-
930
983
  // src/setup/adapters/openclaw.ts
931
984
  function asRecord(v) {
932
985
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
933
986
  }
934
- function buildPlan6(ctx) {
987
+ function buildPlan7(ctx) {
935
988
  const base = validatedRoutingUrl(ctx.routingUrl);
936
989
  const models = ctx.models.map((m) => ({ id: m.id, name: m.id }));
937
990
  return {
@@ -991,10 +1044,11 @@ openclaw: restart the gateway, then \`openclaw models list\` and \`openclaw mode
991
1044
  var ADAPTERS = {
992
1045
  opencode: buildPlan,
993
1046
  pi: buildPlan2,
994
- openclaw: buildPlan6,
1047
+ openclaw: buildPlan7,
995
1048
  "claude-code": buildPlan3,
996
- "copilot-cli": buildPlan4,
997
- generic: buildPlan5
1049
+ codex: buildPlan4,
1050
+ "copilot-cli": buildPlan5,
1051
+ generic: buildPlan6
998
1052
  };
999
1053
  var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension"];
1000
1054
  var PI_INSTALL_HINT = "Install pi first: npm install -g --ignore-scripts @earendil-works/pi-coding-agent (https://pi.dev)";
@@ -1008,7 +1062,7 @@ async function runSetup(harness, opts, deps = {
1008
1062
  return 1;
1009
1063
  }
1010
1064
  if (opts.key && (opts.extension || opts.models)) {
1011
- console.error("--key runs the full pi flow; drop --extension/--models.");
1065
+ console.error("--key runs the full pi or opencode flow; drop --extension/--models.");
1012
1066
  return 1;
1013
1067
  }
1014
1068
  const adapter = ADAPTERS[harness];
@@ -1089,7 +1143,7 @@ async function runSetup(harness, opts, deps = {
1089
1143
  };
1090
1144
  let plan;
1091
1145
  try {
1092
- plan = harness === "pi" && opts.key ? buildKeyPlan("<your-api-key>") : adapter(ctx);
1146
+ plan = harness === "pi" && opts.key ? buildKeyPlan2("<your-api-key>") : adapter(ctx);
1093
1147
  } catch (err) {
1094
1148
  console.error(err instanceof Error ? err.message : err);
1095
1149
  return 1;
@@ -1166,7 +1220,7 @@ async function runPiSetup(opts, stored, routingUrl, deps) {
1166
1220
  console.log("\nSkipped. Inside pi, run /login lunaroute to sign in.");
1167
1221
  return 0;
1168
1222
  }
1169
- const plan = combinePlans(buildKeyPlan(creds2.routing_key), stalePiProviderExists() ? buildStaleProviderCleanupPlan() : null);
1223
+ const plan = combinePlans(buildKeyPlan2(creds2.routing_key), stalePiProviderExists() ? buildStaleProviderCleanupPlan() : null);
1170
1224
  const wcode = await applyAndReport(plan, creds2.routing_key, true);
1171
1225
  if (wcode !== 0) return wcode;
1172
1226
  console.log("\nDone. Start pi \u2014 it is signed in. Pick a model with /model.");
@@ -1291,11 +1345,48 @@ async function runHermesConfigSets(spawn, routingUrl, key) {
1291
1345
  async function runOpencodeSetup(stored, settings, routingUrl, opts, deps) {
1292
1346
  try {
1293
1347
  if (opts.print) {
1348
+ const plan = combinePlans(
1349
+ buildPlan(
1350
+ { routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
1351
+ { extension: true }
1352
+ ),
1353
+ opts.key ? buildKeyPlan("<your-api-key>") : null
1354
+ );
1355
+ return applyAndReport(plan, stored?.routing_key ?? "", false);
1356
+ }
1357
+ if (opts.key) {
1358
+ const creds2 = await requireCreds(opts, deps, stored);
1359
+ if (!creds2) return 1;
1360
+ const install = await deps.confirm("Install the LunaRoute OpenCode extension?", {
1361
+ yes: opts.yes
1362
+ });
1363
+ if (!install) {
1364
+ return applyOpencodeProvider(
1365
+ creds2,
1366
+ routingUrl,
1367
+ await deps.confirm("Merge LunaRoute provider into opencode.json (backup kept, other providers preserved)?", {
1368
+ yes: opts.yes
1369
+ })
1370
+ );
1371
+ }
1294
1372
  const plan = buildPlan(
1295
1373
  { routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
1296
1374
  { extension: true }
1297
1375
  );
1298
- return applyAndReport(plan, stored?.routing_key ?? "", false);
1376
+ const code = await applyAndReport(plan, creds2.routing_key, true);
1377
+ if (code !== 0) return code;
1378
+ const store = await deps.confirm(
1379
+ "Store the key in ~/.local/share/opencode/auth.json (replaces any existing LunaRoute login; other credentials preserved)?",
1380
+ { yes: opts.yes }
1381
+ );
1382
+ if (!store) {
1383
+ console.log("\nSkipped. Inside opencode, run /connect to sign in.");
1384
+ return 0;
1385
+ }
1386
+ const wcode = await applyAndReport(buildKeyPlan(creds2.routing_key), creds2.routing_key, true);
1387
+ if (wcode !== 0) return wcode;
1388
+ console.log("\nDone. Start opencode \u2014 it is signed in. Pick a model with /models.");
1389
+ return 0;
1299
1390
  }
1300
1391
  if (await deps.confirm(
1301
1392
  "Install the LunaRoute OpenCode extension (recommended \u2014 /connect login, models auto-sync)?",
@@ -2014,12 +2105,19 @@ function buildRunSpec(ctx) {
2014
2105
 
2015
2106
  // src/run/adapters/codex.ts
2016
2107
  function buildRunSpec2(ctx) {
2108
+ const provider = `{name="LunaRoute",base_url="${ctx.routingUrl}/v1",env_key="LUNAROUTE_API_KEY",wire_api="responses"}`;
2017
2109
  return {
2018
2110
  command: "codex",
2019
- args: ["--model", ctx.model],
2111
+ args: [
2112
+ "-c",
2113
+ 'model_provider="lunaroute"',
2114
+ "-c",
2115
+ `model_providers.lunaroute=${provider}`,
2116
+ "-c",
2117
+ `model="${ctx.model}"`
2118
+ ],
2020
2119
  env: {
2021
- OPENAI_BASE_URL: `${ctx.routingUrl}/v1`,
2022
- OPENAI_API_KEY: ctx.apiKey
2120
+ LUNAROUTE_API_KEY: ctx.apiKey
2023
2121
  }
2024
2122
  };
2025
2123
  }
@@ -2106,7 +2204,7 @@ program.command("whoami").description("Show the signed-in user and organization.
2106
2204
  program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
2107
2205
  logout(program.opts().profile);
2108
2206
  });
2109
- 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) => {
2207
+ program.command("setup <harness>").description("Configure a coding harness (opencode | pi | hermes | claude-code | codex | 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) => {
2110
2208
  const code = await runSetup(harness, {
2111
2209
  profile: program.opts().profile,
2112
2210
  print: opts.print,
@@ -2118,14 +2216,19 @@ program.command("setup <harness>").description("Configure a coding harness (open
2118
2216
  });
2119
2217
  if (code !== 0) process.exit(code);
2120
2218
  });
2121
- program.command("run <harness>").description("Launch a coding harness (claude | claude-code | codex) configured to use LunaRoute for this session.").option("--model <id>", "model id to launch on (default: first model in the catalog)").allowUnknownOption(true).action(async (harness, opts) => {
2122
- const raw = process.argv;
2123
- const ddIdx = raw.indexOf("--");
2124
- const runIdx = raw.indexOf("run");
2125
- const passthrough = ddIdx > runIdx && ddIdx !== -1 ? raw.slice(ddIdx + 1) : [];
2219
+ program.command("run <harness>").description("Launch a coding harness (claude | claude-code | codex) configured to use LunaRoute for this session.").option("--model <id>", "model id to launch on (default: first model in the catalog)").argument("[extra...]").action(async (harness, extra, opts) => {
2220
+ const sep = process.argv.indexOf("--");
2221
+ const post = sep !== -1 ? process.argv.slice(sep + 1) : [];
2222
+ const before = post.length <= extra.length ? extra.slice(0, extra.length - post.length) : extra;
2223
+ if (before.length > 0) {
2224
+ console.error(
2225
+ `Unexpected argument "${before[0]}" \u2014 harness flags go after "--": lunaroute run ${harness} -- ${before.join(" ")}`
2226
+ );
2227
+ process.exit(1);
2228
+ }
2126
2229
  const code = await runRun(
2127
2230
  harness,
2128
- { profile: program.opts().profile, model: opts.model, passthrough }
2231
+ { profile: program.opts().profile, model: opts.model, passthrough: post }
2129
2232
  );
2130
2233
  if (code !== 0) process.exit(code);
2131
2234
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunaroute/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "LunaRoute CLI — configure coding harnesses and manage your LunaRoute account.",
5
5
  "repository": {
6
6
  "type": "git",