@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.
- package/dist/index.js +97 -14
- 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
|
|
592
|
-
// routing edge recognizes the lr_ prefix, authenticates it, and strips
|
|
593
|
-
// header before forwarding upstream — so the key never leaks and
|
|
594
|
-
// sits in the URL path.
|
|
595
|
-
|
|
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
|
|
602
|
-
"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.",
|
|
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 "
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|