@lunaroute/cli 0.2.1 → 0.2.3
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 +382 -84
- package/package.json +3 -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.3";
|
|
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,9 +466,101 @@ 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
|
-
// src/
|
|
405
|
-
import { spawn } from "child_process";
|
|
539
|
+
// src/spawn.ts
|
|
540
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
541
|
+
import { spawn as crossSpawn } from "cross-spawn";
|
|
542
|
+
function makeRealSpawn(platform = process.platform) {
|
|
543
|
+
if (platform === "win32") {
|
|
544
|
+
return (command, args, env) => crossSpawn(command, args, { stdio: "inherit", env });
|
|
545
|
+
}
|
|
546
|
+
return (command, args, env) => nodeSpawn(command, args, { stdio: "inherit", env });
|
|
547
|
+
}
|
|
548
|
+
function describeSpawnError(command, installHint, err) {
|
|
549
|
+
const code = err?.code;
|
|
550
|
+
if (code === "ENOENT") {
|
|
551
|
+
return {
|
|
552
|
+
message: `Error: "${command}" not found on PATH. ${installHint} (exit 127)`,
|
|
553
|
+
exitCode: 127
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (code === "EINVAL") {
|
|
557
|
+
return {
|
|
558
|
+
message: `Error: "${command}" could not be launched \u2014 Node on Windows refuses to execute npm .cmd shims directly (EINVAL). The tool is likely installed; try running it manually. (exit 126)`,
|
|
559
|
+
exitCode: 126
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return { message: err instanceof Error ? err.message : String(err), exitCode: 1 };
|
|
563
|
+
}
|
|
406
564
|
|
|
407
565
|
// src/setup/paths.ts
|
|
408
566
|
import { execSync } from "child_process";
|
|
@@ -657,24 +815,24 @@ function buildPlan5(ctx) {
|
|
|
657
815
|
|
|
658
816
|
// src/setup/routing-url.ts
|
|
659
817
|
function validatedRoutingUrl(routingUrl) {
|
|
660
|
-
function
|
|
818
|
+
function fail2(reason) {
|
|
661
819
|
throw new Error(
|
|
662
820
|
`routingUrl rejected (reason: ${reason}; <redacted URL>) \u2014 check --routing-url or the profile's routing URL`
|
|
663
821
|
);
|
|
664
822
|
}
|
|
665
|
-
if (!/^https?:\/\/[^/?#]/.test(routingUrl))
|
|
666
|
-
if (/[?#]/.test(routingUrl))
|
|
667
|
-
if (routingUrl !== routingUrl.trim())
|
|
668
|
-
if (routingUrl.endsWith("/"))
|
|
823
|
+
if (!/^https?:\/\/[^/?#]/.test(routingUrl)) fail2("invalid-url");
|
|
824
|
+
if (/[?#]/.test(routingUrl)) fail2("query-or-fragment");
|
|
825
|
+
if (routingUrl !== routingUrl.trim()) fail2("whitespace");
|
|
826
|
+
if (routingUrl.endsWith("/")) fail2("trailing-slash");
|
|
669
827
|
let parsed;
|
|
670
828
|
try {
|
|
671
829
|
parsed = new URL(routingUrl);
|
|
672
830
|
} catch {
|
|
673
|
-
|
|
831
|
+
fail2("invalid-url");
|
|
674
832
|
}
|
|
675
|
-
if (parsed.username !== "" || parsed.password !== "")
|
|
676
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
677
|
-
if (parsed.hostname === "")
|
|
833
|
+
if (parsed.username !== "" || parsed.password !== "") fail2("userinfo");
|
|
834
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail2("invalid-url");
|
|
835
|
+
if (parsed.hostname === "") fail2("invalid-url");
|
|
678
836
|
return routingUrl;
|
|
679
837
|
}
|
|
680
838
|
|
|
@@ -750,7 +908,8 @@ var ADAPTERS = {
|
|
|
750
908
|
var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension", "npm:pi-mcp-adapter"];
|
|
751
909
|
async function runSetup(harness, opts, deps = {
|
|
752
910
|
confirm,
|
|
753
|
-
spawn: (
|
|
911
|
+
spawn: makeRealSpawn(),
|
|
912
|
+
runLogin
|
|
754
913
|
}) {
|
|
755
914
|
if (harness === "pi" && opts.extension && opts.models) {
|
|
756
915
|
console.error("Choose one of --extension or --models.");
|
|
@@ -763,12 +922,9 @@ async function runSetup(harness, opts, deps = {
|
|
|
763
922
|
);
|
|
764
923
|
return 1;
|
|
765
924
|
}
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return 1;
|
|
770
|
-
}
|
|
771
|
-
const routingUrlRaw = opts.routingUrl || creds.routing_url;
|
|
925
|
+
const stored = loadProfile(opts.profile);
|
|
926
|
+
const settings = resolveSettings(opts.profile);
|
|
927
|
+
const routingUrlRaw = opts.routingUrl || stored?.routing_url || settings.routing_url;
|
|
772
928
|
if (!routingUrlRaw) {
|
|
773
929
|
console.error("No routing URL in profile; pass --routing-url.");
|
|
774
930
|
return 1;
|
|
@@ -781,11 +937,13 @@ async function runSetup(harness, opts, deps = {
|
|
|
781
937
|
return 1;
|
|
782
938
|
}
|
|
783
939
|
if (harness === "pi" && !opts.print) {
|
|
784
|
-
return runPiSetup(opts, deps);
|
|
940
|
+
return runPiSetup(opts, stored, routingUrl, deps);
|
|
785
941
|
}
|
|
786
942
|
if (harness === "opencode") {
|
|
787
|
-
return runOpencodeSetup(
|
|
943
|
+
return runOpencodeSetup(stored, settings, routingUrl, opts, deps);
|
|
788
944
|
}
|
|
945
|
+
const creds = await requireCreds(opts, deps, stored);
|
|
946
|
+
if (!creds) return 1;
|
|
789
947
|
if (harness === "hermes") {
|
|
790
948
|
return runHermesSetup(creds, routingUrl, opts, deps);
|
|
791
949
|
}
|
|
@@ -822,7 +980,39 @@ Wrote: ${summary.written.join(", ")}`);
|
|
|
822
980
|
return 1;
|
|
823
981
|
}
|
|
824
982
|
}
|
|
825
|
-
|
|
983
|
+
var NOT_LOGGED_IN = 'Not logged in. Run "lunaroute login" first.';
|
|
984
|
+
var OPENCODE_LOGIN_HINT = "Or re-run and choose the extension \u2014 it logs in via /connect inside opencode.";
|
|
985
|
+
var PI_LOGIN_HINT = "Or re-run and choose the extension \u2014 login happens via /login lunaroute inside pi.";
|
|
986
|
+
async function requireCreds(opts, deps, stored, hint) {
|
|
987
|
+
if (stored?.routing_key) return stored;
|
|
988
|
+
const decline = [NOT_LOGGED_IN, hint].filter(Boolean).join(" ");
|
|
989
|
+
try {
|
|
990
|
+
const ok = await deps.confirm('Not logged in. Run "lunaroute login" now? (opens your browser)');
|
|
991
|
+
if (!ok) {
|
|
992
|
+
console.error(decline);
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
} catch (err) {
|
|
996
|
+
if (err instanceof NonInteractiveTerminalError) {
|
|
997
|
+
console.error(decline);
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
throw err;
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
await deps.runLogin(opts.profile);
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
console.error(`Login failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
const fresh = loadProfile(opts.profile);
|
|
1009
|
+
if (!fresh?.routing_key) {
|
|
1010
|
+
console.error(NOT_LOGGED_IN);
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
return fresh;
|
|
1014
|
+
}
|
|
1015
|
+
async function runPiSetup(opts, stored, routingUrl, deps) {
|
|
826
1016
|
try {
|
|
827
1017
|
if (opts.extension) {
|
|
828
1018
|
const ok = await deps.confirm(
|
|
@@ -834,19 +1024,28 @@ async function runPiSetup(opts, deps) {
|
|
|
834
1024
|
console.log(` pi install ${PI_INSTALL_PACKAGES.join(" && pi install ")}`);
|
|
835
1025
|
return 0;
|
|
836
1026
|
}
|
|
837
|
-
return installPiExtension(deps
|
|
1027
|
+
return installPiExtension(deps, opts);
|
|
838
1028
|
}
|
|
839
1029
|
if (opts.models) {
|
|
840
|
-
|
|
1030
|
+
const creds2 = await requireCreds(opts, deps, stored, PI_LOGIN_HINT);
|
|
1031
|
+
if (!creds2) return 1;
|
|
1032
|
+
return applyPiModels(
|
|
1033
|
+
creds2,
|
|
1034
|
+
routingUrl,
|
|
1035
|
+
await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", { yes: opts.yes })
|
|
1036
|
+
);
|
|
841
1037
|
}
|
|
842
1038
|
if (await deps.confirm(
|
|
843
1039
|
"Install the LunaRoute Pi extension + MCP adapter via 'pi install' (recommended \u2014 auto-registers models)?",
|
|
844
1040
|
{ yes: opts.yes }
|
|
845
1041
|
)) {
|
|
846
|
-
return installPiExtension(deps
|
|
1042
|
+
return installPiExtension(deps, opts);
|
|
847
1043
|
}
|
|
1044
|
+
const creds = await requireCreds(opts, deps, stored, PI_LOGIN_HINT);
|
|
1045
|
+
if (!creds) return 1;
|
|
848
1046
|
return applyPiModels(
|
|
849
|
-
|
|
1047
|
+
creds,
|
|
1048
|
+
routingUrl,
|
|
850
1049
|
await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", {
|
|
851
1050
|
yes: opts.yes
|
|
852
1051
|
})
|
|
@@ -899,23 +1098,20 @@ function hermesConfigSetArgs(routingUrl, key) {
|
|
|
899
1098
|
["config", "set", "LUNAROUTE_API_KEY", key]
|
|
900
1099
|
];
|
|
901
1100
|
}
|
|
902
|
-
async function runHermesConfigSets(
|
|
1101
|
+
async function runHermesConfigSets(spawn, routingUrl, key) {
|
|
903
1102
|
const commands = hermesConfigSetArgs(routingUrl, key);
|
|
904
1103
|
const setKeys = commands.map((c) => c[2]);
|
|
905
1104
|
for (let i = 0; i < commands.length; i++) {
|
|
906
1105
|
const code = await new Promise((resolve) => {
|
|
907
|
-
const child =
|
|
1106
|
+
const child = spawn("hermes", commands[i]);
|
|
908
1107
|
child.on("error", (err) => {
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
917
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
918
|
-
resolve(1);
|
|
1108
|
+
const { message, exitCode } = describeSpawnError(
|
|
1109
|
+
"hermes",
|
|
1110
|
+
"Install Hermes Agent first: https://hermes-agent.nousresearch.com",
|
|
1111
|
+
err
|
|
1112
|
+
);
|
|
1113
|
+
console.error(message);
|
|
1114
|
+
resolve(exitCode);
|
|
919
1115
|
});
|
|
920
1116
|
child.on("exit", (code2) => {
|
|
921
1117
|
resolve(typeof code2 === "number" ? code2 : 1);
|
|
@@ -934,24 +1130,24 @@ async function runHermesConfigSets(spawn3, routingUrl, key) {
|
|
|
934
1130
|
console.log(" hermes model # interactive provider/model picker");
|
|
935
1131
|
return 0;
|
|
936
1132
|
}
|
|
937
|
-
async function runOpencodeSetup(
|
|
1133
|
+
async function runOpencodeSetup(stored, settings, routingUrl, opts, deps) {
|
|
938
1134
|
try {
|
|
939
1135
|
if (opts.print) {
|
|
940
1136
|
const plan = buildPlan(
|
|
941
|
-
{ routingUrl, orgId:
|
|
1137
|
+
{ routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
|
|
942
1138
|
{ extension: true }
|
|
943
1139
|
);
|
|
944
|
-
return applyAndReport(plan,
|
|
1140
|
+
return applyAndReport(plan, stored?.routing_key ?? "", false);
|
|
945
1141
|
}
|
|
946
1142
|
if (await deps.confirm(
|
|
947
1143
|
"Install the LunaRoute OpenCode extension (recommended \u2014 /connect login, models auto-sync)?",
|
|
948
1144
|
{ yes: opts.yes }
|
|
949
1145
|
)) {
|
|
950
1146
|
const plan = buildPlan(
|
|
951
|
-
{ routingUrl, orgId:
|
|
1147
|
+
{ routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
|
|
952
1148
|
{ extension: true }
|
|
953
1149
|
);
|
|
954
|
-
const code = await applyAndReport(plan,
|
|
1150
|
+
const code = await applyAndReport(plan, stored?.routing_key ?? "", true);
|
|
955
1151
|
if (code !== 0) return code;
|
|
956
1152
|
if (await deps.confirm("Start opencode now? (if it's already open, quit and reopen it to load the extension)", {
|
|
957
1153
|
yes: opts.yes
|
|
@@ -961,6 +1157,8 @@ async function runOpencodeSetup(creds, routingUrl, opts, deps) {
|
|
|
961
1157
|
console.log("\nStart opencode when ready \u2014 then run /connect to log in and /models to pick a LunaRoute model.");
|
|
962
1158
|
return 0;
|
|
963
1159
|
}
|
|
1160
|
+
const creds = await requireCreds(opts, deps, stored, OPENCODE_LOGIN_HINT);
|
|
1161
|
+
if (!creds) return 1;
|
|
964
1162
|
const write = await deps.confirm(
|
|
965
1163
|
"Merge LunaRoute provider into opencode.json instead? (backup kept, other providers preserved)",
|
|
966
1164
|
{ yes: opts.yes }
|
|
@@ -1007,37 +1205,35 @@ Wrote: ${summary.written.join(", ")}`);
|
|
|
1007
1205
|
return 1;
|
|
1008
1206
|
}
|
|
1009
1207
|
}
|
|
1010
|
-
async function launchOpencode(
|
|
1208
|
+
async function launchOpencode(spawn) {
|
|
1011
1209
|
return new Promise((resolve) => {
|
|
1012
|
-
const child =
|
|
1210
|
+
const child = spawn("opencode", []);
|
|
1013
1211
|
child.on("error", (err) => {
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
resolve(1);
|
|
1212
|
+
const { message, exitCode } = describeSpawnError(
|
|
1213
|
+
"opencode",
|
|
1214
|
+
"Install OpenCode first: https://opencode.ai",
|
|
1215
|
+
err
|
|
1216
|
+
);
|
|
1217
|
+
console.error(message);
|
|
1218
|
+
resolve(exitCode);
|
|
1022
1219
|
});
|
|
1023
1220
|
child.on("exit", (code) => {
|
|
1024
1221
|
resolve(typeof code === "number" ? code : 1);
|
|
1025
1222
|
});
|
|
1026
1223
|
});
|
|
1027
1224
|
}
|
|
1028
|
-
async function installPiExtension(
|
|
1225
|
+
async function installPiExtension(deps, opts) {
|
|
1029
1226
|
for (const pkg of PI_INSTALL_PACKAGES) {
|
|
1030
1227
|
const code = await new Promise((resolve) => {
|
|
1031
|
-
const child =
|
|
1228
|
+
const child = deps.spawn("pi", ["install", pkg]);
|
|
1032
1229
|
child.on("error", (err) => {
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
resolve(1);
|
|
1230
|
+
const { message, exitCode } = describeSpawnError(
|
|
1231
|
+
"pi",
|
|
1232
|
+
"Install pi first: https://pi.dev",
|
|
1233
|
+
err
|
|
1234
|
+
);
|
|
1235
|
+
console.error(message);
|
|
1236
|
+
resolve(exitCode);
|
|
1041
1237
|
});
|
|
1042
1238
|
child.on("exit", (code2) => {
|
|
1043
1239
|
resolve(typeof code2 === "number" ? code2 : 1);
|
|
@@ -1051,22 +1247,41 @@ async function installPiExtension(spawn3) {
|
|
|
1051
1247
|
return code || 1;
|
|
1052
1248
|
}
|
|
1053
1249
|
}
|
|
1054
|
-
|
|
1250
|
+
if (await deps.confirm("Start pi now? (then run /login lunaroute to sign in)", { yes: opts.yes })) {
|
|
1251
|
+
return launchPi(deps.spawn);
|
|
1252
|
+
}
|
|
1253
|
+
console.log("\nStart pi when ready \u2014 then inside pi:");
|
|
1055
1254
|
console.log(" 1. /login lunaroute \u2014 browser login issues and stores an lr_ key.");
|
|
1056
1255
|
console.log(" 2. /model \u2014 pick a lunaroute/* model (models auto-sync from /v1/models).");
|
|
1057
1256
|
return 0;
|
|
1058
1257
|
}
|
|
1059
|
-
async function
|
|
1060
|
-
|
|
1258
|
+
async function launchPi(spawn) {
|
|
1259
|
+
return new Promise((resolve) => {
|
|
1260
|
+
const child = spawn("pi", []);
|
|
1261
|
+
child.on("error", (err) => {
|
|
1262
|
+
const { message, exitCode } = describeSpawnError(
|
|
1263
|
+
"pi",
|
|
1264
|
+
"Install pi first: https://pi.dev",
|
|
1265
|
+
err
|
|
1266
|
+
);
|
|
1267
|
+
console.error(message);
|
|
1268
|
+
resolve(exitCode);
|
|
1269
|
+
});
|
|
1270
|
+
child.on("exit", (code) => {
|
|
1271
|
+
resolve(typeof code === "number" ? code : 1);
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
async function applyPiModels(creds, routingUrl, write) {
|
|
1061
1276
|
let models;
|
|
1062
1277
|
try {
|
|
1063
|
-
models = await fetchModels(
|
|
1278
|
+
models = await fetchModels(routingUrl);
|
|
1064
1279
|
} catch (err) {
|
|
1065
1280
|
console.error(`Could not fetch the model catalog: ${err instanceof Error ? err.message : err}`);
|
|
1066
1281
|
return 1;
|
|
1067
1282
|
}
|
|
1068
1283
|
const plan = buildPlan2({
|
|
1069
|
-
routingUrl
|
|
1284
|
+
routingUrl,
|
|
1070
1285
|
orgId: creds.org_id,
|
|
1071
1286
|
models,
|
|
1072
1287
|
keyEnvVar: "LUNAROUTE_API_KEY"
|
|
@@ -1260,12 +1475,12 @@ async function readMemory(ctx, p) {
|
|
|
1260
1475
|
}
|
|
1261
1476
|
|
|
1262
1477
|
// src/commands/memory.ts
|
|
1263
|
-
var
|
|
1478
|
+
var NOT_LOGGED_IN2 = 'Not logged in. Run "lunaroute login" first.';
|
|
1264
1479
|
var NO_PROJECT = "No project context: run inside a git repo with an origin remote, or set LUNAROUTE_PROJECT_ID.";
|
|
1265
1480
|
function contextFor(profile) {
|
|
1266
1481
|
const s = resolveSettings(profile);
|
|
1267
1482
|
if (!s.routing_key) {
|
|
1268
|
-
console.error(
|
|
1483
|
+
console.error(NOT_LOGGED_IN2);
|
|
1269
1484
|
return 1;
|
|
1270
1485
|
}
|
|
1271
1486
|
const projectId = resolveProjectId();
|
|
@@ -1333,6 +1548,86 @@ async function runMemoryRead(profile, id, opts) {
|
|
|
1333
1548
|
return 0;
|
|
1334
1549
|
}
|
|
1335
1550
|
|
|
1551
|
+
// src/commands/searchKeys.ts
|
|
1552
|
+
var notLoggedIn = () => {
|
|
1553
|
+
console.error('Not logged in. Run "lunaroute login" first.');
|
|
1554
|
+
return 1;
|
|
1555
|
+
};
|
|
1556
|
+
var fail = (err) => {
|
|
1557
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1558
|
+
return 1;
|
|
1559
|
+
};
|
|
1560
|
+
async function runSearchKeysList(profile, opts) {
|
|
1561
|
+
const s = resolveSettings(profile);
|
|
1562
|
+
if (!s.routing_key) return notLoggedIn();
|
|
1563
|
+
const ctx = { apiUrl: s.api_url, apiKey: s.routing_key };
|
|
1564
|
+
try {
|
|
1565
|
+
if (opts.json) {
|
|
1566
|
+
console.log(await listSearchKeysRaw(ctx));
|
|
1567
|
+
return 0;
|
|
1568
|
+
}
|
|
1569
|
+
const keys = await listSearchKeys(ctx);
|
|
1570
|
+
if (keys.length === 0) {
|
|
1571
|
+
console.log("No search provider keys configured.");
|
|
1572
|
+
return 0;
|
|
1573
|
+
}
|
|
1574
|
+
console.log(
|
|
1575
|
+
renderTable(
|
|
1576
|
+
["PROVIDER", "CREATED", "UPDATED"],
|
|
1577
|
+
keys.map((k) => [k.provider, k.created_at, k.updated_at])
|
|
1578
|
+
)
|
|
1579
|
+
);
|
|
1580
|
+
return 0;
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
return fail(err);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async function runSearchKeysSet(profile, opts) {
|
|
1586
|
+
const s = resolveSettings(profile);
|
|
1587
|
+
if (!s.routing_key) return notLoggedIn();
|
|
1588
|
+
const ctx = { apiUrl: s.api_url, apiKey: s.routing_key };
|
|
1589
|
+
try {
|
|
1590
|
+
let provider = opts.provider;
|
|
1591
|
+
let apiKey = opts.apiKey ?? process.env.LUNAROUTE_SEARCH_API_KEY;
|
|
1592
|
+
const providers = await listSearchProviders(ctx);
|
|
1593
|
+
const known = providers.map((p) => p.key);
|
|
1594
|
+
if (!provider) {
|
|
1595
|
+
provider = await promptInput(`Provider (${known.join(", ")})`);
|
|
1596
|
+
}
|
|
1597
|
+
if (!known.includes(provider)) {
|
|
1598
|
+
console.error(`unknown provider "${provider}" \u2014 this server offers: ${known.join(", ")}`);
|
|
1599
|
+
return 1;
|
|
1600
|
+
}
|
|
1601
|
+
if (!apiKey) {
|
|
1602
|
+
apiKey = await promptInput("API key", { hidden: true });
|
|
1603
|
+
}
|
|
1604
|
+
const meta = await putSearchKey(ctx, provider, apiKey);
|
|
1605
|
+
console.log(`Stored key for ${meta.provider} (updated ${meta.updated_at}).`);
|
|
1606
|
+
return 0;
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
return fail(err);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
async function runSearchKeysRemove(profile, provider, opts) {
|
|
1612
|
+
const s = resolveSettings(profile);
|
|
1613
|
+
if (!s.routing_key) return notLoggedIn();
|
|
1614
|
+
const ctx = { apiUrl: s.api_url, apiKey: s.routing_key };
|
|
1615
|
+
try {
|
|
1616
|
+
if (!opts.yes) {
|
|
1617
|
+
const ok = await confirm(`Remove the ${provider} search key?`);
|
|
1618
|
+
if (!ok) {
|
|
1619
|
+
console.log("Aborted.");
|
|
1620
|
+
return 0;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
await deleteSearchKey(ctx, provider);
|
|
1624
|
+
console.log(`Removed key for ${provider}.`);
|
|
1625
|
+
return 0;
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
return fail(err);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1336
1631
|
// src/mcp.ts
|
|
1337
1632
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1338
1633
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -1513,9 +1808,6 @@ async function runSkillInstall(profile, opts) {
|
|
|
1513
1808
|
return 0;
|
|
1514
1809
|
}
|
|
1515
1810
|
|
|
1516
|
-
// src/commands/run.ts
|
|
1517
|
-
import { spawn as spawn2 } from "child_process";
|
|
1518
|
-
|
|
1519
1811
|
// src/run/adapters/claudeCode.ts
|
|
1520
1812
|
function buildRunSpec(ctx) {
|
|
1521
1813
|
return {
|
|
@@ -1562,7 +1854,7 @@ var INSTALL_HINT = {
|
|
|
1562
1854
|
codex: "Install Codex: https://github.com/openai/codex#install"
|
|
1563
1855
|
};
|
|
1564
1856
|
var realDeps = {
|
|
1565
|
-
spawn: (
|
|
1857
|
+
spawn: makeRealSpawn(),
|
|
1566
1858
|
fetchModels
|
|
1567
1859
|
};
|
|
1568
1860
|
async function runRun(harness, opts, deps = realDeps) {
|
|
@@ -1602,16 +1894,9 @@ async function runRun(harness, opts, deps = realDeps) {
|
|
|
1602
1894
|
const child = deps.spawn(spec.command, args, env);
|
|
1603
1895
|
return new Promise((resolve) => {
|
|
1604
1896
|
child.on("error", (err) => {
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
`Error: "${spec.command}" not found on PATH. ${INSTALL_HINT[harness]} (exit 127)`
|
|
1609
|
-
);
|
|
1610
|
-
resolve(127);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
1614
|
-
resolve(1);
|
|
1897
|
+
const { message, exitCode } = describeSpawnError(spec.command, INSTALL_HINT[harness], err);
|
|
1898
|
+
console.error(message);
|
|
1899
|
+
resolve(exitCode);
|
|
1615
1900
|
});
|
|
1616
1901
|
child.on("exit", (code) => {
|
|
1617
1902
|
resolve(typeof code === "number" ? code : 1);
|
|
@@ -1700,6 +1985,19 @@ skill.command("install").description("Install the LunaRoute memory skill + MCP s
|
|
|
1700
1985
|
const code = await runSkillInstall(program.opts().profile, opts);
|
|
1701
1986
|
if (code !== 0) process.exit(code);
|
|
1702
1987
|
});
|
|
1988
|
+
var searchKeys = program.command("search-keys").description("Manage web-search provider keys (BYOK) for your organization.");
|
|
1989
|
+
searchKeys.command("list").description("List configured search provider keys.").option("--json", "output the raw API envelope", false).action(async (opts) => {
|
|
1990
|
+
const code = await runSearchKeysList(program.opts().profile, { json: opts.json });
|
|
1991
|
+
if (code !== 0) process.exit(code);
|
|
1992
|
+
});
|
|
1993
|
+
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) => {
|
|
1994
|
+
const code = await runSearchKeysSet(program.opts().profile, { provider: opts.provider, apiKey: opts.apiKey });
|
|
1995
|
+
if (code !== 0) process.exit(code);
|
|
1996
|
+
});
|
|
1997
|
+
searchKeys.command("remove <provider>").description("Remove a search provider key.").option("--yes", "skip the confirmation prompt", false).action(async (provider, opts) => {
|
|
1998
|
+
const code = await runSearchKeysRemove(program.opts().profile, provider, { yes: opts.yes });
|
|
1999
|
+
if (code !== 0) process.exit(code);
|
|
2000
|
+
});
|
|
1703
2001
|
program.parseAsync(process.argv).catch((err) => {
|
|
1704
2002
|
console.error(err instanceof Error ? err.message : err);
|
|
1705
2003
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunaroute/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "LunaRoute CLI — configure coding harnesses and manage your LunaRoute account.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
32
|
"commander": "^15.0.0",
|
|
33
|
+
"cross-spawn": "^7.0.6",
|
|
33
34
|
"open": "^11.0.1",
|
|
34
35
|
"zod": "^4.4.3"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
38
|
+
"@types/cross-spawn": "^6.0.6",
|
|
37
39
|
"@types/node": "^26.2.0",
|
|
38
40
|
"tsup": "^8.3.0",
|
|
39
41
|
"tsx": "^4.23.12",
|