pennyrouter 0.3.1 → 0.3.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/package.json +1 -1
- package/src/cli.js +68 -6
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -607,6 +607,16 @@ export async function install(flags = {}) {
|
|
|
607
607
|
funding = await configureUserFunding(
|
|
608
608
|
flags, needsAuth, claudeAnthropicAuth, claudeProviders, codexProviders);
|
|
609
609
|
gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
|
|
610
|
+
if (flags.gatewayBaseUrl) {
|
|
611
|
+
const normalized = normalizeGatewayBaseUrl(flags.gatewayBaseUrl);
|
|
612
|
+
if (!normalized) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
`--gateway-base-url "${flags.gatewayBaseUrl}" is not a usable URL. Use a host like `
|
|
615
|
+
+ "localhost:8400 or https://gateway.example.com.",
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
gatewayBaseUrl = normalized;
|
|
619
|
+
}
|
|
610
620
|
|
|
611
621
|
// A user-supplied proxy may live on a private network that the hosted gateway cannot
|
|
612
622
|
// reach. Let interactive installs choose the gateway after they choose the proxy: blank
|
|
@@ -615,10 +625,20 @@ export async function install(flags = {}) {
|
|
|
615
625
|
// explicit localhost:8400 developer shortcut and skips this prompt.
|
|
616
626
|
if ((claudeProviders.has("anthropic-proxy") || codexProviders.has("openai-proxy")) && !flags.gatewayBaseUrl
|
|
617
627
|
&& !flags.yes && input.isTTY && output.isTTY) {
|
|
618
|
-
const
|
|
628
|
+
const answer = (await promptText(
|
|
619
629
|
"PennyRouter gateway URL for this proxy (blank for hosted gateway): ",
|
|
620
630
|
)).trim();
|
|
621
|
-
if (
|
|
631
|
+
if (answer) {
|
|
632
|
+
const privateGateway = normalizeGatewayBaseUrl(answer);
|
|
633
|
+
if (!privateGateway) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`"${answer}" is not a usable gateway URL. Use a host like `
|
|
636
|
+
+ "localhost:8400 or https://gateway.example.com.",
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
// Show the normalized form: the user typed "localhost:8000" and everything from here on
|
|
640
|
+
// uses "http://localhost:8000", so any later error names a URL they recognize.
|
|
641
|
+
if (privateGateway !== answer) console.log(`Using gateway ${privateGateway}.`);
|
|
622
642
|
gatewayBaseUrl = privateGateway;
|
|
623
643
|
flags.privateGateway = true;
|
|
624
644
|
}
|
|
@@ -633,7 +653,8 @@ export async function install(flags = {}) {
|
|
|
633
653
|
console.log("");
|
|
634
654
|
console.log(`Private gateway: ${gatewayBaseUrl}`);
|
|
635
655
|
console.log("Your PennyRouter administrator issues keys for this gateway.");
|
|
636
|
-
|
|
656
|
+
// No echo: this is an issued credential, and a visible paste lingers in scrollback.
|
|
657
|
+
privateKey = (await promptSecretish("Paste your private gateway key (pr-...): ")).trim();
|
|
637
658
|
}
|
|
638
659
|
if (!privateKey) {
|
|
639
660
|
throw new Error(
|
|
@@ -650,19 +671,20 @@ export async function install(flags = {}) {
|
|
|
650
671
|
}
|
|
651
672
|
|
|
652
673
|
for (const credential of funding.credentials) {
|
|
653
|
-
await storeUpstreamCredential({
|
|
674
|
+
await withGatewayContext(gatewayBaseUrl, () => storeUpstreamCredential({
|
|
654
675
|
gatewayBaseUrl,
|
|
655
676
|
apiKey: claim.api_key,
|
|
656
677
|
provider: credential.provider,
|
|
657
678
|
token: credential.token,
|
|
658
679
|
metadata: credential.metadata,
|
|
659
|
-
});
|
|
680
|
+
}));
|
|
660
681
|
}
|
|
661
682
|
}
|
|
662
683
|
|
|
663
684
|
if (needsAuth.some((harness) => harness.id === "claude-code")) {
|
|
664
685
|
console.log("Verifying modern Claude Code compatibility before changing local settings...");
|
|
665
|
-
await
|
|
686
|
+
await withGatewayContext(
|
|
687
|
+
gatewayBaseUrl, () => probeClaudeCodeGateway({ gatewayBaseUrl, apiKey: claim.api_key }));
|
|
666
688
|
console.log("");
|
|
667
689
|
}
|
|
668
690
|
|
|
@@ -844,6 +866,46 @@ function mcpTargets(flags) {
|
|
|
844
866
|
/** The gateway URL worth pinning into the MCP entry, or "" for the default.
|
|
845
867
|
The MCP server already falls back to production on its own, so writing that value would only
|
|
846
868
|
freeze today's default into every user's config. */
|
|
869
|
+
/* Users type gateways the way they say them out loud — "localhost:8000",
|
|
870
|
+
"gateway.example.com". Those are not absolute URLs, so fetch() rejects them with a bare
|
|
871
|
+
"fetch failed" long after the prompt, where the cause is invisible. Supply the obvious scheme
|
|
872
|
+
(plain http for loopback, https elsewhere) and prove the result parses, so a typo fails here
|
|
873
|
+
instead. Returns null when the value cannot be salvaged. */
|
|
874
|
+
function normalizeGatewayBaseUrl(raw) {
|
|
875
|
+
const value = String(raw || "").trim().replace(/\/+$/, "");
|
|
876
|
+
if (!value) return null;
|
|
877
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value)
|
|
878
|
+
? value
|
|
879
|
+
: `${/^(localhost|127\.0\.0\.1|\[::1\])(:|$)/i.test(value) ? "http" : "https"}://${value}`;
|
|
880
|
+
let parsed;
|
|
881
|
+
try {
|
|
882
|
+
parsed = new URL(withScheme);
|
|
883
|
+
} catch {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
887
|
+
if (!parsed.hostname) return null;
|
|
888
|
+
return withScheme.replace(/\/+$/, "");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/* A gateway that is unreachable (wrong host, wrong port, not running) surfaces from fetch() as
|
|
892
|
+
an opaque "fetch failed" with no status. Name the gateway we could not reach so the user knows
|
|
893
|
+
which URL to fix. Real HTTP errors carry their own message and pass through untouched. */
|
|
894
|
+
async function withGatewayContext(gatewayBaseUrl, action) {
|
|
895
|
+
try {
|
|
896
|
+
return await action();
|
|
897
|
+
} catch (error) {
|
|
898
|
+
const message = String(error?.message || error);
|
|
899
|
+
if (/fetch failed|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ECONNRESET|network/i.test(message)) {
|
|
900
|
+
throw new Error(
|
|
901
|
+
`Could not reach the PennyRouter gateway at ${gatewayBaseUrl} (${message}). `
|
|
902
|
+
+ "Check that the URL is correct and that the gateway is running.",
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
847
909
|
function customGatewayBaseUrl(url) {
|
|
848
910
|
const value = String(url || "").trim().replace(/\/+$/, "");
|
|
849
911
|
return !value || value === DEFAULT_GATEWAY_BASE_URL ? "" : value;
|