@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.
Files changed (3) hide show
  1. package/README.md +2 -3
  2. package/dist/index.js +329 -38
  3. 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 login # authorize this device in your browser
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: for `pi` it first offers to install the LunaRoute Pi extension + MCP adapter (recommended — models then auto-sync), and falls back to a `~/.pi/agent/models.json` merge if you decline. It finishes with an `export LUNAROUTE_API_KEY=…` line — run it in your shell, then start your agent and it routes through LunaRoute.
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.1";
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 fail(reason) {
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)) fail("invalid-url");
666
- if (/[?#]/.test(routingUrl)) fail("query-or-fragment");
667
- if (routingUrl !== routingUrl.trim()) fail("whitespace");
668
- if (routingUrl.endsWith("/")) fail("trailing-slash");
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
- fail("invalid-url");
808
+ fail2("invalid-url");
674
809
  }
675
- if (parsed.username !== "" || parsed.password !== "") fail("userinfo");
676
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail("invalid-url");
677
- if (parsed.hostname === "") fail("invalid-url");
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 creds = loadProfile(opts.profile);
767
- if (!creds || !creds.routing_key) {
768
- console.error('Not logged in. Run "lunaroute login" first.');
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(creds, routingUrl, opts, deps);
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
- async function runPiSetup(opts, deps) {
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.spawn);
1004
+ return installPiExtension(deps, opts);
838
1005
  }
839
1006
  if (opts.models) {
840
- return applyPiModels(opts, await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", { yes: opts.yes }));
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.spawn);
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
- opts,
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(creds, routingUrl, opts, deps) {
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: creds.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
1117
+ { routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
942
1118
  { extension: true }
943
1119
  );
944
- return applyAndReport(plan, creds.routing_key, false);
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: creds.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
1127
+ { routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
952
1128
  { extension: true }
953
1129
  );
954
- const code = await applyAndReport(plan, creds.routing_key, !opts.print);
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(spawn3) {
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 = spawn3("pi", ["install", pkg]);
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
- console.log("\nInstalled. Next steps inside pi:");
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 applyPiModels(opts, write) {
1060
- const creds = loadProfile(opts.profile);
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(opts.routingUrl || creds.routing_url || "");
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: opts.routingUrl || creds.routing_url,
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 NOT_LOGGED_IN = 'Not logged in. Run "lunaroute login" first.';
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(NOT_LOGGED_IN);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunaroute/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "LunaRoute CLI — configure coding harnesses and manage your LunaRoute account.",
5
5
  "repository": {
6
6
  "type": "git",