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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +68 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
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 privateGateway = (await promptText(
628
+ const answer = (await promptText(
619
629
  "PennyRouter gateway URL for this proxy (blank for hosted gateway): ",
620
630
  )).trim();
621
- if (privateGateway) {
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
- privateKey = (await promptText("Paste your private gateway key (pr-...): ")).trim();
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 probeClaudeCodeGateway({ gatewayBaseUrl, apiKey: claim.api_key });
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;