@lunaroute/cli 0.2.3 → 0.2.5
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 +3 -1
- package/dist/index.js +226 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ Requires Node.js 20+.
|
|
|
18
18
|
lunaroute setup pi # or: opencode | claude-code | copilot-cli | generic
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
One command from a fresh machine: `setup` asks before touching anything — for `pi` it first offers to install the LunaRoute Pi extension
|
|
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
|
+
|
|
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).
|
|
22
24
|
|
|
23
25
|
Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accepts all prompts, and `--print` previews without writing.
|
|
24
26
|
|
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.
|
|
7
|
+
var version = "0.2.5";
|
|
8
8
|
|
|
9
9
|
// src/login.ts
|
|
10
10
|
import { createServer } from "http";
|
|
@@ -359,10 +359,29 @@ function logout(profile) {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
// src/catalog.ts
|
|
362
|
-
|
|
362
|
+
var KeyRejectedError = class extends Error {
|
|
363
|
+
};
|
|
364
|
+
var NON_CHAT_CAPABILITIES = [
|
|
365
|
+
"image_generation",
|
|
366
|
+
"embeddings",
|
|
367
|
+
"rerank"
|
|
368
|
+
];
|
|
369
|
+
function isChatModel(m) {
|
|
370
|
+
for (const c of NON_CHAT_CAPABILITIES) {
|
|
371
|
+
if (m.capabilities?.[c] === true) return false;
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
async function fetchModels(routingUrl, key) {
|
|
363
376
|
const url = `${routingUrl}/v1/models`;
|
|
364
|
-
const
|
|
377
|
+
const headers = key ? { Authorization: `Bearer ${key}` } : {};
|
|
378
|
+
const res = await fetch(url, { method: "GET", headers });
|
|
365
379
|
if (!res.ok) {
|
|
380
|
+
if (res.status === 401) {
|
|
381
|
+
throw new KeyRejectedError(
|
|
382
|
+
key ? `the routing service rejected that key (HTTP 401) \u2014 check that you copied the whole lr_ key` : `the routing service requires a key for /v1/models (HTTP 401) \u2014 run "lunaroute login" or pass --key`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
366
385
|
throw new Error(`failed to fetch models: HTTP ${res.status} from ${url}`);
|
|
367
386
|
}
|
|
368
387
|
const body = await res.json();
|
|
@@ -373,9 +392,9 @@ async function fetchModels(routingUrl) {
|
|
|
373
392
|
max_output_tokens: m.max_output_tokens,
|
|
374
393
|
capabilities: m.capabilities,
|
|
375
394
|
client_compat: m.client_compat
|
|
376
|
-
})).filter((m) => m.id.length > 0);
|
|
395
|
+
})).filter((m) => m.id.length > 0 && isChatModel(m));
|
|
377
396
|
if (models.length === 0) {
|
|
378
|
-
throw new Error(`no models available from ${url}`);
|
|
397
|
+
throw new Error(`no chat models available from ${url}`);
|
|
379
398
|
}
|
|
380
399
|
return models.sort((a, b) => a.id.localeCompare(b.id));
|
|
381
400
|
}
|
|
@@ -578,6 +597,9 @@ function openclawConfigPath() {
|
|
|
578
597
|
function piModelsPath() {
|
|
579
598
|
return join2(homedir2(), ".pi", "agent", "models.json");
|
|
580
599
|
}
|
|
600
|
+
function piAuthPath() {
|
|
601
|
+
return join2(homedir2(), ".pi", "agent", "auth.json");
|
|
602
|
+
}
|
|
581
603
|
var LUNAROUTE_SKILL_NAME = "lunaroute-memory";
|
|
582
604
|
function claudeUserConfigPath() {
|
|
583
605
|
return join2(homedir2(), ".claude.json");
|
|
@@ -688,15 +710,17 @@ function providerPlan(ctx) {
|
|
|
688
710
|
}
|
|
689
711
|
|
|
690
712
|
// src/setup/adapters/pi.ts
|
|
713
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
691
714
|
function buildPiModelEntry(m) {
|
|
715
|
+
if (!m.context_window_tokens || !m.max_output_tokens) return null;
|
|
692
716
|
const reasoning = m.capabilities?.reasoning === true;
|
|
693
717
|
const entry = {
|
|
694
718
|
id: m.id,
|
|
695
719
|
name: m.display_name ?? m.id,
|
|
696
720
|
reasoning,
|
|
697
721
|
input: m.capabilities?.vision ? ["text", "image"] : ["text"],
|
|
698
|
-
contextWindow: m.context_window_tokens
|
|
699
|
-
maxTokens: m.max_output_tokens
|
|
722
|
+
contextWindow: m.context_window_tokens,
|
|
723
|
+
maxTokens: m.max_output_tokens
|
|
700
724
|
};
|
|
701
725
|
if (!reasoning) return entry;
|
|
702
726
|
const pi = m.client_compat?.pi;
|
|
@@ -712,13 +736,32 @@ function buildPiModelEntry(m) {
|
|
|
712
736
|
return entry;
|
|
713
737
|
}
|
|
714
738
|
function buildPlan2(ctx) {
|
|
739
|
+
const skipped = [];
|
|
740
|
+
const models = [];
|
|
741
|
+
for (const m of ctx.models) {
|
|
742
|
+
const entry = buildPiModelEntry(m);
|
|
743
|
+
if (entry) models.push(entry);
|
|
744
|
+
else skipped.push(m.id);
|
|
745
|
+
}
|
|
746
|
+
if (models.length === 0) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
`no LunaRoute model has a context window in the catalog${skipped.length ? ` (skipped: ${skipped.join(", ")})` : ""}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
715
751
|
const block = {
|
|
716
752
|
baseUrl: `${ctx.routingUrl}/v1`,
|
|
717
753
|
api: "openai-completions",
|
|
718
754
|
// Real key; rides the native Authorization: Bearer header.
|
|
719
755
|
apiKey: `$${ctx.keyEnvVar}`,
|
|
720
|
-
models
|
|
756
|
+
models
|
|
721
757
|
};
|
|
758
|
+
const notes = [
|
|
759
|
+
"\npi: open /model to pick a LunaRoute model (models.json hot-reloads).",
|
|
760
|
+
"Tip: the lunaroute-pi-extension auto-registers models from /v1/models so you can skip this file \u2014 `pi install npm:@lunaroute/pi-extension` then `/login lunaroute`."
|
|
761
|
+
];
|
|
762
|
+
if (skipped.length > 0) {
|
|
763
|
+
notes.push(`Skipped (no context window in the catalog): ${skipped.join(", ")}`);
|
|
764
|
+
}
|
|
722
765
|
return {
|
|
723
766
|
fileWrites: [
|
|
724
767
|
{
|
|
@@ -733,12 +776,60 @@ function buildPlan2(ctx) {
|
|
|
733
776
|
}
|
|
734
777
|
],
|
|
735
778
|
exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
|
|
779
|
+
notes
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function stalePiProviderExists() {
|
|
783
|
+
try {
|
|
784
|
+
const obj = JSON.parse(readFileSync3(piModelsPath(), "utf8"));
|
|
785
|
+
return Boolean(obj.providers?.lunaroute);
|
|
786
|
+
} catch {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function buildStaleProviderCleanupPlan() {
|
|
791
|
+
return {
|
|
792
|
+
fileWrites: [
|
|
793
|
+
{
|
|
794
|
+
kind: "json",
|
|
795
|
+
path: piModelsPath(),
|
|
796
|
+
merge: (existing) => {
|
|
797
|
+
const obj = existing ?? {};
|
|
798
|
+
const providers = { ...obj.providers };
|
|
799
|
+
delete providers.lunaroute;
|
|
800
|
+
const next = { ...obj };
|
|
801
|
+
if (Object.keys(providers).length > 0) next.providers = providers;
|
|
802
|
+
else delete next.providers;
|
|
803
|
+
return next;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
exports: [],
|
|
736
808
|
notes: [
|
|
737
|
-
"
|
|
738
|
-
"Tip: the lunaroute-pi-extension auto-registers models from /v1/models so you can skip this file \u2014 `pi install npm:@lunaroute/pi-extension` then `/login lunaroute`."
|
|
809
|
+
"pi: removed the stale providers.lunaroute block from ~/.pi/agent/models.json (backup kept) \u2014 the extension syncs models live."
|
|
739
810
|
]
|
|
740
811
|
};
|
|
741
812
|
}
|
|
813
|
+
var KEY_EXPIRES_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
814
|
+
function buildKeyPlan(key) {
|
|
815
|
+
return {
|
|
816
|
+
fileWrites: [
|
|
817
|
+
{
|
|
818
|
+
kind: "json",
|
|
819
|
+
path: piAuthPath(),
|
|
820
|
+
merge: (existing) => {
|
|
821
|
+
const obj = existing ?? {};
|
|
822
|
+
return {
|
|
823
|
+
...obj,
|
|
824
|
+
lunaroute: { type: "oauth", access: key, refresh: "", expires: Date.now() + KEY_EXPIRES_MS }
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
],
|
|
829
|
+
exports: [],
|
|
830
|
+
notes: ["pi: key stored in ~/.pi/agent/auth.json \u2014 start pi, then /model to pick a lunaroute/* model."]
|
|
831
|
+
};
|
|
832
|
+
}
|
|
742
833
|
|
|
743
834
|
// src/setup/adapters/claudeCode.ts
|
|
744
835
|
function buildPlan3(ctx) {
|
|
@@ -905,7 +996,8 @@ var ADAPTERS = {
|
|
|
905
996
|
"copilot-cli": buildPlan4,
|
|
906
997
|
generic: buildPlan5
|
|
907
998
|
};
|
|
908
|
-
var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension"
|
|
999
|
+
var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension"];
|
|
1000
|
+
var PI_INSTALL_HINT = "Install pi first: npm install -g --ignore-scripts @earendil-works/pi-coding-agent (https://pi.dev)";
|
|
909
1001
|
async function runSetup(harness, opts, deps = {
|
|
910
1002
|
confirm,
|
|
911
1003
|
spawn: makeRealSpawn(),
|
|
@@ -915,6 +1007,10 @@ async function runSetup(harness, opts, deps = {
|
|
|
915
1007
|
console.error("Choose one of --extension or --models.");
|
|
916
1008
|
return 1;
|
|
917
1009
|
}
|
|
1010
|
+
if (opts.key && (opts.extension || opts.models)) {
|
|
1011
|
+
console.error("--key runs the full pi flow; drop --extension/--models.");
|
|
1012
|
+
return 1;
|
|
1013
|
+
}
|
|
918
1014
|
const adapter = ADAPTERS[harness];
|
|
919
1015
|
if (!adapter && harness !== "hermes") {
|
|
920
1016
|
console.error(
|
|
@@ -922,8 +1018,14 @@ async function runSetup(harness, opts, deps = {
|
|
|
922
1018
|
);
|
|
923
1019
|
return 1;
|
|
924
1020
|
}
|
|
925
|
-
|
|
1021
|
+
let stored = loadProfile(opts.profile);
|
|
926
1022
|
const settings = resolveSettings(opts.profile);
|
|
1023
|
+
if (opts.key && !LR_KEY_RE.test(opts.key)) {
|
|
1024
|
+
console.error(
|
|
1025
|
+
"Invalid key: a LunaRoute routing key starts with lr_ (copy the whole key from the dashboard)."
|
|
1026
|
+
);
|
|
1027
|
+
return 1;
|
|
1028
|
+
}
|
|
927
1029
|
const routingUrlRaw = opts.routingUrl || stored?.routing_url || settings.routing_url;
|
|
928
1030
|
if (!routingUrlRaw) {
|
|
929
1031
|
console.error("No routing URL in profile; pass --routing-url.");
|
|
@@ -936,6 +1038,29 @@ async function runSetup(harness, opts, deps = {
|
|
|
936
1038
|
console.error(err instanceof Error ? err.message : err);
|
|
937
1039
|
return 1;
|
|
938
1040
|
}
|
|
1041
|
+
if (opts.key) {
|
|
1042
|
+
try {
|
|
1043
|
+
await fetchModels(routingUrl, opts.key);
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
if (err instanceof KeyRejectedError) {
|
|
1046
|
+
console.error(err.message);
|
|
1047
|
+
return 1;
|
|
1048
|
+
}
|
|
1049
|
+
console.warn(`Note: ${err instanceof Error ? err.message : err}`);
|
|
1050
|
+
console.warn(
|
|
1051
|
+
"Continuing without a synced catalog. If your organization has no chat models enabled yet, contact hello@lunaroute.com \u2014 models appear on the next refresh once they are on."
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
saveProfile(opts.profile, {
|
|
1055
|
+
api_url: settings.api_url,
|
|
1056
|
+
routing_url: opts.routingUrl || settings.routing_url,
|
|
1057
|
+
front_url: settings.front_url,
|
|
1058
|
+
org_id: stored?.org_id ?? "",
|
|
1059
|
+
user_email: stored?.user_email ?? "",
|
|
1060
|
+
routing_key: opts.key
|
|
1061
|
+
});
|
|
1062
|
+
stored = loadProfile(opts.profile);
|
|
1063
|
+
}
|
|
939
1064
|
if (harness === "pi" && !opts.print) {
|
|
940
1065
|
return runPiSetup(opts, stored, routingUrl, deps);
|
|
941
1066
|
}
|
|
@@ -949,10 +1074,12 @@ async function runSetup(harness, opts, deps = {
|
|
|
949
1074
|
}
|
|
950
1075
|
let models;
|
|
951
1076
|
try {
|
|
952
|
-
models = await fetchModels(routingUrl);
|
|
1077
|
+
models = await fetchModels(routingUrl, creds.routing_key);
|
|
953
1078
|
} catch (err) {
|
|
954
|
-
console.error(
|
|
955
|
-
|
|
1079
|
+
console.error(
|
|
1080
|
+
`Could not fetch the model catalog (${err instanceof Error ? err.message : err}) \u2014 continuing; model ids show as <model> until it answers.`
|
|
1081
|
+
);
|
|
1082
|
+
models = [];
|
|
956
1083
|
}
|
|
957
1084
|
const ctx = {
|
|
958
1085
|
routingUrl,
|
|
@@ -962,7 +1089,7 @@ async function runSetup(harness, opts, deps = {
|
|
|
962
1089
|
};
|
|
963
1090
|
let plan;
|
|
964
1091
|
try {
|
|
965
|
-
plan = adapter(ctx);
|
|
1092
|
+
plan = harness === "pi" && opts.key ? buildKeyPlan("<your-api-key>") : adapter(ctx);
|
|
966
1093
|
} catch (err) {
|
|
967
1094
|
console.error(err instanceof Error ? err.message : err);
|
|
968
1095
|
return 1;
|
|
@@ -981,6 +1108,7 @@ Wrote: ${summary.written.join(", ")}`);
|
|
|
981
1108
|
}
|
|
982
1109
|
}
|
|
983
1110
|
var NOT_LOGGED_IN = 'Not logged in. Run "lunaroute login" first.';
|
|
1111
|
+
var LR_KEY_RE = /^lr_[A-Za-z0-9_-]{8,128}$/;
|
|
984
1112
|
var OPENCODE_LOGIN_HINT = "Or re-run and choose the extension \u2014 it logs in via /connect inside opencode.";
|
|
985
1113
|
var PI_LOGIN_HINT = "Or re-run and choose the extension \u2014 login happens via /login lunaroute inside pi.";
|
|
986
1114
|
async function requireCreds(opts, deps, stored, hint) {
|
|
@@ -1014,9 +1142,39 @@ async function requireCreds(opts, deps, stored, hint) {
|
|
|
1014
1142
|
}
|
|
1015
1143
|
async function runPiSetup(opts, stored, routingUrl, deps) {
|
|
1016
1144
|
try {
|
|
1145
|
+
if (opts.key) {
|
|
1146
|
+
const creds2 = await requireCreds(opts, deps, stored);
|
|
1147
|
+
if (!creds2) return 1;
|
|
1148
|
+
const install = await deps.confirm(
|
|
1149
|
+
"Install the LunaRoute Pi extension via 'pi install'?",
|
|
1150
|
+
{ yes: opts.yes }
|
|
1151
|
+
);
|
|
1152
|
+
if (!install) {
|
|
1153
|
+
return applyPiModels(
|
|
1154
|
+
creds2,
|
|
1155
|
+
routingUrl,
|
|
1156
|
+
await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", { yes: opts.yes })
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
const code = await installPiPackages(deps);
|
|
1160
|
+
if (code !== 0) return code;
|
|
1161
|
+
const store = await deps.confirm(
|
|
1162
|
+
"Store the key in ~/.pi/agent/auth.json (replaces any existing LunaRoute login; other credentials preserved)?",
|
|
1163
|
+
{ yes: opts.yes }
|
|
1164
|
+
);
|
|
1165
|
+
if (!store) {
|
|
1166
|
+
console.log("\nSkipped. Inside pi, run /login lunaroute to sign in.");
|
|
1167
|
+
return 0;
|
|
1168
|
+
}
|
|
1169
|
+
const plan = combinePlans(buildKeyPlan(creds2.routing_key), stalePiProviderExists() ? buildStaleProviderCleanupPlan() : null);
|
|
1170
|
+
const wcode = await applyAndReport(plan, creds2.routing_key, true);
|
|
1171
|
+
if (wcode !== 0) return wcode;
|
|
1172
|
+
console.log("\nDone. Start pi \u2014 it is signed in. Pick a model with /model.");
|
|
1173
|
+
return 0;
|
|
1174
|
+
}
|
|
1017
1175
|
if (opts.extension) {
|
|
1018
1176
|
const ok = await deps.confirm(
|
|
1019
|
-
"Install the LunaRoute Pi extension
|
|
1177
|
+
"Install the LunaRoute Pi extension via 'pi install'?",
|
|
1020
1178
|
{ yes: opts.yes }
|
|
1021
1179
|
);
|
|
1022
1180
|
if (!ok) {
|
|
@@ -1036,7 +1194,7 @@ async function runPiSetup(opts, stored, routingUrl, deps) {
|
|
|
1036
1194
|
);
|
|
1037
1195
|
}
|
|
1038
1196
|
if (await deps.confirm(
|
|
1039
|
-
"Install the LunaRoute Pi extension
|
|
1197
|
+
"Install the LunaRoute Pi extension via 'pi install' (recommended \u2014 auto-registers models)?",
|
|
1040
1198
|
{ yes: opts.yes }
|
|
1041
1199
|
)) {
|
|
1042
1200
|
return installPiExtension(deps, opts);
|
|
@@ -1178,7 +1336,7 @@ async function runOpencodeSetup(stored, settings, routingUrl, opts, deps) {
|
|
|
1178
1336
|
async function applyOpencodeProvider(creds, routingUrl, write) {
|
|
1179
1337
|
let models;
|
|
1180
1338
|
try {
|
|
1181
|
-
models = await fetchModels(routingUrl);
|
|
1339
|
+
models = await fetchModels(routingUrl, creds.routing_key);
|
|
1182
1340
|
} catch (err) {
|
|
1183
1341
|
console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
|
|
1184
1342
|
return 1;
|
|
@@ -1191,6 +1349,14 @@ async function applyOpencodeProvider(creds, routingUrl, write) {
|
|
|
1191
1349
|
});
|
|
1192
1350
|
return applyAndReport(plan, creds.routing_key, write);
|
|
1193
1351
|
}
|
|
1352
|
+
function combinePlans(a, b) {
|
|
1353
|
+
if (!b) return a;
|
|
1354
|
+
return {
|
|
1355
|
+
fileWrites: [...a.fileWrites, ...b.fileWrites],
|
|
1356
|
+
exports: [...a.exports, ...b.exports],
|
|
1357
|
+
notes: [...a.notes, ...b.notes]
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1194
1360
|
async function applyAndReport(plan, key, write) {
|
|
1195
1361
|
try {
|
|
1196
1362
|
const summary = await applyPlan(plan, { print: !write, key });
|
|
@@ -1222,16 +1388,12 @@ async function launchOpencode(spawn) {
|
|
|
1222
1388
|
});
|
|
1223
1389
|
});
|
|
1224
1390
|
}
|
|
1225
|
-
async function
|
|
1391
|
+
async function installPiPackages(deps) {
|
|
1226
1392
|
for (const pkg of PI_INSTALL_PACKAGES) {
|
|
1227
1393
|
const code = await new Promise((resolve) => {
|
|
1228
1394
|
const child = deps.spawn("pi", ["install", pkg]);
|
|
1229
1395
|
child.on("error", (err) => {
|
|
1230
|
-
const { message, exitCode } = describeSpawnError(
|
|
1231
|
-
"pi",
|
|
1232
|
-
"Install pi first: https://pi.dev",
|
|
1233
|
-
err
|
|
1234
|
-
);
|
|
1396
|
+
const { message, exitCode } = describeSpawnError("pi", PI_INSTALL_HINT, err);
|
|
1235
1397
|
console.error(message);
|
|
1236
1398
|
resolve(exitCode);
|
|
1237
1399
|
});
|
|
@@ -1242,11 +1404,26 @@ async function installPiExtension(deps, opts) {
|
|
|
1242
1404
|
if (code !== 0) {
|
|
1243
1405
|
const done = PI_INSTALL_PACKAGES.slice(0, PI_INSTALL_PACKAGES.indexOf(pkg));
|
|
1244
1406
|
console.error(
|
|
1245
|
-
`'pi install ${pkg}' failed (exit ${code}). Installed so far: ${done.length ? done.join(", ") : "nothing"}. Re-run
|
|
1407
|
+
`'pi install ${pkg}' failed (exit ${code}). Installed so far: ${done.length ? done.join(", ") : "nothing"}. Re-run the same command to retry.`
|
|
1246
1408
|
);
|
|
1247
1409
|
return code || 1;
|
|
1248
1410
|
}
|
|
1249
1411
|
}
|
|
1412
|
+
return 0;
|
|
1413
|
+
}
|
|
1414
|
+
async function installPiExtension(deps, opts) {
|
|
1415
|
+
const code = await installPiPackages(deps);
|
|
1416
|
+
if (code !== 0) return code;
|
|
1417
|
+
if (stalePiProviderExists()) {
|
|
1418
|
+
const remove = await deps.confirm(
|
|
1419
|
+
"Remove the stale LunaRoute block from ~/.pi/agent/models.json (backup kept, other providers preserved)?",
|
|
1420
|
+
{ yes: opts.yes }
|
|
1421
|
+
);
|
|
1422
|
+
if (remove) {
|
|
1423
|
+
const ccode = await applyAndReport(buildStaleProviderCleanupPlan(), "", true);
|
|
1424
|
+
if (ccode !== 0) return ccode;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1250
1427
|
if (await deps.confirm("Start pi now? (then run /login lunaroute to sign in)", { yes: opts.yes })) {
|
|
1251
1428
|
return launchPi(deps.spawn);
|
|
1252
1429
|
}
|
|
@@ -1259,11 +1436,7 @@ async function launchPi(spawn) {
|
|
|
1259
1436
|
return new Promise((resolve) => {
|
|
1260
1437
|
const child = spawn("pi", []);
|
|
1261
1438
|
child.on("error", (err) => {
|
|
1262
|
-
const { message, exitCode } = describeSpawnError(
|
|
1263
|
-
"pi",
|
|
1264
|
-
"Install pi first: https://pi.dev",
|
|
1265
|
-
err
|
|
1266
|
-
);
|
|
1439
|
+
const { message, exitCode } = describeSpawnError("pi", PI_INSTALL_HINT, err);
|
|
1267
1440
|
console.error(message);
|
|
1268
1441
|
resolve(exitCode);
|
|
1269
1442
|
});
|
|
@@ -1275,7 +1448,7 @@ async function launchPi(spawn) {
|
|
|
1275
1448
|
async function applyPiModels(creds, routingUrl, write) {
|
|
1276
1449
|
let models;
|
|
1277
1450
|
try {
|
|
1278
|
-
models = await fetchModels(routingUrl);
|
|
1451
|
+
models = await fetchModels(routingUrl, creds.routing_key);
|
|
1279
1452
|
} catch (err) {
|
|
1280
1453
|
console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
|
|
1281
1454
|
return 1;
|
|
@@ -1316,6 +1489,8 @@ async function runModels(profile, opts) {
|
|
|
1316
1489
|
}
|
|
1317
1490
|
if (opts.json) {
|
|
1318
1491
|
console.log(JSON.stringify(ids, null, 2));
|
|
1492
|
+
} else if (ids.length === 0) {
|
|
1493
|
+
console.log("// no models enabled for your org yet \u2014 contact hello@lunaroute.com to get models enabled");
|
|
1319
1494
|
} else {
|
|
1320
1495
|
for (const id of ids) console.log(id);
|
|
1321
1496
|
}
|
|
@@ -1350,6 +1525,10 @@ async function runPricing(profile, opts) {
|
|
|
1350
1525
|
console.log(JSON.stringify(rows, null, 2));
|
|
1351
1526
|
return 0;
|
|
1352
1527
|
}
|
|
1528
|
+
if (rows.length === 0) {
|
|
1529
|
+
console.log("// no models enabled for your org yet \u2014 contact hello@lunaroute.com to get models enabled");
|
|
1530
|
+
return 0;
|
|
1531
|
+
}
|
|
1353
1532
|
const cell = (n) => n === void 0 ? "\u2014" : String(n);
|
|
1354
1533
|
const table = renderTable(
|
|
1355
1534
|
["MODEL", "INPUT (cr/M)", "OUTPUT (cr/M)", "CACHED (cr/M)"],
|
|
@@ -1384,6 +1563,9 @@ async function runUsage(profile, opts) {
|
|
|
1384
1563
|
console.log(
|
|
1385
1564
|
`Wallet: available ${w.available_credits} (balance ${w.balance_credits}, reserved ${w.reserved_credits})`
|
|
1386
1565
|
);
|
|
1566
|
+
if (w.available_credits === 0) {
|
|
1567
|
+
console.log("// wallet is empty \u2014 add credits in the dashboard \u2192 Billing");
|
|
1568
|
+
}
|
|
1387
1569
|
console.log("");
|
|
1388
1570
|
const table = renderTable(
|
|
1389
1571
|
["TIME", "MODEL", "IN", "OUT", "CACHED", "\u0394CREDITS", "BALANCE"],
|
|
@@ -1872,7 +2054,7 @@ async function runRun(harness, opts, deps = realDeps) {
|
|
|
1872
2054
|
if (!model) {
|
|
1873
2055
|
let models;
|
|
1874
2056
|
try {
|
|
1875
|
-
models = await deps.fetchModels(creds.routing_url);
|
|
2057
|
+
models = await deps.fetchModels(creds.routing_url, creds.routing_key);
|
|
1876
2058
|
} catch (err) {
|
|
1877
2059
|
console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
|
|
1878
2060
|
return 1;
|
|
@@ -1905,6 +2087,14 @@ async function runRun(harness, opts, deps = realDeps) {
|
|
|
1905
2087
|
}
|
|
1906
2088
|
|
|
1907
2089
|
// src/index.ts
|
|
2090
|
+
var MIN_NODE_MAJOR = 20;
|
|
2091
|
+
var nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
2092
|
+
if (nodeMajor < MIN_NODE_MAJOR) {
|
|
2093
|
+
console.error(
|
|
2094
|
+
`LunaRoute CLI requires Node.js ${MIN_NODE_MAJOR}+ \u2014 you're running ${process.versions.node}. Upgrade from https://nodejs.org/en/download and try again.`
|
|
2095
|
+
);
|
|
2096
|
+
process.exit(1);
|
|
2097
|
+
}
|
|
1908
2098
|
var program = new Command();
|
|
1909
2099
|
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");
|
|
1910
2100
|
program.command("login").description("Authorize this device via the browser and store a routing key.").action(async () => {
|
|
@@ -1916,14 +2106,15 @@ program.command("whoami").description("Show the signed-in user and organization.
|
|
|
1916
2106
|
program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
|
|
1917
2107
|
logout(program.opts().profile);
|
|
1918
2108
|
});
|
|
1919
|
-
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
|
|
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) => {
|
|
1920
2110
|
const code = await runSetup(harness, {
|
|
1921
2111
|
profile: program.opts().profile,
|
|
1922
2112
|
print: opts.print,
|
|
1923
2113
|
routingUrl: opts.routingUrl,
|
|
1924
2114
|
yes: opts.yes,
|
|
1925
2115
|
extension: opts.extension,
|
|
1926
|
-
models: opts.models
|
|
2116
|
+
models: opts.models,
|
|
2117
|
+
key: opts.key
|
|
1927
2118
|
});
|
|
1928
2119
|
if (code !== 0) process.exit(code);
|
|
1929
2120
|
});
|