@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.
- package/README.md +1 -1
- package/dist/index.js +345 -19
- 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
|
-
|
|
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 =
|
|
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
|
|
541
|
-
// routing edge recognizes the lr_ prefix, authenticates it, and strips
|
|
542
|
-
// header before forwarding upstream — so the key never leaks and
|
|
543
|
-
// sits in the URL path.
|
|
544
|
-
|
|
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
|
|
551
|
-
"If your Claude Code build
|
|
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 "
|
|
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(
|
|
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
|
|
629
|
-
if (!
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|