maxpool 1.5.77 → 1.5.78

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/package.json +1 -1
  2. package/src/index.js +32 -0
  3. package/src/tui.js +134 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.5.77",
3
+ "version": "1.5.78",
4
4
  "description": "Multi-account Claude Code proxy with adaptive, rate-aware load balancing across Claude accounts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -2060,6 +2060,38 @@ async function syncAccountsFromDisk(diskConfig, memConfig, accountManager) {
2060
2060
  if (memConfig.routing.mode === 'preferred' && !preferredApplied) {
2061
2061
  memConfig.routing = { mode: 'automatic', preferredAccount: null };
2062
2062
  }
2063
+
2064
+ // Config PROVIDERS (GLM/Kimi) sync too. Without this a provider added to config —
2065
+ // by the TUI or by hand — stayed invisible until a full restart, because this
2066
+ // function only ever walked `accounts`. Reported 2026-08-10: a new GLM key was in
2067
+ // GCP and in config and served ZERO requests.
2068
+ if (Array.isArray(diskConfig.providers)) {
2069
+ const before = JSON.stringify(memConfig.providers || []);
2070
+ const after = JSON.stringify(diskConfig.providers);
2071
+ if (before !== after) {
2072
+ try {
2073
+ const { resolveSecrets } = await import('./secret-resolver.js');
2074
+ const names = diskConfig.providers.filter(p => p.secretName).map(p => p.secretName);
2075
+ const resolved = await resolveSecrets(names);
2076
+ const entries = diskConfig.providers.map(p => ({
2077
+ ...p,
2078
+ token: p.secretName ? (resolved[p.secretName] || null) : (p.apiKey || null),
2079
+ }));
2080
+ accountManager.loadConfigProviders(entries);
2081
+ for (const p of diskConfig.providers) {
2082
+ const a = accountManager.accounts.find(a => a.name === p.name);
2083
+ if (a && p.secretName) a.secretName = p.secretName;
2084
+ }
2085
+ memConfig.providers = diskConfig.providers;
2086
+ const ok = entries.filter(e => e.token).length;
2087
+ console.log(`[Maxpool] Config providers re-synced from disk: ${ok} active`);
2088
+ added += Math.max(0, entries.length - JSON.parse(before).length);
2089
+ } catch (err) {
2090
+ console.error(`[Maxpool] Provider re-sync failed: ${err.message}`);
2091
+ }
2092
+ }
2093
+ }
2094
+
2063
2095
  return added;
2064
2096
  }
2065
2097
 
package/src/tui.js CHANGED
@@ -257,7 +257,10 @@ export class TUI {
257
257
 
258
258
  this.log = []; // completed activity entries
259
259
  this.active = new Map(); // in-flight requests
260
- this.mode = 'normal'; // normal | accounts | routing | select | input | confirm
260
+ this.mode = 'normal'; // normal | accounts | routing | select | input | confirm | providers | addtype
261
+ // Hide disabled accounts from the table. With 8 dead/disabled accounts the live
262
+ // ones scroll off the top; `h` collapses them to a one-line summary.
263
+ this.hideDisabled = false;
261
264
  this.selAction = null; // prefer | toggle | delete
262
265
  this.selIdx = 0;
263
266
  this.inputPrompt = '';
@@ -406,6 +409,7 @@ export class TUI {
406
409
  case 'routing': this._keyRouting(k); break;
407
410
  case 'updates': this._keyUpdates(k); break;
408
411
  case 'providers': this._keyProviders(k); break;
412
+ case 'addtype': this._keyAddType(k); break;
409
413
  case 'select': this._keySelect(k); break;
410
414
  case 'input': this._keyInput(k); break;
411
415
  case 'confirm': this._keyConfirm(k); break;
@@ -445,6 +449,9 @@ export class TUI {
445
449
  this.mode = 'updates';
446
450
  } else if (k === 'p') {
447
451
  this.mode = 'providers';
452
+ } else if (k === 'h') {
453
+ this.hideDisabled = !this.hideDisabled;
454
+ this._addLog(this.hideDisabled ? 'Hiding disabled accounts' : 'Showing all accounts');
448
455
  }
449
456
  // Enable/disable lives ONLY under [a] Accounts now (with rename/delete/login) —
450
457
  // one home for every account mutation, instead of a duplicate top-level toggle.
@@ -555,19 +562,15 @@ export class TUI {
555
562
  }
556
563
 
557
564
  _keyAccounts(k) {
558
- if (k === 'k') {
559
- this.mode = 'input';
560
- this.inputPrompt = 'Anthropic API key';
561
- this.inputBuf = '';
562
- this.inputSensitive = true;
563
- this.inputCb = value => {
564
- if (!value) return;
565
- this._confirm(
566
- 'Add this API key?',
567
- 'Store it in Maxpool config as a new Anthropic API account.',
568
- () => this._doAddKey(value),
569
- );
570
- };
565
+ if (k === 'a') {
566
+ // ONE entry point for every account type. Previously `l` (browser) and `k`
567
+ // (Anthropic API key) were the only options and GLM/Kimi had no path at all —
568
+ // the provider screen existed but nothing in the Accounts UI led to it
569
+ // (reported 2026-08-10: "it only offers anthropic accounts via browser").
570
+ this.mode = 'addtype';
571
+ } else if (k === 'k') {
572
+ // Kept as a shortcut; the `a` flow reaches the same place.
573
+ this._addAccountOfType('anthropic-key');
571
574
  } else if (k === 'l') {
572
575
  this._confirm(
573
576
  'Log in / re-authenticate via browser?',
@@ -585,6 +588,89 @@ export class TUI {
585
588
  }
586
589
  }
587
590
 
591
+ // ── unified add-account flow ────────────────────────────────────────────────
592
+ // Four account types, two credential sources. Every path lands here so there is
593
+ // exactly ONE thing to learn: press `a`, pick a type, supply a credential.
594
+ static ADD_TYPES = [
595
+ { key: '1', id: 'anthropic-oauth', label: 'Anthropic subscription (browser login)' },
596
+ { key: '2', id: 'anthropic-key', label: 'Anthropic API key' },
597
+ { key: '3', id: 'zai', label: 'GLM / z.ai API key' },
598
+ { key: '4', id: 'kimi', label: 'Kimi / Moonshot API key' },
599
+ ];
600
+
601
+ _keyAddType(k) {
602
+ if (k === 'esc' || k === 'q') { this.mode = 'accounts'; return; }
603
+ const pick = TUI.ADD_TYPES.find(t => t.key === k);
604
+ if (pick) this._addAccountOfType(pick.id);
605
+ }
606
+
607
+ _addAccountOfType(typeId) {
608
+ if (typeId === 'anthropic-oauth') {
609
+ this.mode = 'accounts';
610
+ this._confirm(
611
+ 'Log in / re-authenticate via browser?',
612
+ 'Opens a browser. Logging into an account that is already added RE-AUTHENTICATES it in place — no duplicate.',
613
+ () => this._doLogin(),
614
+ );
615
+ return;
616
+ }
617
+ if (typeId === 'anthropic-key') {
618
+ this.mode = 'input';
619
+ this.inputPrompt = 'Anthropic API key — paste it, or type a GCP secret name';
620
+ this.inputBuf = '';
621
+ this.inputSensitive = false; // a GCP NAME is not a secret; the key is masked on save
622
+ this.inputCb = async value => {
623
+ const input = String(value || '').trim();
624
+ if (!input) { this.mode = 'accounts'; return; }
625
+ const key = await this._resolveCredential(input);
626
+ if (!key) return;
627
+ this.mode = 'accounts';
628
+ this._confirm('Add this API key?', 'Store it in Maxpool config as a new Anthropic API account.',
629
+ () => this._doAddKey(key));
630
+ };
631
+ return;
632
+ }
633
+ // GLM / Kimi — the provider path. Same two credential sources.
634
+ this._providerAddStep('secret', { provider: typeId === 'kimi' ? 'kimi' : 'zai' });
635
+ }
636
+
637
+ /** Accept EITHER a pasted API key or a GCP Secret Manager name, and return the key.
638
+ * A GCP name is UPPER_SNAKE with no dots; real keys carry dots/mixed case. Resolving
639
+ * here (not at each call site) is what makes "type it or point at GCP" one concept. */
640
+ async _resolveCredential(input) {
641
+ const looksLikeSecretName = /^[A-Z][A-Z0-9_-]{2,60}$/.test(input) && !input.includes('.');
642
+ if (!looksLikeSecretName) return input; // a pasted key
643
+ this._addLog(`Resolving GCP secret "${input}"…`);
644
+ this.render();
645
+ try {
646
+ const { resolveSecret } = await import('../secret-resolver.js');
647
+ const v = await resolveSecret(input);
648
+ if (!v) {
649
+ this._addLog(`✗ GCP secret "${input}" not found (or gcloud is not authenticated). ` +
650
+ 'Run: gcloud auth application-default login');
651
+ this.mode = 'accounts';
652
+ return null;
653
+ }
654
+ return v;
655
+ } catch (err) {
656
+ this._addLog(`✗ Could not reach GCP: ${err.message}`);
657
+ this.mode = 'accounts';
658
+ return null;
659
+ }
660
+ }
661
+
662
+ _renderAddType(buf) {
663
+ buf.push(`${bold('Add an account')} ${dim('— pick a type')}`);
664
+ buf.push('');
665
+ for (const t of TUI.ADD_TYPES) {
666
+ buf.push(` ${bold(t.key)} ${t.label}`);
667
+ }
668
+ buf.push('');
669
+ buf.push(dim(' API keys: paste the key, OR type a GCP Secret Manager name'));
670
+ buf.push(dim(' (e.g. RESTRICTED_MAX_MAXPOOL_ZAI_PLUS1) to keep it out of your config.'));
671
+ buf.push(dim(' GCP needs: gcloud auth application-default login'));
672
+ }
673
+
588
674
  // Derive a human-readable account name from a GCP secret name.
589
675
  // RESTRICTED_AL_MAXPOOL_ZAI → glm al | ZAI_API_KEY → glm primary
590
676
  // RESTRICTED_MAX_KIMI_API_KEY → kimi max | KIMI_API_KEY → kimi primary
@@ -713,10 +799,16 @@ export class TUI {
713
799
  if (!providers.length) {
714
800
  buf.push(dim(' No providers configured.'));
715
801
  buf.push('');
716
- buf.push(dim(' Press ') + bold('a') + dim(' to add one. You\'ll need:'));
717
- buf.push(dim(' 1. An API key from z.ai (GLM) or Moonshot (Kimi)'));
718
- buf.push(dim(' 2. The key stored in GCP: ') + 'gcloud secrets create <name> --data-file=-');
719
- buf.push(dim(' 3. The GCP secret name (e.g. RESTRICTED_MAXPOOL_ZAI_NEW)'));
802
+ buf.push(dim(' Press ') + bold('a') + dim(' to add one. Two ways to supply the key:'));
803
+ buf.push('');
804
+ buf.push(dim(' A) Paste the API key directly — stored in your config (0600).'));
805
+ buf.push(dim(' Simplest; works with no cloud setup.'));
806
+ buf.push('');
807
+ buf.push(dim(' B) Point at a GCP Secret Manager name — the key never touches disk.'));
808
+ buf.push(dim(' Store it: ') + 'gcloud secrets create MY_KEY --data-file=-');
809
+ buf.push(dim(' Maxpool resolves it via YOUR gcloud login:'));
810
+ buf.push(dim(' ') + 'gcloud auth application-default login');
811
+ buf.push(dim(' Delete the secret and the provider stops working everywhere.'));
720
812
  return;
721
813
  }
722
814
  for (const a of providers) {
@@ -1427,9 +1519,14 @@ export class TUI {
1427
1519
  // display order over the canonical am.accounts array (which stays untouched so
1428
1520
  // routing/index-keyed actions are unaffected). Selection navigation shares the
1429
1521
  // SAME order via _selectableIndexes → _displayOrder.
1522
+ let hidden = 0;
1430
1523
  for (const i of this._displayOrder()) {
1524
+ if (this.hideDisabled && this.am.accounts[i]?.enabled === false) { hidden++; continue; }
1431
1525
  lines.push(this._renderAcct(i, bw, showBoth));
1432
1526
  }
1527
+ if (hidden > 0) {
1528
+ lines.push(` ${dim(`… ${hidden} disabled account${hidden === 1 ? '' : 's'} hidden — press h to show`)}`);
1529
+ }
1433
1530
  // Glossary FOOTER (expands the abbreviations the header + inline labels can't
1434
1531
  // spell out). Below the rows so it never breaks the header↔column alignment.
1435
1532
  if (W >= 88) {
@@ -1464,6 +1561,21 @@ export class TUI {
1464
1561
  lines.push(` ${sp} ${gray(r.t)} ${r.method} ${r.path}${a} ${dim(`(${el}s...)`)}`);
1465
1562
  }
1466
1563
 
1564
+ // Providers panel — BEFORE the log so it lands inside the visible region. It used
1565
+ // to be pushed after the pad-to-full-height loop, so every line fell past the
1566
+ // bottom edge and the screen rendered only its header (reported 2026-08-10:
1567
+ // "when I click on providers I just see the title — how is that helpful?").
1568
+ if (this.mode === 'providers') {
1569
+ const pLines = [];
1570
+ this._renderProviders(pLines, W);
1571
+ lines.push('', ...pLines);
1572
+ }
1573
+ if (this.mode === 'addtype') {
1574
+ const aLines = [];
1575
+ this._renderAddType(aLines);
1576
+ lines.push('', ...aLines);
1577
+ }
1578
+
1467
1579
  // Completed log
1468
1580
  // 2 = separator + footer; confirm adds its detail line; updates adds its detail block.
1469
1581
  const footerH = this.mode === 'confirm' ? 3
@@ -1477,13 +1589,6 @@ export class TUI {
1477
1589
  // Pad to fill
1478
1590
  while (lines.length < H - footerH) lines.push('');
1479
1591
 
1480
- // Providers panel — shown when the user is on the providers screen
1481
- if (this.mode === 'providers' || this.mode === 'select') {
1482
- const pLines = [];
1483
- this._renderProviders(pLines, W);
1484
- lines.push(...pLines);
1485
- }
1486
-
1487
1592
  // ── Footer
1488
1593
  lines.push(' ' + dim('─'.repeat(W - 2)));
1489
1594
  if (this.mode === 'confirm') lines.push(` ${this.confirmDetail}`);
@@ -1744,13 +1849,15 @@ export class TUI {
1744
1849
  _renderFooter() {
1745
1850
  switch (this.mode) {
1746
1851
  case 'normal':
1747
- return ` ${bold('a')} Accounts ${bold('p')} Providers ${bold('m')} Routing ${bold('s')} Sync ${bold('u')} Updates ${bold('r')} Restart ${bold('q')} Stop`;
1852
+ return ` ${bold('a')} Accounts ${bold('p')} Providers ${bold('m')} Routing ${bold('h')} Hide disabled ${bold('s')} Sync ${bold('u')} Updates ${bold('r')} Restart ${bold('q')} Stop`;
1748
1853
  case 'updates': {
1749
1854
  const state = this._autoUpdateOn() ? green('on') : dim('off');
1750
1855
  return ` ${bold('c')} Check & apply now ${bold('t')} Automatic updates: ${state} ↻ ${bold('Esc')} Back`;
1751
1856
  }
1752
1857
  case 'accounts':
1753
- return ` ${bold('l')} Login/re-auth (browser) ${bold('k')} API key ${bold('n')} Rename ${bold('t')} Enable/disable ${bold('d')} Delete ${bold('Esc')} Back`;
1858
+ return ` ${bold('a')} Add account ${bold('l')} Re-auth (browser) ${bold('n')} Rename ${bold('t')} Enable/disable ${bold('d')} Delete ${bold('Esc')} Back`;
1859
+ case 'addtype':
1860
+ return ` ${bold('1')}-${bold('4')} pick a type ${bold('Esc')} Back`;
1754
1861
  case 'providers':
1755
1862
  return ` ${bold('a')} Add provider ${bold('d')} Delete ${bold('t')} Enable/disable ${bold('Esc')} Back`;
1756
1863
  case 'routing': {