pennyrouter 0.3.10 → 0.3.11

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 +98 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
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
@@ -642,7 +642,16 @@ export async function install(flags = {}) {
642
642
  // key against a store this install never talks to. Ask for the gateway, mint there, and
643
643
  // skip the claim entirely.
644
644
  gatewayBaseUrl = await resolvePrivateGatewayUrl(flags);
645
- claim = { api_key: await obtainPrivateGatewayKey(flags, gatewayBaseUrl), gateway_base_url: gatewayBaseUrl };
645
+ const launchConfig = await loadPennyConfig();
646
+ const globalAuth = await readClaudeCodeGatewayAuth();
647
+ const storedGateway = launchConfig.gateway_base_url || globalAuth.gatewayBaseUrl;
648
+ const storedKey = storedGateway && sameGatewayBaseUrl(gatewayBaseUrl, storedGateway)
649
+ ? (launchConfig.api_key || globalAuth.apiKey)
650
+ : null;
651
+ claim = {
652
+ api_key: await obtainPrivateGatewayKey(flags, gatewayBaseUrl, storedKey),
653
+ gateway_base_url: gatewayBaseUrl,
654
+ };
646
655
 
647
656
  const claudeProviders = await selectClaudeFundingProviders(flags, needsAuth);
648
657
  const codexProviders = await selectCodexFundingProviders(flags, needsAuth);
@@ -661,48 +670,86 @@ export async function install(flags = {}) {
661
670
  }));
662
671
  }
663
672
  } else {
664
- const session = await createCliSession({
665
- appUrl: flags.appUrl || APP_URL,
666
- harnesses: selected.map((harness) => harness.id),
667
- accountMode: "existing",
668
- });
673
+ // Re-running the ordinary installer should refresh local harness configuration, not mint
674
+ // another machine key. Reuse a key already saved for this same gateway; an explicit gateway
675
+ // URL still wins, and a URL change deliberately falls through to fresh authorization.
676
+ const launchConfig = await loadPennyConfig();
677
+ const globalAuth = await readClaudeCodeGatewayAuth();
678
+ const existingApiKey = flags.pennyKey || launchConfig.api_key || globalAuth.apiKey;
679
+ const existingGateway = launchConfig.gateway_base_url || globalAuth.gatewayBaseUrl
680
+ || DEFAULT_GATEWAY_BASE_URL;
681
+ const requestedGateway = flags.gatewayBaseUrl || existingGateway;
682
+ const reusableKey = existingApiKey
683
+ && sameGatewayBaseUrl(requestedGateway, existingGateway)
684
+ ? existingApiKey
685
+ : null;
686
+
687
+ if (reusableKey) {
688
+ gatewayBaseUrl = existingGateway;
689
+ claim = { api_key: reusableKey, gateway_base_url: gatewayBaseUrl };
690
+ console.log(`Using existing PennyRouter key for ${gatewayBaseUrl}.`);
691
+ console.log("");
669
692
 
670
- console.log("Opening your browser to log in and authorize this machine with your PennyRouter account...");
671
- console.log(`If your browser did not open, visit:\n${session.authorize_url}`);
672
- if (!flags.noBrowser) openBrowser(session.authorize_url);
673
- console.log("");
674
- console.log("Waiting for approval...");
693
+ const claudeProviders = await selectClaudeFundingProviders(flags, needsAuth);
694
+ const codexProviders = await selectCodexFundingProviders(flags, needsAuth);
695
+ claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(
696
+ flags, needsAuth, claudeProviders.has("subscription"));
697
+ funding = await configureUserFunding(
698
+ flags, needsAuth, claudeAnthropicAuth, claudeProviders, codexProviders);
675
699
 
676
- claim = await pollForKey(session);
677
- console.log(`Authorized. Received key ${maskKey(claim.api_key)}.`);
678
- console.log("");
700
+ for (const credential of funding.credentials) {
701
+ await withGatewayContext(gatewayBaseUrl, () => storeUpstreamCredential({
702
+ gatewayBaseUrl,
703
+ apiKey: claim.api_key,
704
+ provider: credential.provider,
705
+ token: credential.token,
706
+ metadata: credential.metadata,
707
+ }));
708
+ }
709
+ } else {
710
+ const session = await createCliSession({
711
+ appUrl: flags.appUrl || APP_URL,
712
+ harnesses: selected.map((harness) => harness.id),
713
+ accountMode: "existing",
714
+ });
679
715
 
680
- const claudeProviders = await selectClaudeFundingProviders(flags, needsAuth);
681
- const codexProviders = await selectCodexFundingProviders(flags, needsAuth);
682
- claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(
683
- flags, needsAuth, claudeProviders.has("subscription"));
684
- funding = await configureUserFunding(
685
- flags, needsAuth, claudeAnthropicAuth, claudeProviders, codexProviders);
686
- gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
687
- if (flags.gatewayBaseUrl) {
688
- const normalized = normalizeGatewayBaseUrl(flags.gatewayBaseUrl);
689
- if (!normalized) {
690
- throw new Error(
691
- `--gateway-base-url "${flags.gatewayBaseUrl}" is not a usable URL. Use a host like `
692
- + "localhost:8400 or https://gateway.example.com.",
693
- );
716
+ console.log("Opening your browser to log in and authorize this machine with your PennyRouter account...");
717
+ console.log(`If your browser did not open, visit:\n${session.authorize_url}`);
718
+ if (!flags.noBrowser) openBrowser(session.authorize_url);
719
+ console.log("");
720
+ console.log("Waiting for approval...");
721
+
722
+ claim = await pollForKey(session);
723
+ console.log(`Authorized. Received key ${maskKey(claim.api_key)}.`);
724
+ console.log("");
725
+
726
+ const claudeProviders = await selectClaudeFundingProviders(flags, needsAuth);
727
+ const codexProviders = await selectCodexFundingProviders(flags, needsAuth);
728
+ claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(
729
+ flags, needsAuth, claudeProviders.has("subscription"));
730
+ funding = await configureUserFunding(
731
+ flags, needsAuth, claudeAnthropicAuth, claudeProviders, codexProviders);
732
+ gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
733
+ if (flags.gatewayBaseUrl) {
734
+ const normalized = normalizeGatewayBaseUrl(flags.gatewayBaseUrl);
735
+ if (!normalized) {
736
+ throw new Error(
737
+ `--gateway-base-url "${flags.gatewayBaseUrl}" is not a usable URL. Use a host like `
738
+ + "localhost:8000 or https://gateway.example.com.",
739
+ );
740
+ }
741
+ gatewayBaseUrl = normalized;
694
742
  }
695
- gatewayBaseUrl = normalized;
696
- }
697
743
 
698
- for (const credential of funding.credentials) {
699
- await withGatewayContext(gatewayBaseUrl, () => storeUpstreamCredential({
700
- gatewayBaseUrl,
701
- apiKey: claim.api_key,
702
- provider: credential.provider,
703
- token: credential.token,
704
- metadata: credential.metadata,
705
- }));
744
+ for (const credential of funding.credentials) {
745
+ await withGatewayContext(gatewayBaseUrl, () => storeUpstreamCredential({
746
+ gatewayBaseUrl,
747
+ apiKey: claim.api_key,
748
+ provider: credential.provider,
749
+ token: credential.token,
750
+ metadata: credential.metadata,
751
+ }));
752
+ }
706
753
  }
707
754
  }
708
755
 
@@ -926,7 +973,7 @@ async function resolvePrivateGatewayUrl(flags) {
926
973
  /* The key for a private gateway: an admin-issued one if the user has it, otherwise minted
927
974
  from the gateway itself against their email. --private-key wins so an administrator can
928
975
  still hand out keys on a gateway that also allows self-service. */
929
- async function obtainPrivateGatewayKey(flags, gatewayBaseUrl) {
976
+ async function obtainPrivateGatewayKey(flags, gatewayBaseUrl, existingApiKey = null) {
930
977
  const issued = (flags.privateKey || "").trim();
931
978
  if (issued) {
932
979
  if (!issued.startsWith("pr-")) {
@@ -937,6 +984,12 @@ async function obtainPrivateGatewayKey(flags, gatewayBaseUrl) {
937
984
  return issued;
938
985
  }
939
986
 
987
+ if (existingApiKey) {
988
+ console.log(`Using existing PennyRouter key for ${gatewayBaseUrl}.`);
989
+ console.log("");
990
+ return existingApiKey;
991
+ }
992
+
940
993
  let email = (flags.email || "").trim();
941
994
  if (!email) {
942
995
  // A shared --config file names the gateway but not who is installing, so the email is
@@ -985,6 +1038,12 @@ function normalizeGatewayBaseUrl(raw) {
985
1038
  return withScheme.replace(/\/+$/, "");
986
1039
  }
987
1040
 
1041
+ function sameGatewayBaseUrl(left, right) {
1042
+ const normalizedLeft = normalizeGatewayBaseUrl(left);
1043
+ const normalizedRight = normalizeGatewayBaseUrl(right);
1044
+ return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
1045
+ }
1046
+
988
1047
  /* A gateway that is unreachable (wrong host, wrong port, not running) surfaces from fetch() as
989
1048
  an opaque "fetch failed" with no status. Name the gateway we could not reach so the user knows
990
1049
  which URL to fix. Real HTTP errors carry their own message and pass through untouched. */