@lunaroute/cli 0.2.6 → 0.2.7
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 -3
- package/dist/index.js +83 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Requires Node.js 20+.
|
|
|
15
15
|
## Quickstart
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
lunaroute setup pi # or: opencode | claude-code | copilot-cli | generic
|
|
18
|
+
lunaroute setup pi # or: opencode | claude-code | codex | copilot-cli | generic
|
|
19
19
|
```
|
|
20
20
|
|
|
21
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.
|
|
@@ -26,7 +26,7 @@ Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accept
|
|
|
26
26
|
|
|
27
27
|
## What it enables
|
|
28
28
|
|
|
29
|
-
- **One-command setup** — `lunaroute setup` writes the right config for `pi`, `claude-code`, `opencode`, `copilot-cli`, or `generic` (any OpenAI-compatible base URL swap).
|
|
29
|
+
- **One-command setup** — `lunaroute setup` writes the right config for `pi`, `claude-code`, `codex`, `opencode`, `copilot-cli`, or `generic` (any OpenAI-compatible base URL swap).
|
|
30
30
|
- **Session launches** — `lunaroute run claude` or `lunaroute run codex` starts a session wired to LunaRoute; `--model` picks from the catalog, everything after `--` passes through to the harness.
|
|
31
31
|
- **The catalog, in your terminal** — `models`, `pricing`, and `usage` show what's available, what it costs (credits per million tokens), and your wallet balance plus recent ledger entries.
|
|
32
32
|
- **Project memory** — `memory search` and `memory read` query your past agent conversations for the current project. `lunaroute mcp` exposes the same tools as a stdio MCP server, and `skill install` wires the memory skill + MCP into Claude Code.
|
|
@@ -38,7 +38,7 @@ Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accept
|
|
|
38
38
|
| `lunaroute login` | Authorize this device via the browser and store a routing key |
|
|
39
39
|
| `lunaroute whoami` | Show the signed-in user and organization |
|
|
40
40
|
| `lunaroute logout` | Remove stored credentials |
|
|
41
|
-
| `lunaroute setup <harness>` | Configure `opencode` \| `pi` \| `openclaw` \| `claude-code` \| `copilot-cli` \| `generic`. pi setup is interactive (asks before writing); `--extension` / `--models` pick a flow, `--yes` accepts prompts, `--print` previews |
|
|
41
|
+
| `lunaroute setup <harness>` | Configure `opencode` \| `pi` \| `openclaw` \| `claude-code` \| `codex` \| `copilot-cli` \| `generic`. pi setup is interactive (asks before writing); `--extension` / `--models` pick a flow, `--yes` accepts prompts, `--print` previews |
|
|
42
42
|
| `lunaroute run <harness>` | Launch `claude` \| `claude-code` \| `codex` on LunaRoute for this session |
|
|
43
43
|
| `lunaroute models` | List available LunaRoute models |
|
|
44
44
|
| `lunaroute pricing` | Per-model pricing (credits per million tokens) |
|
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.7";
|
|
8
8
|
|
|
9
9
|
// src/login.ts
|
|
10
10
|
import { createServer } from "http";
|
|
@@ -598,6 +598,10 @@ function opencodeAuthPath() {
|
|
|
598
598
|
function openclawConfigPath() {
|
|
599
599
|
return join2(homedir2(), ".openclaw", "openclaw.json");
|
|
600
600
|
}
|
|
601
|
+
function codexProfilePath() {
|
|
602
|
+
const home = process.env.CODEX_HOME || join2(homedir2(), ".codex");
|
|
603
|
+
return join2(home, "lunaroute.config.toml");
|
|
604
|
+
}
|
|
601
605
|
function piModelsPath() {
|
|
602
606
|
return join2(homedir2(), ".pi", "agent", "models.json");
|
|
603
607
|
}
|
|
@@ -885,8 +889,57 @@ function buildPlan3(ctx) {
|
|
|
885
889
|
};
|
|
886
890
|
}
|
|
887
891
|
|
|
888
|
-
// src/setup/
|
|
892
|
+
// src/setup/routing-url.ts
|
|
893
|
+
function validatedRoutingUrl(routingUrl) {
|
|
894
|
+
function fail2(reason) {
|
|
895
|
+
throw new Error(
|
|
896
|
+
`routingUrl rejected (reason: ${reason}; <redacted URL>) \u2014 check --routing-url or the profile's routing URL`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
if (!/^https?:\/\/[^/?#]/.test(routingUrl)) fail2("invalid-url");
|
|
900
|
+
if (/[?#]/.test(routingUrl)) fail2("query-or-fragment");
|
|
901
|
+
if (routingUrl !== routingUrl.trim()) fail2("whitespace");
|
|
902
|
+
if (routingUrl.endsWith("/")) fail2("trailing-slash");
|
|
903
|
+
let parsed;
|
|
904
|
+
try {
|
|
905
|
+
parsed = new URL(routingUrl);
|
|
906
|
+
} catch {
|
|
907
|
+
fail2("invalid-url");
|
|
908
|
+
}
|
|
909
|
+
if (parsed.username !== "" || parsed.password !== "") fail2("userinfo");
|
|
910
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail2("invalid-url");
|
|
911
|
+
if (parsed.hostname === "") fail2("invalid-url");
|
|
912
|
+
return routingUrl;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/setup/adapters/codex.ts
|
|
889
916
|
function buildPlan4(ctx) {
|
|
917
|
+
const base = validatedRoutingUrl(ctx.routingUrl);
|
|
918
|
+
const model = ctx.models[0]?.id ?? "<model>";
|
|
919
|
+
const content = [
|
|
920
|
+
`model = "${model}"`,
|
|
921
|
+
'model_provider = "lunaroute"',
|
|
922
|
+
"",
|
|
923
|
+
"[model_providers.lunaroute]",
|
|
924
|
+
'name = "LunaRoute"',
|
|
925
|
+
`base_url = "${base}/v1"`,
|
|
926
|
+
'env_key = "LUNAROUTE_API_KEY"',
|
|
927
|
+
'wire_api = "responses"'
|
|
928
|
+
].join("\n") + "\n";
|
|
929
|
+
return {
|
|
930
|
+
fileWrites: [{ kind: "text", path: codexProfilePath(), content }],
|
|
931
|
+
exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
|
|
932
|
+
notes: [
|
|
933
|
+
"\nCodex: add the export above to your shell profile, then start Codex with `codex --profile lunaroute`.",
|
|
934
|
+
`Change model to any of: ${ctx.models.map((m) => m.id).join(", ")}, or pass \`--model <id>\` for one session.`,
|
|
935
|
+
"The key rides the Authorization: Bearer header via env_key; LunaRoute authenticates the lr_ key at the edge and never forwards it upstream.",
|
|
936
|
+
"One file: the model and the named provider live in the profile file \u2014 your ~/.codex/config.toml is untouched."
|
|
937
|
+
]
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/setup/adapters/copilotCli.ts
|
|
942
|
+
function buildPlan5(ctx) {
|
|
890
943
|
const firstModel = ctx.models[0]?.id ?? "<model>";
|
|
891
944
|
return {
|
|
892
945
|
fileWrites: [],
|
|
@@ -912,7 +965,7 @@ function buildPlan4(ctx) {
|
|
|
912
965
|
}
|
|
913
966
|
|
|
914
967
|
// src/setup/adapters/generic.ts
|
|
915
|
-
function
|
|
968
|
+
function buildPlan6(ctx) {
|
|
916
969
|
return {
|
|
917
970
|
fileWrites: [],
|
|
918
971
|
exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
|
|
@@ -927,34 +980,11 @@ function buildPlan5(ctx) {
|
|
|
927
980
|
};
|
|
928
981
|
}
|
|
929
982
|
|
|
930
|
-
// src/setup/routing-url.ts
|
|
931
|
-
function validatedRoutingUrl(routingUrl) {
|
|
932
|
-
function fail2(reason) {
|
|
933
|
-
throw new Error(
|
|
934
|
-
`routingUrl rejected (reason: ${reason}; <redacted URL>) \u2014 check --routing-url or the profile's routing URL`
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
if (!/^https?:\/\/[^/?#]/.test(routingUrl)) fail2("invalid-url");
|
|
938
|
-
if (/[?#]/.test(routingUrl)) fail2("query-or-fragment");
|
|
939
|
-
if (routingUrl !== routingUrl.trim()) fail2("whitespace");
|
|
940
|
-
if (routingUrl.endsWith("/")) fail2("trailing-slash");
|
|
941
|
-
let parsed;
|
|
942
|
-
try {
|
|
943
|
-
parsed = new URL(routingUrl);
|
|
944
|
-
} catch {
|
|
945
|
-
fail2("invalid-url");
|
|
946
|
-
}
|
|
947
|
-
if (parsed.username !== "" || parsed.password !== "") fail2("userinfo");
|
|
948
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") fail2("invalid-url");
|
|
949
|
-
if (parsed.hostname === "") fail2("invalid-url");
|
|
950
|
-
return routingUrl;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
983
|
// src/setup/adapters/openclaw.ts
|
|
954
984
|
function asRecord(v) {
|
|
955
985
|
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
956
986
|
}
|
|
957
|
-
function
|
|
987
|
+
function buildPlan7(ctx) {
|
|
958
988
|
const base = validatedRoutingUrl(ctx.routingUrl);
|
|
959
989
|
const models = ctx.models.map((m) => ({ id: m.id, name: m.id }));
|
|
960
990
|
return {
|
|
@@ -1014,10 +1044,11 @@ openclaw: restart the gateway, then \`openclaw models list\` and \`openclaw mode
|
|
|
1014
1044
|
var ADAPTERS = {
|
|
1015
1045
|
opencode: buildPlan,
|
|
1016
1046
|
pi: buildPlan2,
|
|
1017
|
-
openclaw:
|
|
1047
|
+
openclaw: buildPlan7,
|
|
1018
1048
|
"claude-code": buildPlan3,
|
|
1019
|
-
|
|
1020
|
-
|
|
1049
|
+
codex: buildPlan4,
|
|
1050
|
+
"copilot-cli": buildPlan5,
|
|
1051
|
+
generic: buildPlan6
|
|
1021
1052
|
};
|
|
1022
1053
|
var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension"];
|
|
1023
1054
|
var PI_INSTALL_HINT = "Install pi first: npm install -g --ignore-scripts @earendil-works/pi-coding-agent (https://pi.dev)";
|
|
@@ -2074,12 +2105,19 @@ function buildRunSpec(ctx) {
|
|
|
2074
2105
|
|
|
2075
2106
|
// src/run/adapters/codex.ts
|
|
2076
2107
|
function buildRunSpec2(ctx) {
|
|
2108
|
+
const provider = `{name="LunaRoute",base_url="${ctx.routingUrl}/v1",env_key="LUNAROUTE_API_KEY",wire_api="responses"}`;
|
|
2077
2109
|
return {
|
|
2078
2110
|
command: "codex",
|
|
2079
|
-
args: [
|
|
2111
|
+
args: [
|
|
2112
|
+
"-c",
|
|
2113
|
+
'model_provider="lunaroute"',
|
|
2114
|
+
"-c",
|
|
2115
|
+
`model_providers.lunaroute=${provider}`,
|
|
2116
|
+
"-c",
|
|
2117
|
+
`model="${ctx.model}"`
|
|
2118
|
+
],
|
|
2080
2119
|
env: {
|
|
2081
|
-
|
|
2082
|
-
OPENAI_API_KEY: ctx.apiKey
|
|
2120
|
+
LUNAROUTE_API_KEY: ctx.apiKey
|
|
2083
2121
|
}
|
|
2084
2122
|
};
|
|
2085
2123
|
}
|
|
@@ -2166,7 +2204,7 @@ program.command("whoami").description("Show the signed-in user and organization.
|
|
|
2166
2204
|
program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
|
|
2167
2205
|
logout(program.opts().profile);
|
|
2168
2206
|
});
|
|
2169
|
-
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) => {
|
|
2207
|
+
program.command("setup <harness>").description("Configure a coding harness (opencode | pi | hermes | claude-code | codex | 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) => {
|
|
2170
2208
|
const code = await runSetup(harness, {
|
|
2171
2209
|
profile: program.opts().profile,
|
|
2172
2210
|
print: opts.print,
|
|
@@ -2178,14 +2216,19 @@ program.command("setup <harness>").description("Configure a coding harness (open
|
|
|
2178
2216
|
});
|
|
2179
2217
|
if (code !== 0) process.exit(code);
|
|
2180
2218
|
});
|
|
2181
|
-
program.command("run <harness>").description("Launch a coding harness (claude | claude-code | codex) configured to use LunaRoute for this session.").option("--model <id>", "model id to launch on (default: first model in the catalog)").
|
|
2182
|
-
const
|
|
2183
|
-
const
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2219
|
+
program.command("run <harness>").description("Launch a coding harness (claude | claude-code | codex) configured to use LunaRoute for this session.").option("--model <id>", "model id to launch on (default: first model in the catalog)").argument("[extra...]").action(async (harness, extra, opts) => {
|
|
2220
|
+
const sep = process.argv.indexOf("--");
|
|
2221
|
+
const post = sep !== -1 ? process.argv.slice(sep + 1) : [];
|
|
2222
|
+
const before = post.length <= extra.length ? extra.slice(0, extra.length - post.length) : extra;
|
|
2223
|
+
if (before.length > 0) {
|
|
2224
|
+
console.error(
|
|
2225
|
+
`Unexpected argument "${before[0]}" \u2014 harness flags go after "--": lunaroute run ${harness} -- ${before.join(" ")}`
|
|
2226
|
+
);
|
|
2227
|
+
process.exit(1);
|
|
2228
|
+
}
|
|
2186
2229
|
const code = await runRun(
|
|
2187
2230
|
harness,
|
|
2188
|
-
{ profile: program.opts().profile, model: opts.model, passthrough }
|
|
2231
|
+
{ profile: program.opts().profile, model: opts.model, passthrough: post }
|
|
2189
2232
|
);
|
|
2190
2233
|
if (code !== 0) process.exit(code);
|
|
2191
2234
|
});
|