Repository navigation
Org-aware account removal + per-account rotation priority - #17
Closed
brendandebeasi wants to merge 21 commits into
Closed
brendandebeasi wants to merge 21 commits into
brendandebeasi wants to merge 21 commits into
Conversation
Extracts the `formatReset` helper into `src/index.js` and updates the
`teamclaude status` command to display time-to-reset (e.g., "resets in 2h30m")
next to the usage percentages for both unified and standard quotas.
Add a computed `remaining` summary to AccountManager.getStatus() and surface it in `teamclaude status`: session/weekly % left for Claude Max accounts, absolute tokens/requests left (with %) for API-key accounts, instead of used %.
- --headless flag runs the server without the interactive TUI (for systemd / backgrounding) - POST /teamclaude/reload re-syncs accounts from config on the running server; login/import/remove now ping it so changes apply with no restart; SIGHUP does the same (systemd ExecReload) - syncAccountsFromDisk now removes accounts deleted from config too (was add-only), keeping the in-memory config and AccountManager in lockstep
Quota utilization + reset times are only returned in /v1/messages response headers, so idle accounts show no quota until they're rotated to. Add a prober that periodically (config.pokeIntervalMs, default 10m; 0=off) sends a minimal 1-token request to each stale/idle account and feeds the rate-limit headers into updateQuota (without counting it as real usage). For Claude Max (OAuth) accounts the probe must mimic Claude Code, so the prober learns the request shape (beta header + system prompt + model) from successful live traffic and replays it. Each probe is bounded by a 20s timeout.
Add a computed `remaining` summary to AccountManager.getStatus() and surface it in `teamclaude status`: session/weekly % left for Claude Max accounts, absolute tokens/requests left (with %) for API-key accounts, instead of used %.
Headless mode, reload API, and atomic account add/remove
Background quota prober for idle accounts
The reload diff in syncAccountsFromDisk decided removals with an existence check, so when two config entries shared a name (e.g. one email across multiple orgs) removing one left the survivor's name satisfying the check for the removed in-memory account — the running server never dropped it and it lingered in `teamclaude status`. Reconcile by count instead: each disk entry claims at most one in-memory account (UUID match preferred, then name); anything left unclaimed is removed.
…ware Fix account removal not propagating on reload for same-named accounts
The same email/person can belong to multiple Anthropic orgs, each with its own OAuth token and quota. Identity was keyed on accountUuid (the person) + name (the email), so a second org overwrote the first on login, dedup collapsed them, and rotated-token persistence/removals matched the wrong entry. Introduce src/identity.js with an org-aware sameIdentity(): same accountUuid requires matching org (orgUuid, falling back to orgName), while a still-unknown org on either side is treated as the same account so a profiled login backfills a legacy entry instead of duplicating it. Capture organization.uuid in fetchProfile, carry orgUuid/orgName on accounts (and in status), and route every match site (upsert dedup, findConfigAccount token persistence, reload sync pairing, accounts dedup, TUI import) through sameIdentity. Display names stay the plain email until a second org collides, then both become "email (orgName)".
Support multiple orgs per email (org-aware identity)
A stale/rotated OAuth token left an account stuck in status 'error' with nothing to retry it: the prober skipped errored accounts and a successful refresh never cleared the flag. Now ensureTokenFresh flips 'error'→'active' on a successful refresh, and the prober force-refreshes errored OAuth accounts each tick (clearing the error when the refresh succeeds, leaving it for the next tick when the refresh token is genuinely dead). The underlying cross-contamination cause was addressed by the org-aware token persistence in the prior change.
Recover accounts from the error state
A freshly added/logged-in account had no quota data until the next poke
interval (up to 10 min), and status gave no hint of when quota was last seen.
Now a reload that adds accounts kicks an immediate prober pass, and status
reports each account's quota freshness ("updated 12s ago" / "no data yet")
plus its org. API-key accounts populate immediately; OAuth still waits for a
request template (existing prober constraint), but the freshness line makes
that state visible.
Probe quota on account add + show quota freshness in status
The prober could only learn OAuth quota by replaying a Claude Code-shaped /v1/messages request, which required first learning a template from live traffic — so a freshly added account showed no quota until real traffic flowed. Use Anthropic's GET /api/oauth/usage endpoint (anthropic-beta: oauth-2025-04-20) instead: it returns five_hour/seven_day utilization + reset directly, works with no template and no message spend, and populates quota the moment an account is added (probe-on-add now actually fills it in). API-key accounts keep the /v1/messages header probe. login/import also print the new account's current quota immediately so you can confirm it works.
Read OAuth quota from /api/oauth/usage (cold, no spend)
With multi-org support, one email can have several accounts (named "email (orgName)"), but `remove` matched only the exact name — so `remove <email>` missed them or silently dropped the first. Now it matches the exact name OR the bare email, errors on an ambiguous email (listing each org) instead of guessing, and takes --org to disambiguate.
Make remove org-aware for same-email accounts
Accounts gain an optional `priority` (lower = used first, default 0). The selector now always picks the highest-priority available account, falling back down the order when one is exhausted/throttled and switching back up the moment a higher-priority account recovers. New `teamclaude priority <account> <n|--first|--last> [--org]` sets it (reusing remove's name/email/--org matching), priority is shown in status and propagated to the running server on reload. Default 0 preserves existing config-order behavior.
Add per-account rotation priority
This was referenced May 28, 2026
MagicalTux
added a commit
that referenced
this pull request
Jun 18, 2026
One Anthropic account (email/accountUuid) can hold multiple subscriptions across different organizations — e.g. a corporate Pro org and a personal Max org. The old identity model keyed only on accountUuid (with name as fallback), so adding the second org overwrote the first (#6) and --name couldn't help because the UUID check won (#7). Identity is now (accountUuid + organization). Re-implemented and reviewed against master rather than cherry-picking the closed fork PRs (#15/#17). - src/identity.js (new): orgKey / sameIdentity / matchAccounts / emailOf, the single source of truth for identity and lookups. sameIdentity treats an unknown org on either side as a match so a freshly-profiled login backfills a legacy entry instead of duplicating it. - fetchProfile: extract organization.uuid; accounts persist orgUuid/orgName. - All dedup/sync/find match sites switched to sameIdentity; syncAccountsFromDisk uses a greedy 1:1 claim so multiple same-person/different-org entries pair correctly. accounts command dedups by (accountUuid, org) and backfills org. - Name disambiguation: one org -> "email"; multiple -> "email (Org)", renaming the existing collider too. Names stay unique (they are the user-facing key). - Org-aware remove + api: resolve by name or email, --org <name|uuid> to disambiguate; lists candidates when ambiguous. - Per-account rotation priority: `priority` field (lower = preferred, default 0) as the primary sort key in _selectNext with the existing weekly heuristic as tiebreaker; new `teamclaude priority` command (--first/--last); session-reset switching won't demote to a worse priority. - Tests: identity/matchAccounts/emailOf and priority rotation (22 passing). - Docs: README, config.example.json, help text. Out of scope (future PRs from the #18 stack): #14 headless/reload API/prober, #16 usage-endpoint quota. #13 (429 account-failover) intentionally not taken — the throttle is IP-based, so the merged #25 back-off is the correct behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
euisan
added a commit
to euisan/teamclaude
that referenced
this pull request
Oct 1, 2026
* feat(tui): fold account rows below 70 columns so usage bars wrap On a narrow terminal (a phone over a remote session, say) an account row could not hold its name, status and quota bars on one line. The list dropped every bar past the first and shrank that one to a cell or two, so the weekly and family windows vanished below 70 columns and the session bar became unreadable near 40. Below LIST_MIN a full-width list now folds each account into a heading (marker, route cells, name, type, status) and one line per quota bar, with the trailing tags wrapped a whole tag at a time beneath them. Each folded bar takes the row less a 2-column right margin (COMPACT_BAR_RIGHT_MARGIN), so it never touches the screen edge. The heading gives the name every column the rest leaves, measured in display columns, so a CJK name is cut where it reaches the type column. The smallest drawable terminal drops from 40x8 to 30x8 so the folded layout is reachable at phone widths. This ports teamcodex KarpelesLab#21 (7296e40, compact layout below 70 columns) and KarpelesLab#23 (0a34280, compact bar right margin), adapted to teamclaude's budgeted _listLayout: the layout flags `narrow`, _renderRow passes the width, and _renderAcct returns lines instead of one string. Split panes are never narrow. teamcodex KarpelesLab#12/KarpelesLab#17/KarpelesLab#18/KarpelesLab#19 are already in 1.1.22 and were not needed. At 70 columns and wider the frame is byte-identical to 1.1.22: the trailing tags moved into a list but are appended in the same order with the same separator. test/fixtures/tui-wide-frames.json holds frames recorded from the unmodified renderer, and test/tui-narrow-bars.test.js pins both sides. docs/teamcodex-port-plan.md classifies all 34 teamcodex commits for later porting; docs/evidence/ holds the real TUI.start() paint at 30-80 columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(tui): use a neutral double-width name in the narrow-layout fixtures The width tests need an account name of East Asian Wide characters, but the one they used read like a real person's name and address. It is now '가나다라마바사-wide@example.test': seven Hangul syllables (fourteen columns) then ASCII, which still exercises the cases the tests care about. - At 40 columns the name is cut exactly where it reaches the type column. - At 30 columns a fifth syllable would straddle the edge, so it is dropped whole and its leftover column padded (a case the old name did not hit). - At 69 columns the whole name fits. The wide-frame fixture was re-recorded from the unmodified 1.1.22 renderer with the new name. The pty capture in docs/evidence was regenerated from the real TUI.start() paint. The comments that named the data Korean now describe it by width. Mutating the folded heading to measure characters instead of display columns still fails "the folded heading spends what the type and status leave on the name, in display columns". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Euisan Kim <git@san.kim> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
euisan
added a commit
to euisan/teamclaude
that referenced
this pull request
Oct 1, 2026
* feat(tui): fold account rows below 70 columns so usage bars wrap On a narrow terminal (a phone over a remote session, say) an account row could not hold its name, status and quota bars on one line. The list dropped every bar past the first and shrank that one to a cell or two, so the weekly and family windows vanished below 70 columns and the session bar became unreadable near 40. Below LIST_MIN a full-width list now folds each account into a heading (marker, route cells, name, type, status) and one line per quota bar, with the trailing tags wrapped a whole tag at a time beneath them. Each folded bar takes the row less a 2-column right margin (COMPACT_BAR_RIGHT_MARGIN), so it never touches the screen edge. The heading gives the name every column the rest leaves, measured in display columns, so a CJK name is cut where it reaches the type column. The smallest drawable terminal drops from 40x8 to 30x8 so the folded layout is reachable at phone widths. This ports teamcodex KarpelesLab#21 (7296e40, compact layout below 70 columns) and KarpelesLab#23 (0a34280, compact bar right margin), adapted to teamclaude's budgeted _listLayout: the layout flags `narrow`, _renderRow passes the width, and _renderAcct returns lines instead of one string. Split panes are never narrow. teamcodex KarpelesLab#12/KarpelesLab#17/KarpelesLab#18/KarpelesLab#19 are already in 1.1.22 and were not needed. At 70 columns and wider the frame is byte-identical to 1.1.22: the trailing tags moved into a list but are appended in the same order with the same separator. test/fixtures/tui-wide-frames.json holds frames recorded from the unmodified renderer, and test/tui-narrow-bars.test.js pins both sides. docs/teamcodex-port-plan.md classifies all 34 teamcodex commits for later porting; docs/evidence/ holds the real TUI.start() paint at 30-80 columns. * test(tui): use a neutral double-width name in the narrow-layout fixtures The width tests need an account name of East Asian Wide characters, but the one they used read like a real person's name and address. It is now '가나다라마바사-wide@example.test': seven Hangul syllables (fourteen columns) then ASCII, which still exercises the cases the tests care about. - At 40 columns the name is cut exactly where it reaches the type column. - At 30 columns a fifth syllable would straddle the edge, so it is dropped whole and its leftover column padded (a case the old name did not hit). - At 69 columns the whole name fits. The wide-frame fixture was re-recorded from the unmodified 1.1.22 renderer with the new name. The pty capture in docs/evidence was regenerated from the real TUI.start() paint. The comments that named the data Korean now describe it by width. Mutating the folded heading to measure characters instead of display columns still fails "the folded heading spends what the type and status leave on the name, in display columns". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
euisan
added a commit
to euisan/teamclaude
that referenced
this pull request
Oct 5, 2026
* feat(tui): fold account rows below 70 columns so usage bars wrap On a narrow terminal (a phone over a remote session, say) an account row could not hold its name, status and quota bars on one line. The list dropped every bar past the first and shrank that one to a cell or two, so the weekly and family windows vanished below 70 columns and the session bar became unreadable near 40. Below LIST_MIN a full-width list now folds each account into a heading (marker, route cells, name, type, status) and one line per quota bar, with the trailing tags wrapped a whole tag at a time beneath them. Each folded bar takes the row less a 2-column right margin (COMPACT_BAR_RIGHT_MARGIN), so it never touches the screen edge. The heading gives the name every column the rest leaves, measured in display columns, so a CJK name is cut where it reaches the type column. The smallest drawable terminal drops from 40x8 to 30x8 so the folded layout is reachable at phone widths. This ports teamcodex KarpelesLab#21 (7296e40, compact layout below 70 columns) and KarpelesLab#23 (0a34280, compact bar right margin), adapted to teamclaude's budgeted _listLayout: the layout flags `narrow`, _renderRow passes the width, and _renderAcct returns lines instead of one string. Split panes are never narrow. teamcodex KarpelesLab#12/KarpelesLab#17/KarpelesLab#18/KarpelesLab#19 are already in 1.1.22 and were not needed. At 70 columns and wider the frame is byte-identical to 1.1.22: the trailing tags moved into a list but are appended in the same order with the same separator. test/fixtures/tui-wide-frames.json holds frames recorded from the unmodified renderer, and test/tui-narrow-bars.test.js pins both sides. docs/teamcodex-port-plan.md classifies all 34 teamcodex commits for later porting; docs/evidence/ holds the real TUI.start() paint at 30-80 columns. * test(tui): use a neutral double-width name in the narrow-layout fixtures The width tests need an account name of East Asian Wide characters, but the one they used read like a real person's name and address. It is now '가나다라마바사-wide@example.test': seven Hangul syllables (fourteen columns) then ASCII, which still exercises the cases the tests care about. - At 40 columns the name is cut exactly where it reaches the type column. - At 30 columns a fifth syllable would straddle the edge, so it is dropped whole and its leftover column padded (a case the old name did not hit). - At 69 columns the whole name fits. The wide-frame fixture was re-recorded from the unmodified 1.1.22 renderer with the new name. The pty capture in docs/evidence was regenerated from the real TUI.start() paint. The comments that named the data Korean now describe it by width. Mutating the folded heading to measure characters instead of display columns still fails "the folded heading spends what the type and status leave on the name, in display columns". Reapplied on 1.1.23: tui-wide-frames.json re-recorded from the unmodified 1.1.23 renderer (percentage off by default since KarpelesLab#514); renderFrame takes a config, and the folded-label test checks percentage on and off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack position: 4 of 4 (top of stack). Builds on #14, #15, and #16 (merge those first). The diff shown here includes their commits until they merge; the changes unique to this PR are below.
What this adds (net-new vs #16)
remove— theremovecommand targets the correct account when several share an email across orgs (index.js).account-manager.js,index.js).Why
Completes org-aware management (removal matching the org-aware identity from #15) and gives operators control over rotation order instead of relying on implicit ordering.
Test plan
removedeletes the intended org's account when emails collide.