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.
- package/package.json +1 -1
- package/src/index.js +32 -0
- package/src/tui.js +134 -27
package/package.json
CHANGED
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 === '
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
this.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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.
|
|
717
|
-
buf.push(
|
|
718
|
-
buf.push(dim('
|
|
719
|
-
buf.push(dim('
|
|
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('
|
|
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': {
|