@rikcodes/teamclaude 1.1.22-rik.4 → 1.1.22-rik.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rikcodes/teamclaude",
3
- "version": "1.1.22-rik.4",
3
+ "version": "1.1.22-rik.5",
4
4
  "description": "Multi-account proxy for Claude Code and Codex: pools Claude Max, ChatGPT/Codex, API-key and third-party backend accounts, and rotates on quota",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/config.js CHANGED
@@ -111,6 +111,7 @@ export function createDefaultConfig() {
111
111
  // the proxy does no throughput work at all (see OutputTracker).
112
112
  throughputMeter: false,
113
113
  projection: { enabled: true, windowMinutes: 90, wasteFloor: 0.1 },
114
+ accountSort: 'arranged',
114
115
  eventLogging: 'hide',
115
116
  defaultClientMode: 'mitm',
116
117
  // Written out rather than left absent, so a fresh config states the one
package/src/index.js CHANGED
@@ -535,6 +535,7 @@ async function serverCommand() {
535
535
  // Read by the server per request and by the TUI per frame, so the reload
536
536
  // is the whole application: the next request is timed, or is not.
537
537
  config.throughputMeter = diskConfig.throughputMeter === true;
538
+ config.accountSort = diskConfig.accountSort === 'weekly-reset' ? 'weekly-reset' : 'arranged';
538
539
  // Read by `run`/`env` from disk, but the TUI settings screen shows it live.
539
540
  config.defaultClientMode = diskConfig.defaultClientMode === 'base-url' ? 'base-url' : 'mitm';
540
541
  // The fleet switch for spending Codex reset credits. The redeemer reads it
@@ -636,6 +637,7 @@ async function serverCommand() {
636
637
  if (config.eventLogging != null) diskConfig.eventLogging = config.eventLogging;
637
638
  if (config.quotaBarPercent != null) diskConfig.quotaBarPercent = config.quotaBarPercent;
638
639
  if (config.throughputMeter != null) diskConfig.throughputMeter = config.throughputMeter;
640
+ if (config.accountSort != null) diskConfig.accountSort = config.accountSort;
639
641
  if (config.defaultClientMode != null) diskConfig.defaultClientMode = config.defaultClientMode;
640
642
  if (config.autoRedeemResets != null) diskConfig.autoRedeemResets = config.autoRedeemResets;
641
643
  if (config.blockedModels != null) diskConfig.blockedModels = config.blockedModels;
package/src/tui.js CHANGED
@@ -286,6 +286,23 @@ const PANE_MIN = 62;
286
286
  // the feature, and there is nothing to migrate.
287
287
  const listRank = (/** @type {any} */ a) => (Number.isFinite(a?.displayOrder) ? a.displayOrder : Infinity);
288
288
 
289
+ // How the account list is ordered inside each provider group (`accountSort`).
290
+ // `arranged` is the operator's own order (listRank); `weekly-reset` puts the
291
+ // account whose weekly window ends soonest first, so quota that is about to
292
+ // expire unspent is at the top of the list.
293
+ const ACCOUNT_SORTS = ['arranged', 'weekly-reset'];
294
+ /** @type {Record<string, string>} */
295
+ const ACCOUNT_SORT_LABELS = { arranged: 'arranged', 'weekly-reset': 'weekly reset' };
296
+
297
+ // When the account's weekly window resets — the sort key behind `weekly-reset`.
298
+ // No reading (an API-key account, or one that has not reported) and a reset
299
+ // that has already passed both sort last: the second is a window that has just
300
+ // started over, so its next reset is the one furthest away.
301
+ const weeklyResetRank = (/** @type {any} */ a, /** @type {number} */ now) => {
302
+ const t = a?.quota?.unified7dReset;
303
+ return Number.isFinite(t) && t > now ? t : Infinity;
304
+ };
305
+
289
306
  // How long a reorder waits after the last move before it is written. Longer
290
307
  // than a terminal's key-repeat interval, so a held arrow is one write; short
291
308
  // enough that the file is current by the time anyone looks at it.
@@ -1468,6 +1485,21 @@ export class TUI {
1468
1485
  });
1469
1486
  }
1470
1487
 
1488
+ if (this.am.accounts.length > 1) {
1489
+ fields.push({
1490
+ id: 'accountSort',
1491
+ label: 'Sort accounts',
1492
+ hint: '←→ cycle',
1493
+ value: () => {
1494
+ const s = this._accountSort();
1495
+ return s === 'arranged' ? gray(ACCOUNT_SORT_LABELS[s]) : green(ACCOUNT_SORT_LABELS[s]);
1496
+ },
1497
+ left: () => this._cycleAccountSort(-1),
1498
+ right: () => this._cycleAccountSort(+1),
1499
+ enter: () => this._cycleAccountSort(+1),
1500
+ });
1501
+ }
1502
+
1471
1503
  fields.push({
1472
1504
  id: 'upstreamProxy',
1473
1505
  label: 'Upstream proxy',
@@ -2093,6 +2125,24 @@ export class TUI {
2093
2125
  if (this.running) this.render();
2094
2126
  }
2095
2127
 
2128
+ /** The configured account sort; anything unknown reads as `arranged`. */
2129
+ _accountSort() {
2130
+ const s = this.config?.accountSort;
2131
+ return ACCOUNT_SORTS.includes(s) ? s : 'arranged';
2132
+ }
2133
+
2134
+ async _cycleAccountSort(dir = 1) {
2135
+ // Read by _displayOrder on every frame, so the assignment is the whole
2136
+ // application and the save is only what survives a restart.
2137
+ const prev = this.config.accountSort;
2138
+ const cur = this._accountSort();
2139
+ const next = ACCOUNT_SORTS[(ACCOUNT_SORTS.indexOf(cur) + dir + ACCOUNT_SORTS.length) % ACCOUNT_SORTS.length];
2140
+ this.config.accountSort = next;
2141
+ if (!await this._saveSetting('account sort', () => { this.config.accountSort = prev; })) return;
2142
+ this._addLog(`Account sort: ${ACCOUNT_SORT_LABELS[next]}`);
2143
+ if (this.running) this.render();
2144
+ }
2145
+
2096
2146
  async _cycleEventLogging(dir = 1) {
2097
2147
  // Claude Code telemetry display/handling: show → hide → block → show.
2098
2148
  const order = ['show', 'hide', 'block'];
@@ -2983,17 +3033,32 @@ export class TUI {
2983
3033
  * rows — see _keySelect, which walks this order but still stores an index.
2984
3034
  * Which is also why the arrangement is a sort key rather than a permutation
2985
3035
  * of `am.accounts`: see _doMoveAccount.
3036
+ *
3037
+ * With `accountSort: "weekly-reset"` the soonest weekly reset goes before
3038
+ * the arrangement, which then only breaks ties. Not on the reorder screen,
3039
+ * and not when `arranged` is asked for: the arrangement is what that screen
3040
+ * edits, so it must see that order.
3041
+ *
3042
+ * @param {{ arranged?: boolean }} [opts]
2986
3043
  */
2987
- _displayOrder() {
3044
+ _displayOrder({ arranged = false } = {}) {
3045
+ const byReset = !arranged && this.config?.accountSort === 'weekly-reset'
3046
+ && !(this.mode === 'select' && this.selAction === 'reorder');
3047
+ const now = Date.now();
2988
3048
  return this.am.accounts
2989
3049
  .map((/** @type {any} */ _, /** @type {number} */ i) => i)
2990
3050
  .filter(i => !isLocalUpstream(this.am.accounts[i]))
2991
3051
  .sort((/** @type {number} */ x, /** @type {number} */ y) => {
2992
3052
  const px = PROVIDER_ORDER.indexOf(providerOf(this.am.accounts[x]));
2993
3053
  const py = PROVIDER_ORDER.indexOf(providerOf(this.am.accounts[y]));
2994
- // Provider, then the arrangement: the provider is what a row IS, so a
2995
- // number the operator set never crosses it.
3054
+ // Provider, then the sort, then the arrangement: the provider is what a
3055
+ // row IS, so no sort and no number the operator set crosses it.
2996
3056
  if (px !== py) return px - py;
3057
+ if (byReset) {
3058
+ const tx = weeklyResetRank(this.am.accounts[x], now);
3059
+ const ty = weeklyResetRank(this.am.accounts[y], now);
3060
+ if (tx !== ty) return tx < ty ? -1 : 1; // Infinity - Infinity is NaN, so compare
3061
+ }
2997
3062
  const rx = listRank(this.am.accounts[x]);
2998
3063
  const ry = listRank(this.am.accounts[y]);
2999
3064
  // Infinity !== Infinity is false, so two unplaced accounts fall through
@@ -3281,9 +3346,13 @@ export class TUI {
3281
3346
  * Every account except the locally-served ones. _displayOrder already leaves
3282
3347
  * those out (they draw as conduit lines, not rows), so they hold no position
3283
3348
  * and their array slots are simply stepped over.
3349
+ *
3350
+ * Always in the arranged order, whatever `accountSort` says: a move
3351
+ * renumbers every account from this list, so a sorted list here would
3352
+ * write the sort into `displayOrder`.
3284
3353
  */
3285
3354
  _arrangeable() {
3286
- return this._displayOrder();
3355
+ return this._displayOrder({ arranged: true });
3287
3356
  }
3288
3357
 
3289
3358
  /** The rows that carry ►: the cursor, or in a mixed pool each provider's current
@@ -3624,6 +3693,11 @@ export class TUI {
3624
3693
  lines.push(row(byId('addAccount')));
3625
3694
  if (byId('removeAccount')) lines.push(row(byId('removeAccount')));
3626
3695
  if (byId('orderAccounts')) lines.push(row(byId('orderAccounts')));
3696
+ if (byId('accountSort')) {
3697
+ lines.push(row(byId('accountSort')));
3698
+ lines.push(dim(' Weekly reset lists the account whose week ends soonest first;'));
3699
+ lines.push(dim(' the arranged order breaks ties.'));
3700
+ }
3627
3701
  lines.push('');
3628
3702
  // ── Network
3629
3703
  // Drawn before the sx.org block, which returns early when sx is unavailable: