@lunaroute/cli 0.2.1 → 0.2.2
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 +2 -3
- package/dist/index.js +329 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,10 @@ Requires Node.js 20+.
|
|
|
15
15
|
## Quickstart
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
lunaroute
|
|
19
|
-
lunaroute setup pi # or: claude-code | opencode | copilot-cli | generic
|
|
18
|
+
lunaroute setup pi # or: opencode | claude-code | copilot-cli | generic
|
|
20
19
|
```
|
|
21
20
|
|
|
22
|
-
`setup` asks before touching anything
|
|
21
|
+
One command from a fresh machine: `setup` asks before touching anything — for `pi` it first offers to install the LunaRoute Pi extension + MCP adapter (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.
|
|
23
22
|
|
|
24
23
|
Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accepts all prompts, and `--print` previews without writing.
|
|
25
24
|
|
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.2";
|
|
8
8
|
|
|
9
9
|
// src/login.ts
|
|
10
10
|
import { createServer } from "http";
|
|
@@ -78,6 +78,71 @@ async function getUsage(ctx, limit) {
|
|
|
78
78
|
const q = limit ? `?limit=${limit}` : "";
|
|
79
79
|
return cliGet(ctx, `/v1/cli/usage${q}`);
|
|
80
80
|
}
|
|
81
|
+
async function cliRequestRaw(ctx, method, path, body) {
|
|
82
|
+
const res = await fetch(`${ctx.apiUrl}${path}`, {
|
|
83
|
+
method,
|
|
84
|
+
headers: {
|
|
85
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
86
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
87
|
+
},
|
|
88
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
let detail = `HTTP ${res.status}`;
|
|
92
|
+
let envelope = false;
|
|
93
|
+
try {
|
|
94
|
+
const parsed2 = await res.json();
|
|
95
|
+
const code = parsed2?.error?.code;
|
|
96
|
+
const message = parsed2?.error?.message;
|
|
97
|
+
if (code) {
|
|
98
|
+
envelope = true;
|
|
99
|
+
detail = message ? `${code}: ${message}` : code;
|
|
100
|
+
} else if (message) {
|
|
101
|
+
detail = message;
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
if (res.status === 404 && !envelope) {
|
|
106
|
+
throw new Error("this server does not support search-keys (server upgrade required)");
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`request failed: ${detail}`);
|
|
109
|
+
}
|
|
110
|
+
const text = await res.text();
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(text);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error("request failed: unexpected response body");
|
|
116
|
+
}
|
|
117
|
+
if (parsed?.success === false && parsed.error?.code) {
|
|
118
|
+
const { code, message } = parsed.error;
|
|
119
|
+
throw new Error(`request failed: ${message ? `${code}: ${message}` : code}`);
|
|
120
|
+
}
|
|
121
|
+
if (parsed?.success !== true || parsed.data === void 0) {
|
|
122
|
+
throw new Error("request failed: unexpected response body");
|
|
123
|
+
}
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
async function cliRequest(ctx, method, path, body) {
|
|
127
|
+
const text = await cliRequestRaw(ctx, method, path, body);
|
|
128
|
+
const parsed = JSON.parse(text);
|
|
129
|
+
return parsed.data;
|
|
130
|
+
}
|
|
131
|
+
async function listSearchProviders(ctx) {
|
|
132
|
+
return cliRequest(ctx, "GET", "/v1/cli/search-providers");
|
|
133
|
+
}
|
|
134
|
+
async function listSearchKeys(ctx) {
|
|
135
|
+
return cliRequest(ctx, "GET", "/v1/cli/search-provider-keys");
|
|
136
|
+
}
|
|
137
|
+
async function listSearchKeysRaw(ctx) {
|
|
138
|
+
return cliRequestRaw(ctx, "GET", "/v1/cli/search-provider-keys");
|
|
139
|
+
}
|
|
140
|
+
async function putSearchKey(ctx, provider, apiKey) {
|
|
141
|
+
return cliRequest(ctx, "PUT", "/v1/cli/search-provider-keys", { provider, api_key: apiKey });
|
|
142
|
+
}
|
|
143
|
+
async function deleteSearchKey(ctx, provider) {
|
|
144
|
+
await cliRequest(ctx, "DELETE", `/v1/cli/search-provider-keys?provider=${encodeURIComponent(provider)}`);
|
|
145
|
+
}
|
|
81
146
|
|
|
82
147
|
// src/config.ts
|
|
83
148
|
import { homedir } from "os";
|
|
@@ -206,6 +271,7 @@ async function startLoopbackServer() {
|
|
|
206
271
|
const state = url.searchParams.get("state") ?? "";
|
|
207
272
|
res.statusCode = 200;
|
|
208
273
|
res.setHeader("Content-Type", "text/html");
|
|
274
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
209
275
|
res.end(
|
|
210
276
|
"<html><body><h2>LunaRoute CLI authorized.</h2><p>You can close this tab and return to your terminal.</p></body></html>"
|
|
211
277
|
);
|
|
@@ -400,6 +466,75 @@ async function confirm(question, opts = {}) {
|
|
|
400
466
|
rl.close();
|
|
401
467
|
}
|
|
402
468
|
}
|
|
469
|
+
async function promptInput(question, opts = {}) {
|
|
470
|
+
if (!process.stdin.isTTY && opts.default === void 0) {
|
|
471
|
+
throw new NonInteractiveTerminalError(`${question} requires a TTY \u2014 pass a flag instead`);
|
|
472
|
+
}
|
|
473
|
+
if (!process.stdin.isTTY) {
|
|
474
|
+
return opts.default;
|
|
475
|
+
}
|
|
476
|
+
process.stdout.write(question + (opts.default !== void 0 ? ` [${opts.default}] ` : ": "));
|
|
477
|
+
const raw = !!opts.hidden && !!process.stdin.isTTY;
|
|
478
|
+
if (raw) {
|
|
479
|
+
process.stdin.setRawMode(true);
|
|
480
|
+
}
|
|
481
|
+
const chunks = [];
|
|
482
|
+
process.stdin.resume();
|
|
483
|
+
await new Promise((resolve) => {
|
|
484
|
+
const off = () => {
|
|
485
|
+
process.stdin.removeListener("data", onData);
|
|
486
|
+
process.stdin.pause();
|
|
487
|
+
};
|
|
488
|
+
const trimOneCodepoint = () => {
|
|
489
|
+
const all = Buffer.concat(chunks);
|
|
490
|
+
let end = all.length;
|
|
491
|
+
while (end > 0 && (all[end - 1] & 192) === 128) end--;
|
|
492
|
+
if (end > 0) end--;
|
|
493
|
+
chunks.length = 0;
|
|
494
|
+
if (end > 0) chunks.push(all.subarray(0, end));
|
|
495
|
+
};
|
|
496
|
+
const onData = (c) => {
|
|
497
|
+
let i = 0;
|
|
498
|
+
let runStart = -1;
|
|
499
|
+
const flushRun = (upto) => {
|
|
500
|
+
if (runStart >= 0) {
|
|
501
|
+
chunks.push(c.subarray(runStart, upto));
|
|
502
|
+
runStart = -1;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
while (i < c.length) {
|
|
506
|
+
const b = c[i];
|
|
507
|
+
if (raw && b === 3) {
|
|
508
|
+
off();
|
|
509
|
+
process.stdin.setRawMode(false);
|
|
510
|
+
process.exit(130);
|
|
511
|
+
}
|
|
512
|
+
if (b === 13 || b === 10) {
|
|
513
|
+
flushRun(i);
|
|
514
|
+
off();
|
|
515
|
+
resolve();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (raw && (b === 127 || b === 8)) {
|
|
519
|
+
flushRun(i);
|
|
520
|
+
trimOneCodepoint();
|
|
521
|
+
i++;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (runStart < 0) runStart = i;
|
|
525
|
+
i++;
|
|
526
|
+
}
|
|
527
|
+
flushRun(c.length);
|
|
528
|
+
};
|
|
529
|
+
process.stdin.on("data", onData);
|
|
530
|
+
});
|
|
531
|
+
if (raw) {
|
|
532
|
+
process.stdin.setRawMode(false);
|
|
533
|
+
process.stdout.write("\n");
|
|
534
|
+
}
|
|
535
|
+
const value = Buffer.concat(chunks).toString("utf8").trim();
|
|
536
|
+
return value === "" && opts.default !== void 0 ? opts.default : value;
|
|
537
|
+
}
|
|
403
538
|
|
|
404
539
|
// src/commands/setup.ts
|
|
405
540
|
import { spawn } from "child_process";
|
|
@@ -657,24 +792,24 @@ function buildPlan5(ctx) {
|
|
|
657
792
|
|
|
658
793
|
// src/setup/routing-url.ts
|
|
659
794
|
function validatedRoutingUrl(routingUrl) {
|
|
660
|
-
function
|
|
795
|
+
function fail2(reason) {
|
|
661
796
|
throw new Error(
|
|
662
797
|
`routingUrl rejected (reason: ${reason}; <redacted URL>) \u2014 check --routing-url or the profile's routing URL`
|
|
663
798
|
);
|
|
664
799
|
}
|
|
665
|
-
if (!/^https?:\/\/[^/?#]/.test(routingUrl))
|
|
666
|
-
if (/[?#]/.test(routingUrl))
|
|
667
|
-
if (routingUrl !== routingUrl.trim())
|
|
668
|
-
if (routingUrl.endsWith("/"))
|
|
800
|
+
if (!/^https?:\/\/[^/?#]/.test(routingUrl)) fail2("invalid-url");
|
|
801
|
+
if (/[?#]/.test(routingUrl)) fail2("query-or-fragment");
|
|
802
|
+
if (routingUrl !== routingUrl.trim()) fail2("whitespace");
|
|
803
|
+
if (routingUrl.endsWith("/")) fail2("trailing-slash");
|
|
669
804
|
let parsed;
|
|
670
805
|
try {
|
|
671
806
|
parsed = new URL(routingUrl);
|
|
672
807
|
} catch {
|
|
673
|
-
|
|
808
|
+
fail2("invalid-url");
|
|
674
809
|
}
|
|
675
|
-
if (parsed.username !== "" || parsed.password !== "")
|
|
676
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
677
|
-
if (parsed.hostname === "")
|
|
810
|
+
if (parsed.username !== "" || parsed.password !== "") fail2("userinfo");
|
|
811
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail2("invalid-url");
|
|
812
|
+
if (parsed.hostname === "") fail2("invalid-url");
|
|
678
813
|
return routingUrl;
|
|
679
814
|
}
|
|
680
815
|
|
|
@@ -750,7 +885,8 @@ var ADAPTERS = {
|
|
|
750
885
|
var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension", "npm:pi-mcp-adapter"];
|
|
751
886
|
async function runSetup(harness, opts, deps = {
|
|
752
887
|
confirm,
|
|
753
|
-
spawn: (command, args) => spawn(command, args, { stdio: "inherit" })
|
|
888
|
+
spawn: (command, args) => spawn(command, args, { stdio: "inherit" }),
|
|
889
|
+
runLogin
|
|
754
890
|
}) {
|
|
755
891
|
if (harness === "pi" && opts.extension && opts.models) {
|
|
756
892
|
console.error("Choose one of --extension or --models.");
|
|
@@ -763,12 +899,9 @@ async function runSetup(harness, opts, deps = {
|
|
|
763
899
|
);
|
|
764
900
|
return 1;
|
|
765
901
|
}
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return 1;
|
|
770
|
-
}
|
|
771
|
-
const routingUrlRaw = opts.routingUrl || creds.routing_url;
|
|
902
|
+
const stored = loadProfile(opts.profile);
|
|
903
|
+
const settings = resolveSettings(opts.profile);
|
|
904
|
+
const routingUrlRaw = opts.routingUrl || stored?.routing_url || settings.routing_url;
|
|
772
905
|
if (!routingUrlRaw) {
|
|
773
906
|
console.error("No routing URL in profile; pass --routing-url.");
|
|
774
907
|
return 1;
|
|
@@ -781,11 +914,13 @@ async function runSetup(harness, opts, deps = {
|
|
|
781
914
|
return 1;
|
|
782
915
|
}
|
|
783
916
|
if (harness === "pi" && !opts.print) {
|
|
784
|
-
return runPiSetup(opts, deps);
|
|
917
|
+
return runPiSetup(opts, stored, routingUrl, deps);
|
|
785
918
|
}
|
|
786
919
|
if (harness === "opencode") {
|
|
787
|
-
return runOpencodeSetup(
|
|
920
|
+
return runOpencodeSetup(stored, settings, routingUrl, opts, deps);
|
|
788
921
|
}
|
|
922
|
+
const creds = await requireCreds(opts, deps, stored);
|
|
923
|
+
if (!creds) return 1;
|
|
789
924
|
if (harness === "hermes") {
|
|
790
925
|
return runHermesSetup(creds, routingUrl, opts, deps);
|
|
791
926
|
}
|
|
@@ -822,7 +957,39 @@ Wrote: ${summary.written.join(", ")}`);
|
|
|
822
957
|
return 1;
|
|
823
958
|
}
|
|
824
959
|
}
|
|
825
|
-
|
|
960
|
+
var NOT_LOGGED_IN = 'Not logged in. Run "lunaroute login" first.';
|
|
961
|
+
var OPENCODE_LOGIN_HINT = "Or re-run and choose the extension \u2014 it logs in via /connect inside opencode.";
|
|
962
|
+
var PI_LOGIN_HINT = "Or re-run and choose the extension \u2014 login happens via /login lunaroute inside pi.";
|
|
963
|
+
async function requireCreds(opts, deps, stored, hint) {
|
|
964
|
+
if (stored?.routing_key) return stored;
|
|
965
|
+
const decline = [NOT_LOGGED_IN, hint].filter(Boolean).join(" ");
|
|
966
|
+
try {
|
|
967
|
+
const ok = await deps.confirm('Not logged in. Run "lunaroute login" now? (opens your browser)');
|
|
968
|
+
if (!ok) {
|
|
969
|
+
console.error(decline);
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
} catch (err) {
|
|
973
|
+
if (err instanceof NonInteractiveTerminalError) {
|
|
974
|
+
console.error(decline);
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
throw err;
|
|
978
|
+
}
|
|
979
|
+
try {
|
|
980
|
+
await deps.runLogin(opts.profile);
|
|
981
|
+
} catch (err) {
|
|
982
|
+
console.error(`Login failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
const fresh = loadProfile(opts.profile);
|
|
986
|
+
if (!fresh?.routing_key) {
|
|
987
|
+
console.error(NOT_LOGGED_IN);
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
return fresh;
|
|
991
|
+
}
|
|
992
|
+
async function runPiSetup(opts, stored, routingUrl, deps) {
|
|
826
993
|
try {
|
|
827
994
|
if (opts.extension) {
|
|
828
995
|
const ok = await deps.confirm(
|
|
@@ -834,19 +1001,28 @@ async function runPiSetup(opts, deps) {
|
|
|
834
1001
|
console.log(` pi install ${PI_INSTALL_PACKAGES.join(" && pi install ")}`);
|
|
835
1002
|
return 0;
|
|
836
1003
|
}
|
|
837
|
-
return installPiExtension(deps
|
|
1004
|
+
return installPiExtension(deps, opts);
|
|
838
1005
|
}
|
|
839
1006
|
if (opts.models) {
|
|
840
|
-
|
|
1007
|
+
const creds2 = await requireCreds(opts, deps, stored, PI_LOGIN_HINT);
|
|
1008
|
+
if (!creds2) return 1;
|
|
1009
|
+
return applyPiModels(
|
|
1010
|
+
creds2,
|
|
1011
|
+
routingUrl,
|
|
1012
|
+
await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", { yes: opts.yes })
|
|
1013
|
+
);
|
|
841
1014
|
}
|
|
842
1015
|
if (await deps.confirm(
|
|
843
1016
|
"Install the LunaRoute Pi extension + MCP adapter via 'pi install' (recommended \u2014 auto-registers models)?",
|
|
844
1017
|
{ yes: opts.yes }
|
|
845
1018
|
)) {
|
|
846
|
-
return installPiExtension(deps
|
|
1019
|
+
return installPiExtension(deps, opts);
|
|
847
1020
|
}
|
|
1021
|
+
const creds = await requireCreds(opts, deps, stored, PI_LOGIN_HINT);
|
|
1022
|
+
if (!creds) return 1;
|
|
848
1023
|
return applyPiModels(
|
|
849
|
-
|
|
1024
|
+
creds,
|
|
1025
|
+
routingUrl,
|
|
850
1026
|
await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", {
|
|
851
1027
|
yes: opts.yes
|
|
852
1028
|
})
|
|
@@ -934,24 +1110,24 @@ async function runHermesConfigSets(spawn3, routingUrl, key) {
|
|
|
934
1110
|
console.log(" hermes model # interactive provider/model picker");
|
|
935
1111
|
return 0;
|
|
936
1112
|
}
|
|
937
|
-
async function runOpencodeSetup(
|
|
1113
|
+
async function runOpencodeSetup(stored, settings, routingUrl, opts, deps) {
|
|
938
1114
|
try {
|
|
939
1115
|
if (opts.print) {
|
|
940
1116
|
const plan = buildPlan(
|
|
941
|
-
{ routingUrl, orgId:
|
|
1117
|
+
{ routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
|
|
942
1118
|
{ extension: true }
|
|
943
1119
|
);
|
|
944
|
-
return applyAndReport(plan,
|
|
1120
|
+
return applyAndReport(plan, stored?.routing_key ?? "", false);
|
|
945
1121
|
}
|
|
946
1122
|
if (await deps.confirm(
|
|
947
1123
|
"Install the LunaRoute OpenCode extension (recommended \u2014 /connect login, models auto-sync)?",
|
|
948
1124
|
{ yes: opts.yes }
|
|
949
1125
|
)) {
|
|
950
1126
|
const plan = buildPlan(
|
|
951
|
-
{ routingUrl, orgId:
|
|
1127
|
+
{ routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
|
|
952
1128
|
{ extension: true }
|
|
953
1129
|
);
|
|
954
|
-
const code = await applyAndReport(plan,
|
|
1130
|
+
const code = await applyAndReport(plan, stored?.routing_key ?? "", true);
|
|
955
1131
|
if (code !== 0) return code;
|
|
956
1132
|
if (await deps.confirm("Start opencode now? (if it's already open, quit and reopen it to load the extension)", {
|
|
957
1133
|
yes: opts.yes
|
|
@@ -961,6 +1137,8 @@ async function runOpencodeSetup(creds, routingUrl, opts, deps) {
|
|
|
961
1137
|
console.log("\nStart opencode when ready \u2014 then run /connect to log in and /models to pick a LunaRoute model.");
|
|
962
1138
|
return 0;
|
|
963
1139
|
}
|
|
1140
|
+
const creds = await requireCreds(opts, deps, stored, OPENCODE_LOGIN_HINT);
|
|
1141
|
+
if (!creds) return 1;
|
|
964
1142
|
const write = await deps.confirm(
|
|
965
1143
|
"Merge LunaRoute provider into opencode.json instead? (backup kept, other providers preserved)",
|
|
966
1144
|
{ yes: opts.yes }
|
|
@@ -1025,10 +1203,10 @@ async function launchOpencode(spawn3) {
|
|
|
1025
1203
|
});
|
|
1026
1204
|
});
|
|
1027
1205
|
}
|
|
1028
|
-
async function installPiExtension(
|
|
1206
|
+
async function installPiExtension(deps, opts) {
|
|
1029
1207
|
for (const pkg of PI_INSTALL_PACKAGES) {
|
|
1030
1208
|
const code = await new Promise((resolve) => {
|
|
1031
|
-
const child =
|
|
1209
|
+
const child = deps.spawn("pi", ["install", pkg]);
|
|
1032
1210
|
child.on("error", (err) => {
|
|
1033
1211
|
const e = err;
|
|
1034
1212
|
if (e?.code === "ENOENT") {
|
|
@@ -1051,22 +1229,42 @@ async function installPiExtension(spawn3) {
|
|
|
1051
1229
|
return code || 1;
|
|
1052
1230
|
}
|
|
1053
1231
|
}
|
|
1054
|
-
|
|
1232
|
+
if (await deps.confirm("Start pi now? (then run /login lunaroute to sign in)", { yes: opts.yes })) {
|
|
1233
|
+
return launchPi(deps.spawn);
|
|
1234
|
+
}
|
|
1235
|
+
console.log("\nStart pi when ready \u2014 then inside pi:");
|
|
1055
1236
|
console.log(" 1. /login lunaroute \u2014 browser login issues and stores an lr_ key.");
|
|
1056
1237
|
console.log(" 2. /model \u2014 pick a lunaroute/* model (models auto-sync from /v1/models).");
|
|
1057
1238
|
return 0;
|
|
1058
1239
|
}
|
|
1059
|
-
async function
|
|
1060
|
-
|
|
1240
|
+
async function launchPi(spawn3) {
|
|
1241
|
+
return new Promise((resolve) => {
|
|
1242
|
+
const child = spawn3("pi", []);
|
|
1243
|
+
child.on("error", (err) => {
|
|
1244
|
+
const e = err;
|
|
1245
|
+
if (e?.code === "ENOENT") {
|
|
1246
|
+
console.error(`Error: "pi" not found on PATH. Install pi first: https://pi.dev (exit 127)`);
|
|
1247
|
+
resolve(127);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1251
|
+
resolve(1);
|
|
1252
|
+
});
|
|
1253
|
+
child.on("exit", (code) => {
|
|
1254
|
+
resolve(typeof code === "number" ? code : 1);
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
async function applyPiModels(creds, routingUrl, write) {
|
|
1061
1259
|
let models;
|
|
1062
1260
|
try {
|
|
1063
|
-
models = await fetchModels(
|
|
1261
|
+
models = await fetchModels(routingUrl);
|
|
1064
1262
|
} catch (err) {
|
|
1065
1263
|
console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
|
|
1066
1264
|
return 1;
|
|
1067
1265
|
}
|
|
1068
1266
|
const plan = buildPlan2({
|
|
1069
|
-
routingUrl
|
|
1267
|
+
routingUrl,
|
|
1070
1268
|
orgId: creds.org_id,
|
|
1071
1269
|
models,
|
|
1072
1270
|
keyEnvVar: "LUNAROUTE_API_KEY"
|
|
@@ -1260,12 +1458,12 @@ async function readMemory(ctx, p) {
|
|
|
1260
1458
|
}
|
|
1261
1459
|
|
|
1262
1460
|
// src/commands/memory.ts
|
|
1263
|
-
var
|
|
1461
|
+
var NOT_LOGGED_IN2 = 'Not logged in. Run "lunaroute login" first.';
|
|
1264
1462
|
var NO_PROJECT = "No project context: run inside a git repo with an origin remote, or set LUNAROUTE_PROJECT_ID.";
|
|
1265
1463
|
function contextFor(profile) {
|
|
1266
1464
|
const s = resolveSettings(profile);
|
|
1267
1465
|
if (!s.routing_key) {
|
|
1268
|
-
console.error(
|
|
1466
|
+
console.error(NOT_LOGGED_IN2);
|
|
1269
1467
|
return 1;
|
|
1270
1468
|
}
|
|
1271
1469
|
const projectId = resolveProjectId();
|
|
@@ -1333,6 +1531,86 @@ async function runMemoryRead(profile, id, opts) {
|
|
|
1333
1531
|
return 0;
|
|
1334
1532
|
}
|
|
1335
1533
|
|
|
1534
|
+
// src/commands/searchKeys.ts
|
|
1535
|
+
var notLoggedIn = () => {
|
|
1536
|
+
console.error('Not logged in. Run "lunaroute login" first.');
|
|
1537
|
+
return 1;
|
|
1538
|
+
};
|
|
1539
|
+
var fail = (err) => {
|
|
1540
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1541
|
+
return 1;
|
|
1542
|
+
};
|
|
1543
|
+
async function runSearchKeysList(profile, opts) {
|
|
1544
|
+
const s = resolveSettings(profile);
|
|
1545
|
+
if (!s.routing_key) return notLoggedIn();
|
|
1546
|
+
const ctx = { apiUrl: s.api_url, apiKey: s.routing_key };
|
|
1547
|
+
try {
|
|
1548
|
+
if (opts.json) {
|
|
1549
|
+
console.log(await listSearchKeysRaw(ctx));
|
|
1550
|
+
return 0;
|
|
1551
|
+
}
|
|
1552
|
+
const keys = await listSearchKeys(ctx);
|
|
1553
|
+
if (keys.length === 0) {
|
|
1554
|
+
console.log("No search provider keys configured.");
|
|
1555
|
+
return 0;
|
|
1556
|
+
}
|
|
1557
|
+
console.log(
|
|
1558
|
+
renderTable(
|
|
1559
|
+
["PROVIDER", "CREATED", "UPDATED"],
|
|
1560
|
+
keys.map((k) => [k.provider, k.created_at, k.updated_at])
|
|
1561
|
+
)
|
|
1562
|
+
);
|
|
1563
|
+
return 0;
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
return fail(err);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function runSearchKeysSet(profile, opts) {
|
|
1569
|
+
const s = resolveSettings(profile);
|
|
1570
|
+
if (!s.routing_key) return notLoggedIn();
|
|
1571
|
+
const ctx = { apiUrl: s.api_url, apiKey: s.routing_key };
|
|
1572
|
+
try {
|
|
1573
|
+
let provider = opts.provider;
|
|
1574
|
+
let apiKey = opts.apiKey ?? process.env.LUNAROUTE_SEARCH_API_KEY;
|
|
1575
|
+
const providers = await listSearchProviders(ctx);
|
|
1576
|
+
const known = providers.map((p) => p.key);
|
|
1577
|
+
if (!provider) {
|
|
1578
|
+
provider = await promptInput(`Provider (${known.join(", ")})`);
|
|
1579
|
+
}
|
|
1580
|
+
if (!known.includes(provider)) {
|
|
1581
|
+
console.error(`unknown provider "${provider}" \u2014 this server offers: ${known.join(", ")}`);
|
|
1582
|
+
return 1;
|
|
1583
|
+
}
|
|
1584
|
+
if (!apiKey) {
|
|
1585
|
+
apiKey = await promptInput("API key", { hidden: true });
|
|
1586
|
+
}
|
|
1587
|
+
const meta = await putSearchKey(ctx, provider, apiKey);
|
|
1588
|
+
console.log(`Stored key for ${meta.provider} (updated ${meta.updated_at}).`);
|
|
1589
|
+
return 0;
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
return fail(err);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
async function runSearchKeysRemove(profile, provider, opts) {
|
|
1595
|
+
const s = resolveSettings(profile);
|
|
1596
|
+
if (!s.routing_key) return notLoggedIn();
|
|
1597
|
+
const ctx = { apiUrl: s.api_url, apiKey: s.routing_key };
|
|
1598
|
+
try {
|
|
1599
|
+
if (!opts.yes) {
|
|
1600
|
+
const ok = await confirm(`Remove the ${provider} search key?`);
|
|
1601
|
+
if (!ok) {
|
|
1602
|
+
console.log("Aborted.");
|
|
1603
|
+
return 0;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
await deleteSearchKey(ctx, provider);
|
|
1607
|
+
console.log(`Removed key for ${provider}.`);
|
|
1608
|
+
return 0;
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
return fail(err);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1336
1614
|
// src/mcp.ts
|
|
1337
1615
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1338
1616
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -1700,6 +1978,19 @@ skill.command("install").description("Install the LunaRoute memory skill + MCP s
|
|
|
1700
1978
|
const code = await runSkillInstall(program.opts().profile, opts);
|
|
1701
1979
|
if (code !== 0) process.exit(code);
|
|
1702
1980
|
});
|
|
1981
|
+
var searchKeys = program.command("search-keys").description("Manage web-search provider keys (BYOK) for your organization.");
|
|
1982
|
+
searchKeys.command("list").description("List configured search provider keys.").option("--json", "output the raw API envelope", false).action(async (opts) => {
|
|
1983
|
+
const code = await runSearchKeysList(program.opts().profile, { json: opts.json });
|
|
1984
|
+
if (code !== 0) process.exit(code);
|
|
1985
|
+
});
|
|
1986
|
+
searchKeys.command("set").description("Store or replace a search provider key (prompts for missing values).").option("--provider <key>", "provider key (e.g. kagi, brave, exa)").option("--api-key <key>", "API key (visible in shell history \u2014 prefer the prompt or LUNAROUTE_SEARCH_API_KEY)").action(async (opts) => {
|
|
1987
|
+
const code = await runSearchKeysSet(program.opts().profile, { provider: opts.provider, apiKey: opts.apiKey });
|
|
1988
|
+
if (code !== 0) process.exit(code);
|
|
1989
|
+
});
|
|
1990
|
+
searchKeys.command("remove <provider>").description("Remove a search provider key.").option("--yes", "skip the confirmation prompt", false).action(async (provider, opts) => {
|
|
1991
|
+
const code = await runSearchKeysRemove(program.opts().profile, provider, { yes: opts.yes });
|
|
1992
|
+
if (code !== 0) process.exit(code);
|
|
1993
|
+
});
|
|
1703
1994
|
program.parseAsync(process.argv).catch((err) => {
|
|
1704
1995
|
console.error(err instanceof Error ? err.message : err);
|
|
1705
1996
|
process.exit(1);
|