@karpeleslab/teamclaude 1.0.8 → 1.0.9
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 +9 -1
- package/src/oauth.js +3 -3
- package/src/tui.js +147 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karpeleslab/teamclaude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Multi-account Claude proxy with automatic quota-based rotation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
"multi-account"
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/KarpelesLab/teamclaude.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/KarpelesLab/teamclaude/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/KarpelesLab/teamclaude#readme",
|
|
26
34
|
"engines": {
|
|
27
35
|
"node": ">=18.0.0"
|
|
28
36
|
},
|
package/src/oauth.js
CHANGED
|
@@ -149,15 +149,15 @@ export async function fetchProfile(accessToken) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// Normalize one usage bucket from the /api/oauth/usage payload into
|
|
152
|
-
// { utilization: 0-1, resetAt: ms-epoch }.
|
|
153
|
-
// percentage
|
|
152
|
+
// { utilization: 0-1, resetAt: ms-epoch }. The endpoint reports utilization
|
|
153
|
+
// as a percentage in the 0-100 range, so 1 means 1%, not 100%.
|
|
154
154
|
export function normalizeUsageBucket(bucket) {
|
|
155
155
|
if (!bucket || typeof bucket !== 'object') return null;
|
|
156
156
|
|
|
157
157
|
const rawPct = bucket.used_percentage ?? bucket.utilization ?? bucket.usedPercentage;
|
|
158
158
|
const parsedPct = typeof rawPct === 'number' ? rawPct : parseFloat(rawPct);
|
|
159
159
|
const utilization = Number.isFinite(parsedPct)
|
|
160
|
-
?
|
|
160
|
+
? parsedPct / 100
|
|
161
161
|
: null;
|
|
162
162
|
|
|
163
163
|
const rawReset = bucket.resets_at ?? bucket.resetsAt ?? bucket.reset_at ?? bucket.resetAt;
|
package/src/tui.js
CHANGED
|
@@ -8,6 +8,7 @@ const ESC = '\x1b[';
|
|
|
8
8
|
const RESET = `${ESC}0m`;
|
|
9
9
|
const BOLD = `${ESC}1m`;
|
|
10
10
|
const DIM = `${ESC}2m`;
|
|
11
|
+
const REV = `${ESC}7m`; // reverse video — used for the BIOS-style settings cursor
|
|
11
12
|
|
|
12
13
|
const bold = s => `${BOLD}${s}${RESET}`;
|
|
13
14
|
const dim = s => `${DIM}${s}${RESET}`;
|
|
@@ -129,9 +130,11 @@ export class TUI {
|
|
|
129
130
|
this.mode = 'normal'; // normal | select | add | input | settings
|
|
130
131
|
this.selAction = null; // switch | remove
|
|
131
132
|
this.selIdx = 0;
|
|
133
|
+
this.setIdx = 0; // cursor row on the settings screen (BIOS-style nav)
|
|
132
134
|
this.inputPrompt = '';
|
|
133
135
|
this.inputBuf = '';
|
|
134
136
|
this.inputCb = null;
|
|
137
|
+
this.inputReturn = 'normal'; // mode to fall back to when an input is cancelled
|
|
135
138
|
this.frame = 0;
|
|
136
139
|
this.running = false;
|
|
137
140
|
this.timer = null;
|
|
@@ -208,6 +211,8 @@ export class TUI {
|
|
|
208
211
|
_onData(d) {
|
|
209
212
|
if (d === '\x1b[A') return this._key('up');
|
|
210
213
|
if (d === '\x1b[B') return this._key('down');
|
|
214
|
+
if (d === '\x1b[C') return this._key('right');
|
|
215
|
+
if (d === '\x1b[D') return this._key('left');
|
|
211
216
|
if (d === '\x1b') return this._key('esc');
|
|
212
217
|
if (d === '\r' || d === '\n') return this._key('enter');
|
|
213
218
|
if (d === '\x03') return this._key('ctrl-c');
|
|
@@ -241,33 +246,118 @@ export class TUI {
|
|
|
241
246
|
}
|
|
242
247
|
else if (k === 'a') { this.mode = 'add'; }
|
|
243
248
|
else if (k === 'R') { this._doSync(); }
|
|
244
|
-
else if (k === 'g' && this.sx) { this.mode = 'settings'; this._loadSxBalance(); }
|
|
249
|
+
else if (k === 'g' && this.sx) { this.mode = 'settings'; this.setIdx = 0; this._loadSxBalance(); }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Navigable rows on the settings screen, top to bottom. Both the renderer and
|
|
253
|
+
// the key handler build this list so the cursor and the display stay in sync.
|
|
254
|
+
// Rows are conditional (sx.org rows only when that build feature is present),
|
|
255
|
+
// so always index through the returned array — never hard-code positions.
|
|
256
|
+
_settingsFields() {
|
|
257
|
+
const fields = [];
|
|
258
|
+
|
|
259
|
+
fields.push({
|
|
260
|
+
id: 'threshold',
|
|
261
|
+
label: 'Switch threshold',
|
|
262
|
+
hint: '←→ ±1%',
|
|
263
|
+
value: () => {
|
|
264
|
+
const thr = this.am.switchThreshold ?? this.config.switchThreshold ?? 0.98;
|
|
265
|
+
return green(`${Math.round(thr * 100)}%`);
|
|
266
|
+
},
|
|
267
|
+
left: () => this._nudgeThreshold(-1),
|
|
268
|
+
right: () => this._nudgeThreshold(+1),
|
|
269
|
+
enter: () => this._promptInput('Switch threshold % (1-100)', v => this._doSetThreshold(v.trim())),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
fields.push({
|
|
273
|
+
id: 'probe',
|
|
274
|
+
label: 'Quota probe',
|
|
275
|
+
hint: '←→ ±30s',
|
|
276
|
+
value: () => {
|
|
277
|
+
const probe = this.config.quotaProbeSeconds || 0;
|
|
278
|
+
return probe > 0 ? green(`${probe}s`) : gray('off (passive)');
|
|
279
|
+
},
|
|
280
|
+
left: () => this._nudgeProbe(-30),
|
|
281
|
+
right: () => this._nudgeProbe(+30),
|
|
282
|
+
enter: () => this._promptInput('Quota probe seconds (0=off, min 30)', v => this._doSetProbe(v.trim())),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (this.sx) {
|
|
286
|
+
fields.push({
|
|
287
|
+
id: 'sxmode',
|
|
288
|
+
label: 'sx.org mode',
|
|
289
|
+
hint: '←→ cycle',
|
|
290
|
+
value: () => {
|
|
291
|
+
const mode = this.sx.getMode();
|
|
292
|
+
return mode === 'always' ? green('always')
|
|
293
|
+
: mode === '429' ? cyan('on 429 only')
|
|
294
|
+
: gray('off');
|
|
295
|
+
},
|
|
296
|
+
left: () => this._cycleSxMode(-1),
|
|
297
|
+
right: () => this._cycleSxMode(+1),
|
|
298
|
+
enter: () => this._cycleSxMode(+1),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
fields.push({
|
|
302
|
+
id: 'sxkey',
|
|
303
|
+
label: 'sx.org API key',
|
|
304
|
+
hint: 'Enter to set',
|
|
305
|
+
value: () => {
|
|
306
|
+
const key = this.config.sx?.apiKey;
|
|
307
|
+
return key ? key.slice(0, 4) + '…' + key.slice(-4) : dim('(not set)');
|
|
308
|
+
},
|
|
309
|
+
enter: () => this._promptInput('sx.org API key', v => this._doSetSxKey(v.trim())),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (this.config.sx?.apiKey) {
|
|
313
|
+
fields.push({
|
|
314
|
+
id: 'sxclear',
|
|
315
|
+
label: 'Clear sx.org key',
|
|
316
|
+
hint: 'Enter to clear',
|
|
317
|
+
value: () => dim('—'),
|
|
318
|
+
enter: () => this._doClearSxKey(),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return fields;
|
|
245
324
|
}
|
|
246
325
|
|
|
247
326
|
_keySettings(k) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
else if (k === '
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
this.inputCb = v => { if (v) this._doSetProbe(v.trim()); };
|
|
259
|
-
}
|
|
260
|
-
else if (k === 'k') {
|
|
261
|
-
this.mode = 'input';
|
|
262
|
-
this.inputPrompt = 'sx.org API key';
|
|
263
|
-
this.inputBuf = '';
|
|
264
|
-
this.inputCb = v => { if (v) this._doSetSxKey(v.trim()); };
|
|
265
|
-
}
|
|
266
|
-
else if (k === 'm') { this._doCycleSxMode(); }
|
|
267
|
-
else if (k === 'x') { this._doClearSxKey(); }
|
|
327
|
+
const fields = this._settingsFields();
|
|
328
|
+
const n = fields.length;
|
|
329
|
+
if (n > 0 && this.setIdx >= n) this.setIdx = n - 1;
|
|
330
|
+
const f = fields[this.setIdx];
|
|
331
|
+
|
|
332
|
+
if (k === 'up' || k === 'k') this.setIdx = (this.setIdx - 1 + n) % n;
|
|
333
|
+
else if (k === 'down' || k === 'j') this.setIdx = (this.setIdx + 1) % n;
|
|
334
|
+
else if (k === 'left') f?.left?.();
|
|
335
|
+
else if (k === 'right') f?.right?.();
|
|
336
|
+
else if (k === 'enter') f?.enter?.();
|
|
268
337
|
else if (k === 'esc' || k === 'q') { this.mode = 'normal'; }
|
|
269
338
|
}
|
|
270
339
|
|
|
340
|
+
// Open the text-input prompt and return to the settings screen afterward.
|
|
341
|
+
_promptInput(prompt, cb) {
|
|
342
|
+
this.mode = 'input';
|
|
343
|
+
this.inputReturn = 'settings';
|
|
344
|
+
this.inputPrompt = prompt;
|
|
345
|
+
this.inputBuf = '';
|
|
346
|
+
this.inputCb = v => { if (v) cb(v); };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_nudgeThreshold(deltaPct) {
|
|
350
|
+
const cur = Math.round((this.am.switchThreshold ?? this.config.switchThreshold ?? 0.98) * 100);
|
|
351
|
+
const next = Math.max(1, Math.min(100, cur + deltaPct));
|
|
352
|
+
if (next !== cur) this._doSetThreshold(String(next));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
_nudgeProbe(deltaSec) {
|
|
356
|
+
const cur = this.config.quotaProbeSeconds || 0;
|
|
357
|
+
const next = Math.max(0, cur + deltaSec);
|
|
358
|
+
if (next !== cur) this._doSetProbe(String(next));
|
|
359
|
+
}
|
|
360
|
+
|
|
271
361
|
async _doSetThreshold(input) {
|
|
272
362
|
const pct = Number(input);
|
|
273
363
|
if (!Number.isFinite(pct) || pct < 1 || pct > 100) {
|
|
@@ -322,6 +412,7 @@ export class TUI {
|
|
|
322
412
|
if (k === 'i') { this._doImport(); this.mode = 'normal'; }
|
|
323
413
|
else if (k === 'k') {
|
|
324
414
|
this.mode = 'input';
|
|
415
|
+
this.inputReturn = 'normal';
|
|
325
416
|
this.inputPrompt = 'API key';
|
|
326
417
|
this.inputBuf = '';
|
|
327
418
|
this.inputCb = v => { if (v) this._doAddKey(v); };
|
|
@@ -333,10 +424,10 @@ export class TUI {
|
|
|
333
424
|
if (k === 'enter') {
|
|
334
425
|
const cb = this.inputCb;
|
|
335
426
|
const v = this.inputBuf;
|
|
336
|
-
this.mode =
|
|
427
|
+
this.mode = this.inputReturn; this.inputCb = null; this.inputBuf = '';
|
|
337
428
|
cb?.(v);
|
|
338
429
|
}
|
|
339
|
-
else if (k === 'esc') { this.mode =
|
|
430
|
+
else if (k === 'esc') { this.mode = this.inputReturn; this.inputCb = null; this.inputBuf = ''; }
|
|
340
431
|
else if (k === 'bs') { this.inputBuf = this.inputBuf.slice(0, -1); }
|
|
341
432
|
else if (k.length === 1) { this.inputBuf += k; }
|
|
342
433
|
}
|
|
@@ -383,11 +474,11 @@ export class TUI {
|
|
|
383
474
|
if (this.running) this.render();
|
|
384
475
|
}
|
|
385
476
|
|
|
386
|
-
// Cycle off → on-429 → always
|
|
387
|
-
// sx.org without deconfiguring it.
|
|
388
|
-
async
|
|
477
|
+
// Cycle off → on-429 → always (dir +1) or the reverse (dir -1). Keeps the API
|
|
478
|
+
// key, so the user can disable sx.org without deconfiguring it.
|
|
479
|
+
async _cycleSxMode(dir = 1) {
|
|
389
480
|
const order = ['off', '429', 'always'];
|
|
390
|
-
const next = order[(order.indexOf(this.sx.getMode()) +
|
|
481
|
+
const next = order[(order.indexOf(this.sx.getMode()) + dir + order.length) % order.length];
|
|
391
482
|
this.config.sx = { ...(this.config.sx || {}), mode: next };
|
|
392
483
|
try { await this.saveConfig(this.config); }
|
|
393
484
|
catch (e) { this._addLog(`Failed to save: ${e.message}`); }
|
|
@@ -672,37 +763,53 @@ export class TUI {
|
|
|
672
763
|
}
|
|
673
764
|
|
|
674
765
|
_renderSettings(lines) {
|
|
766
|
+
const fields = this._settingsFields();
|
|
767
|
+
if (this.setIdx >= fields.length) this.setIdx = Math.max(0, fields.length - 1);
|
|
768
|
+
const selId = fields[this.setIdx]?.id;
|
|
769
|
+
const byId = id => fields.find(f => f.id === id);
|
|
770
|
+
|
|
771
|
+
// Render a navigable setting row with a BIOS-style highlight bar on the
|
|
772
|
+
// cursor row. Read-only info rows pass field=null and never highlight.
|
|
773
|
+
const row = field => {
|
|
774
|
+
const selected = field && field.id === selId;
|
|
775
|
+
const label = (field ? field.label : '').padEnd(16);
|
|
776
|
+
const value = field ? field.value() : '';
|
|
777
|
+
if (selected) {
|
|
778
|
+
const hint = field.hint ? ` ${dim(field.hint)}` : '';
|
|
779
|
+
const inner = rpad(` ${label} ${strip(value)} `, 34);
|
|
780
|
+
return ` ${cyan('▸')}${REV}${inner}${RESET}${hint}`;
|
|
781
|
+
}
|
|
782
|
+
return ` ${dim(label)} ${value}`;
|
|
783
|
+
};
|
|
784
|
+
// A plain read-only info line (not selectable), aligned with the rows above.
|
|
785
|
+
const info = (label, value) => ` ${dim(label.padEnd(16))} ${value}`;
|
|
786
|
+
|
|
675
787
|
lines.push('');
|
|
676
788
|
// ── Rotation
|
|
677
|
-
const thr = this.am.switchThreshold ?? this.config.switchThreshold ?? 0.98;
|
|
678
789
|
lines.push(bold(' Rotation') + dim(' — switch accounts when quota crosses the threshold'));
|
|
679
|
-
lines.push(
|
|
790
|
+
lines.push(row(byId('threshold')));
|
|
680
791
|
lines.push('');
|
|
681
792
|
// ── Quota probe
|
|
682
|
-
const probe = this.config.quotaProbeSeconds || 0;
|
|
683
793
|
lines.push(bold(' Quota probe') + dim(' — refresh idle accounts from the usage endpoint'));
|
|
684
|
-
lines.push(
|
|
794
|
+
lines.push(row(byId('probe')));
|
|
685
795
|
lines.push('');
|
|
686
796
|
// ── sx.org
|
|
687
797
|
lines.push(bold(' sx.org proxy') + dim(' — route upstream via a residential IP (429 workaround)'));
|
|
688
798
|
lines.push('');
|
|
689
799
|
if (!this.sx) { lines.push(yellow(' Unavailable in this build.')); return; }
|
|
690
800
|
const key = this.config.sx?.apiKey;
|
|
691
|
-
const masked = key ? key.slice(0, 4) + '…' + key.slice(-4) : dim('(not set)');
|
|
692
801
|
const mode = this.sx.getMode();
|
|
693
|
-
const modeStr = mode === 'always' ? green('always')
|
|
694
|
-
: mode === '429' ? cyan('on 429 only')
|
|
695
|
-
: gray('off');
|
|
696
802
|
const p = this.sx.getProxy?.();
|
|
697
803
|
const proxyStr = mode === 'off' ? gray('—')
|
|
698
804
|
: this.sx.isProvisioned() ? green(`${p.host}:${p.port}`)
|
|
699
805
|
: key ? yellow('not provisioned')
|
|
700
806
|
: gray('no key');
|
|
701
807
|
const b = this.sxBalance;
|
|
702
|
-
lines.push(
|
|
703
|
-
lines.push(
|
|
704
|
-
lines.push(
|
|
705
|
-
lines.push(
|
|
808
|
+
lines.push(row(byId('sxmode')));
|
|
809
|
+
lines.push(row(byId('sxkey')));
|
|
810
|
+
lines.push(info('Proxy', proxyStr));
|
|
811
|
+
lines.push(info('Balance', b ? green('$' + Number(b.balance).toFixed(4)) : dim('…')));
|
|
812
|
+
if (byId('sxclear')) lines.push(row(byId('sxclear')));
|
|
706
813
|
lines.push('');
|
|
707
814
|
lines.push(dim(' always tunnel ALL upstream traffic through sx.org'));
|
|
708
815
|
lines.push(dim(' on 429 only retry through sx.org after a 429 (fresh IP)'));
|
|
@@ -716,7 +823,7 @@ export class TUI {
|
|
|
716
823
|
case 'normal':
|
|
717
824
|
return ` ${bold('s')}witch ${bold('a')}dd ${bold('r')}emove ${bold('d')}isable ${bold('R')}eload ${bold('g')} settings ${bold('q')}uit`;
|
|
718
825
|
case 'settings':
|
|
719
|
-
return ` ${
|
|
826
|
+
return ` ${dim('↑↓')} navigate ${dim('←→')} change ${bold('Enter')} edit ${bold('Esc')} back`;
|
|
720
827
|
case 'select': {
|
|
721
828
|
const act = this.selAction === 'switch' ? 'switch'
|
|
722
829
|
: this.selAction === 'toggle' ? 'enable/disable'
|