@rikcodes/teamclaude 1.1.19-rik.2 → 1.1.19-rik.3

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/README.md CHANGED
@@ -24,9 +24,9 @@
24
24
  > Already have upstream installed globally? Run `npm uninstall -g @karpeleslab/teamclaude` first —
25
25
  > both packages provide the `teamclaude` command.
26
26
  >
27
- > Branch: `rik/soonest-weekly-pool`. Everything else matches upstream.
27
+ > Branch: `rik/main`. Everything else matches upstream.
28
28
 
29
- [![CI](https://github.com/rikbrown/teamclaude/actions/workflows/ci.yml/badge.svg?branch=rik/soonest-weekly-pool)](https://github.com/rikbrown/teamclaude/actions/workflows/ci.yml)
29
+ [![CI](https://github.com/rikbrown/teamclaude/actions/workflows/ci.yml/badge.svg?branch=rik/main)](https://github.com/rikbrown/teamclaude/actions/workflows/ci.yml)
30
30
  [![npm version](https://img.shields.io/npm/v/@rikcodes/teamclaude.svg)](https://www.npmjs.com/package/@rikcodes/teamclaude)
31
31
  [![node](https://img.shields.io/node/v/@rikcodes/teamclaude.svg)](https://nodejs.org)
32
32
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
@@ -190,12 +190,12 @@ Versions are `<upstream base>-rik.<n>`, e.g. `1.1.19-rik.1`. The self-updater or
190
190
 
191
191
  1. Rebase onto the upstream release you want as the base, if any.
192
192
  2. Bump `version` in `package.json` and commit.
193
- 3. Push to `rik/soonest-weekly-pool` — the Publish workflow runs the tests, publishes to npm, and cuts a GitHub release.
193
+ 3. Push to `rik/main` — the Publish workflow runs the tests, publishes to npm, and cuts a GitHub release.
194
194
 
195
195
  The workflow authenticates with npm Trusted Publishing (OIDC), which needs a one-time setup on npmjs.com: `@rikcodes/teamclaude` → Settings → Trusted Publisher → GitHub Actions, owner `rikbrown`, repo `teamclaude`, workflow `publish.yml`. Until that exists, publish by hand:
196
196
 
197
197
  ```bash
198
- pnpm publish --publish-branch rik/soonest-weekly-pool --tag latest --otp=<code>
198
+ pnpm publish --publish-branch rik/main --tag latest --otp=<code>
199
199
  ```
200
200
 
201
201
  A prerelease version always needs an explicit `--tag`, and `latest` is the tag the self-updater reads.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rikcodes/teamclaude",
3
- "version": "1.1.19-rik.2",
3
+ "version": "1.1.19-rik.3",
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/provider.js CHANGED
@@ -199,3 +199,23 @@ export function upstreamFor(account, configuredUpstream) {
199
199
  export function rewritesBody(account) {
200
200
  return PROVIDERS[providerOf(account)].rewritesBody;
201
201
  }
202
+
203
+ /** Whether `account` is served by a process on this machine rather than by a
204
+ * vendor endpoint — typically a local translating proxy in front of another
205
+ * backend.
206
+ *
207
+ * Keyed on the upstream resolving to loopback. Pairing the account against a
208
+ * declared local process does not generalise: such a declaration carries a
209
+ * COMMAND rather than a port, and the port sits inside its argv, where every
210
+ * program spells it differently. A loopback upstream says the same thing
211
+ * directly, and says it for a hand-started process too. A remote third-party
212
+ * backend (DeepSeek, GLM) keeps a public host and is not caught.
213
+ */
214
+ export function isLocalUpstream(account) {
215
+ if (!account?.upstream) return false;
216
+ let hostname;
217
+ try { hostname = new URL(account.upstream).hostname; }
218
+ catch { return false; } // not a URL we can judge — treat it as a normal account
219
+ const host = hostname.replace(/^\[|\]$/g, '').toLowerCase(); // URL brackets IPv6
220
+ return host === 'localhost' || host === '::1' || /^127\./.test(host);
221
+ }
package/src/tui.js CHANGED
@@ -15,6 +15,7 @@ import { resolveMaxUsage } from './model.js';
15
15
  import { formatProjection } from './quota-projection.js';
16
16
  import { parseProxyUrl, proxyToUrl, describeProxy, describeSelfProxy, resolveUpstreamProxy, setUpstreamProxy, getUpstreamProxy } from './upstream-proxy.js';
17
17
  import { sanitizeText, safeLine } from './safe-text.js';
18
+ import { isLocalUpstream } from './provider.js';
18
19
 
19
20
  // ── ANSI helpers ─────────────────────────────────────────────
20
21
 
@@ -757,7 +758,7 @@ export class TUI {
757
758
  label: 'Remove account',
758
759
  hint: 'Enter to pick',
759
760
  value: () => dim('—'),
760
- enter: () => { this.mode = 'select'; this.selAction = 'remove'; this.selIdx = 0; this.selReturn = 'settings'; },
761
+ enter: () => { this.mode = 'select'; this.selAction = 'remove'; this.selIdx = this._displayOrder()[0] ?? 0; this.selReturn = 'settings'; },
761
762
  });
762
763
  }
763
764
 
@@ -910,9 +911,13 @@ export class TUI {
910
911
  }
911
912
 
912
913
  _keySelect(k) {
913
- const len = this.am.accounts.length;
914
- if (k === 'up' || k === 'k') this.selIdx = Math.max(0, this.selIdx - 1);
915
- else if (k === 'down' || k === 'j') this.selIdx = Math.min(len - 1, this.selIdx + 1);
914
+ // Step through the rows AS DRAWN (_displayOrder), while selIdx itself stays
915
+ // a manager index — everything it feeds (switch, toggle, remove, route pins)
916
+ // addresses an account by that index, not by its position on screen.
917
+ const order = this._displayOrder();
918
+ const pos = order.indexOf(this.selIdx);
919
+ if (k === 'up' || k === 'k') this.selIdx = order[Math.max(0, pos - 1)] ?? this.selIdx;
920
+ else if (k === 'down' || k === 'j') this.selIdx = order[Math.min(order.length - 1, pos + 1)] ?? this.selIdx;
916
921
  // Tab / ←→ (switch only): cycle which route the pick applies to. null = the
917
922
  // global default account; each getRoutes() entry = a per-route manual pin.
918
923
  // ↑↓ move within the account list, so ←→ are free to move across targets.
@@ -1504,7 +1509,7 @@ export class TUI {
1504
1509
  fable: anyFable ? this.am.previewRouteIndex('claude-fable-5') : null,
1505
1510
  sonnet: anySonnet ? this.am.previewRouteIndex('claude-sonnet-4-6') : null,
1506
1511
  };
1507
- for (let i = 0; i < this.am.accounts.length; i++) {
1512
+ for (const i of this._displayOrder()) {
1508
1513
  const b = budgets.get(categoryOf(this.am.accounts[i]));
1509
1514
  lines.push(this._renderAcct(i, b.bw, b.showBoth, routes, genRoutes, familyTarget, b.showFamily, nameW));
1510
1515
  }
@@ -1559,6 +1564,29 @@ export class TUI {
1559
1564
  this._paint(buf, force);
1560
1565
  }
1561
1566
 
1567
+ /** Manager indices in the order the rows are drawn: accounts served by a
1568
+ * local process last, every other account left where it is.
1569
+ *
1570
+ * A local backend (a translating proxy in front of another vendor, say) is
1571
+ * infrastructure rather than a seat to rotate between, so it reads as noise
1572
+ * wedged among the accounts that do rotate. Config order cannot keep it out
1573
+ * of the way on its own, because a newly added account is appended AFTER it
1574
+ * and puts it back in the middle.
1575
+ *
1576
+ * Display only. `selIdx`, `currentIndex`, session pins and route entries all
1577
+ * stay manager indices, so nothing about selection or routing moves with the
1578
+ * rows — see _keySelect, which walks this order but still stores an index.
1579
+ */
1580
+ _displayOrder() {
1581
+ return this.am.accounts
1582
+ .map((_, i) => i)
1583
+ .sort((x, y) => {
1584
+ const sx = isLocalUpstream(this.am.accounts[x]) ? 1 : 0;
1585
+ const sy = isLocalUpstream(this.am.accounts[y]) ? 1 : 0;
1586
+ return sx - sy || x - y; // ties keep list order, so the sort is stable
1587
+ });
1588
+ }
1589
+
1562
1590
  _renderAcct(idx, bw, showBoth, routes = this.am.getRoutes(), genRoutes = routes.filter(r => routeFamily(r) === null), familyTarget = {}, showFamily = true, nameW = NAME_MIN) {
1563
1591
  const a = this.am.accounts[idx];
1564
1592
  const isCur = idx === this.am.currentIndex;