@modelprofile.com/authswitch 3.2.0 → 4.0.0

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.
Files changed (87) hide show
  1. package/dist_ts/00_commitinfo_data.js +3 -3
  2. package/dist_ts/accounts.d.ts +63 -13
  3. package/dist_ts/accounts.js +199 -31
  4. package/dist_ts/classes.accountlist.d.ts +0 -24
  5. package/dist_ts/classes.accountlist.js +2 -213
  6. package/dist_ts/classes.claudecodeharness.d.ts +61 -3
  7. package/dist_ts/classes.claudecodeharness.js +130 -14
  8. package/dist_ts/classes.claudecodelocks.d.ts +35 -0
  9. package/dist_ts/classes.claudecodelocks.js +117 -0
  10. package/dist_ts/classes.claudestatus.d.ts +17 -4
  11. package/dist_ts/classes.claudestatus.js +89 -73
  12. package/dist_ts/classes.claudetokenrefresh.d.ts +32 -0
  13. package/dist_ts/classes.claudetokenrefresh.js +77 -0
  14. package/dist_ts/classes.cli.d.ts +29 -7
  15. package/dist_ts/classes.cli.js +172 -78
  16. package/dist_ts/classes.codexharness.d.ts +6 -2
  17. package/dist_ts/classes.codexharness.js +7 -3
  18. package/dist_ts/classes.codexstatus.d.ts +5 -4
  19. package/dist_ts/classes.codexstatus.js +36 -19
  20. package/dist_ts/classes.credentialstore.d.ts +28 -2
  21. package/dist_ts/classes.credentialstore.js +41 -12
  22. package/dist_ts/classes.fileharness.d.ts +39 -19
  23. package/dist_ts/classes.fileharness.js +58 -24
  24. package/dist_ts/classes.limits.d.ts +46 -7
  25. package/dist_ts/classes.limits.js +106 -32
  26. package/dist_ts/classes.listrenderer.d.ts +17 -0
  27. package/dist_ts/classes.listrenderer.js +313 -0
  28. package/dist_ts/classes.login.d.ts +1 -1
  29. package/dist_ts/classes.login.js +1 -1
  30. package/dist_ts/classes.opencodeharness.d.ts +6 -2
  31. package/dist_ts/classes.opencodeharness.js +9 -5
  32. package/dist_ts/classes.operations.js +3 -2
  33. package/dist_ts/classes.tui.js +10 -6
  34. package/dist_ts/classes.watch.d.ts +108 -0
  35. package/dist_ts/classes.watch.js +219 -0
  36. package/dist_ts/classes.watchlock.d.ts +33 -0
  37. package/dist_ts/classes.watchlock.js +118 -0
  38. package/dist_ts/claudehttp.d.ts +39 -0
  39. package/dist_ts/claudehttp.js +83 -0
  40. package/dist_ts/cliargs.d.ts +36 -0
  41. package/dist_ts/cliargs.js +60 -0
  42. package/dist_ts/consoletable.d.ts +21 -0
  43. package/dist_ts/consoletable.js +63 -0
  44. package/dist_ts/helpers.d.ts +7 -0
  45. package/dist_ts/helpers.js +16 -1
  46. package/dist_ts/index.d.ts +3 -0
  47. package/dist_ts/index.js +4 -1
  48. package/dist_ts/interfaces.harness.d.ts +76 -15
  49. package/dist_ts/interfaces.list.d.ts +7 -1
  50. package/dist_ts/plugins.d.ts +7 -0
  51. package/dist_ts/plugins.js +6 -1
  52. package/dist_ts/ratelimit.d.ts +8 -0
  53. package/dist_ts/ratelimit.js +13 -0
  54. package/dist_ts/watchpolicy.d.ts +44 -0
  55. package/dist_ts/watchpolicy.js +82 -0
  56. package/package.json +5 -3
  57. package/readme.md +360 -106
  58. package/ts/00_commitinfo_data.ts +3 -3
  59. package/ts/accounts.ts +202 -34
  60. package/ts/classes.accountlist.ts +2 -216
  61. package/ts/classes.claudecodeharness.ts +126 -13
  62. package/ts/classes.claudecodelocks.ts +132 -0
  63. package/ts/classes.claudestatus.ts +88 -54
  64. package/ts/classes.claudetokenrefresh.ts +85 -0
  65. package/ts/classes.cli.ts +163 -63
  66. package/ts/classes.codexharness.ts +7 -3
  67. package/ts/classes.codexstatus.ts +29 -18
  68. package/ts/classes.credentialstore.ts +53 -9
  69. package/ts/classes.fileharness.ts +69 -31
  70. package/ts/classes.limits.ts +126 -33
  71. package/ts/classes.listrenderer.ts +328 -0
  72. package/ts/classes.login.ts +1 -1
  73. package/ts/classes.opencodeharness.ts +9 -5
  74. package/ts/classes.operations.ts +2 -1
  75. package/ts/classes.tui.ts +9 -5
  76. package/ts/classes.watch.ts +263 -0
  77. package/ts/classes.watchlock.ts +100 -0
  78. package/ts/claudehttp.ts +92 -0
  79. package/ts/cliargs.ts +71 -0
  80. package/ts/consoletable.ts +62 -0
  81. package/ts/helpers.ts +14 -0
  82. package/ts/index.ts +3 -0
  83. package/ts/interfaces.harness.ts +78 -6
  84. package/ts/interfaces.list.ts +7 -1
  85. package/ts/plugins.ts +9 -0
  86. package/ts/ratelimit.ts +14 -0
  87. package/ts/watchpolicy.ts +121 -0
package/readme.md CHANGED
@@ -58,9 +58,11 @@ authswitch active # which account each provider is using right n
58
58
  authswitch codex stash # save the active credential under its account email
59
59
  authswitch codex list # Codex accounts, including an unsaved active login, with live status
60
60
  authswitch codex use [email] # activate one; prompts when no email is given
61
- authswitch claude use [email] --stop # stop running Claude Code instances first
62
- authswitch claude use [email] --keep-running # switch without stopping anything
61
+ authswitch opencode use [email] --stop # stop running OpenCode instances first
62
+ authswitch opencode use [email] --keep-running # switch without stopping anything
63
63
  authswitch codex preuse <email> # send the default prompt without activating the account
64
+ authswitch watch # check usage every 2 minutes and switch when an account runs out
65
+ authswitch claude watch --threshold 90 --dry-run # report what it would switch, for one harness
64
66
  authswitch codex current # print the account currently in use
65
67
  authswitch codex drop <email> # forget a stash
66
68
  authswitch codex doctor # report credential storage and remote-control state
@@ -81,25 +83,42 @@ the management TUI and human/JSON lists. With an AGL installation that supports
81
83
  authswitch coordination, the CLI delegates credential changes to AGL. AGL owns
82
84
  stopping and restarting its OpenCode runtime. Idle changes need no additional
83
85
  restart confirmation; active work requires consent to wait for it to finish.
84
- Every operation, including `use` and `stash`, now works while OpenCode or Claude
86
+ Every operation, including `use` and `stash`, works while OpenCode or Claude
85
87
  Code is running: the credential file is written atomically, the outgoing login is
86
88
  re-saved from disk first, and a write that races a native rewrite is detected and
87
- refused rather than half-applied. Status lookups remain read-only. `preuse`
88
- remains a Codex capability.
89
+ refused rather than half-applied. A running Claude Code picks a switch up on its next
90
+ request, so it never needs a restart. Status lookups never activate, switch or clear a
91
+ login; reading a saved Claude login whose access token has expired does renew that saved
92
+ copy's own tokens, and nothing else. `preuse` remains a Codex capability.
89
93
 
90
94
  #### Running instances during a switch
91
95
 
92
- A harness that keeps running through a switch still costs something: it holds the
93
- previous login in memory and can write it back at its next OAuth token refresh.
94
- So before `use` and before `stash` without `--keep`, authswitch lists that
95
- harness's own running instances -- pid, start time and command line -- and offers
96
- to stop them:
96
+ **Claude Code picks a switch up live.** A running session re-reads its credential
97
+ file whenever the file changes, before its next request, and reloads `.claude.json`
98
+ when that changes (verified against Claude Code 2.1.273). So `use`, `stash` and a
99
+ backend activation never ask for Claude Code to be stopped and never suggest a
100
+ restart; a switch reports `Running Claude Code sessions use it from their next
101
+ request.` What authswitch must not do is write in the middle of that session's own
102
+ writes, so every Claude Code operation that reads or writes the native login runs
103
+ while holding Claude Code's own locks, taken with its own library and options: the
104
+ OAuth refresh lock (`<config dir>/.oauth_refresh.lock` and its legacy twin
105
+ `<config dir>.lock`), the credential write lock (`<config dir>/.storage-write.lock`)
106
+ and the global config lock (`.claude.json.lock`). A lock Claude Code holds is waited
107
+ for up to 15 seconds, without holding the others meanwhile, so a live session is
108
+ never blocked by a waiting switch; after that the operation fails with nothing
109
+ changed. A lock left behind by a Claude Code that died goes stale after its own
110
+ threshold and is taken over, as Claude Code itself would.
111
+
112
+ **OpenCode keeps the previous login until it restarts.** It holds that login in
113
+ memory and can write it back at its next OAuth token refresh. So before `use` and
114
+ before `stash` without `--keep`, authswitch lists OpenCode's own running instances --
115
+ pid, start time and command line -- and offers to stop them:
97
116
 
98
117
  ```
99
- 2 Claude Code process(es) are running:
100
- pid 4821 started 2026-09-16 14:02 UTC claude --session-id 9f2c
101
- pid 5533 started 2026-09-16 14:02 UTC claude agents
102
- ? Stop these 2 Claude Code process(es) first? (y/N)
118
+ 2 OpenCode process(es) are running:
119
+ pid 4821 started 2026-09-16 14:02 UTC opencode
120
+ pid 5533 started 2026-09-16 14:02 UTC opencode serve
121
+ ? Stop these 2 OpenCode process(es) first? (y/N)
103
122
  ```
104
123
 
105
124
  Answering yes sends `SIGTERM` to exactly those pids and waits up to ten seconds;
@@ -112,6 +131,10 @@ decide the same thing without a prompt:
112
131
  | `--force-stop` | the same, then `SIGKILL` the ones that ignored `SIGTERM` |
113
132
  | `--keep-running` | switch without stopping anything |
114
133
 
134
+ `--stop` and `--force-stop` also stop Claude Code's instances when you ask for it;
135
+ without them nothing about Claude Code's processes is listed or offered, and
136
+ `--keep-running` is accepted and changes nothing.
137
+
115
138
  Without a terminal and without a flag, nothing is ever signalled: the instances are
116
139
  listed, the switch proceeds, and the command says which flag would have changed that.
117
140
  `SIGKILL` is only ever sent for `--force-stop`. Only pids authswitch enumerated for
@@ -123,8 +146,8 @@ On Windows the instances are listed but not stopped, because its process list do
123
146
  not identify their owner. `stash --keep` needs none of this: it writes no native
124
147
  file and is guarded by proving the native sources unchanged around the read.
125
148
 
126
- After a switch that left instances running, the command says so explicitly, because
127
- one of them refreshing its token can put the previous account back. A backend
149
+ After an OpenCode switch that left instances running, the command says so explicitly,
150
+ because one of them refreshing its token can put the previous account back. A backend
128
151
  activation through `AuthSwitchService` gets that same caveat in its outcome, but no
129
152
  listing and no offer: a host has no terminal to consent with, and a supervisor that
130
153
  completed the mutation itself owns its own runtime's lifecycle.
@@ -206,7 +229,23 @@ and model windows.
206
229
  These endpoints require the `user:profile` scope and may reject expired or restricted
207
230
  logins. A null reset timestamp is shown as **Not scheduled**. Billing renewal and
208
231
  cancellation dates and earned reset credits are unavailable from these endpoints;
209
- authswitch does not infer them from account creation or token expiry dates.
232
+ authswitch does not infer them from account creation or token expiry dates. The
233
+ Claude Code adapter declares that, so `limits` explains it once for the provider
234
+ instead of per account.
235
+
236
+ Nothing but Claude Code refreshes a Claude login, so a saved one stops being readable
237
+ a few hours after it was saved. Reading a **saved, inactive** account's status therefore
238
+ refreshes it first, exactly as Claude Code would: when its access token expires within
239
+ five minutes, authswitch exchanges the stored refresh token at Claude's token endpoint,
240
+ keeps every other field of the login, and writes the result only while the saved record
241
+ still holds the refresh token it sent. The refresh holds the account store's lock, so a
242
+ switch started at the same time waits for it and then activates the refreshed login. The
243
+ **active** login is never refreshed -- Claude Code owns it, and two parties refreshing one
244
+ login can invalidate the grant. A refusal is reported, never worked around: `Login expired
245
+ or was rejected. Log in again with Claude Code and save it.` for a rejected grant, and a
246
+ service failure, an unreachable service or an unsupported response leave the saved login
247
+ untouched. Requests carry `User-Agent: authswitch/<version>`, and HTTP 429 is reported as
248
+ rate limiting with the service's retry time -- never as an exhausted or expired account.
210
249
 
211
250
  OpenCode and Claude saved records are separate owner-only files under
212
251
  `~/.authswitch/opencode` and `~/.authswitch/claude`. Emails are accepted as account
@@ -251,6 +290,70 @@ inference failure exits with 1, invalid arguments or unsupported harnesses with
251
290
  2, and an interrupted prompt with 130. Check `authswitch list` before repeating
252
291
  an uncertain request. `--json` remains a list-only option.
253
292
 
293
+ ### Watching usage and switching automatically
294
+
295
+ `authswitch watch` reads every account's usage on an interval and switches a harness to a
296
+ better saved account when the one in use runs out:
297
+
298
+ ```bash
299
+ authswitch watch # every harness that supports it, every 2m, at 95%
300
+ authswitch claude watch # one harness (also: authswitch watch claude)
301
+ authswitch watch --interval 5m --threshold 90 # check less often, switch earlier
302
+ authswitch watch --dry-run # report decisions, change nothing
303
+ authswitch watch --once # one check, then exit
304
+ authswitch watch --json # one JSON event per line
305
+ ```
306
+
307
+ ```
308
+ 14:02:07 Watching Claude Code, Codex every 2m; switching at 95%.
309
+ 14:02:07 Running Codex sessions keep the login they loaded; new sessions use the switched one.
310
+ 14:02:09 Claude Code alice@example.com (type:Max) (5h:17%) (w:4%) ok
311
+ 14:02:10 Codex switched bob@example.com → carol@example.com (type:Pro) (5h:3%) (w:12%) — Codex primary 96% ≥ 95%
312
+ 14:04:09 Claude Code alice@example.com (type:Max) (5h:21%) (w:4%) ok
313
+ 14:04:10 Codex carol@example.com (type:Pro) (5h:4%) (w:12%) ok
314
+ ```
315
+
316
+ Watch covers the harnesses whose adapter declares `autoSwitch`: Claude Code and Codex.
317
+ OpenCode is not watched, because most of its providers report no usage and its lifecycle
318
+ belongs to its supervisor. Naming an unsupported harness exits with 2, as does an interval
319
+ outside 1 minute to 1 day, a threshold outside 50 to 100, or any other argument the command
320
+ cannot act on. `--interval` takes seconds or a unit (`120`, `90s`, `2m`, `1h`, `1d`).
321
+
322
+ **How it decides.** Each check reads the harness's accounts and every account's usage, then
323
+ applies one rule set per harness:
324
+
325
+ - The account in use is *used up* when any of its general account windows has reached the
326
+ threshold. The watch then switches to the saved account whose account windows are all at
327
+ least ten points below the threshold, preferring the one whose longest window resets
328
+ soonest (capacity that expires first is used first), then the least used, then by name.
329
+ - When every other saved account is used up too, it switches to the one that is usable
330
+ again first, and only when that is more than one interval sooner than the account in use.
331
+ - An account limited only on a feature window (one model or surface) is traded for an
332
+ account that is below the margin on *every* window; without one it stays, because it
333
+ still serves everything else.
334
+ - A reading that is unknown, too old, rejected or rate limited never counts as used up and
335
+ never makes an account a target. The line then says so instead of saying `ok`.
336
+
337
+ **What it does and does not do.** A switch goes through the same path as `use`, including
338
+ AGL coordination when AGL manages the login, and never asks for a session of yours to be
339
+ stopped: Claude Code picks the new login up on its next request, and running Codex sessions
340
+ keep the login they loaded while new ones use the switched account (the start line says so
341
+ once). A Codex switch does stop and start Codex' own managed app-server, because `auth.json`
342
+ cannot be rewritten underneath it, so its remote-control connection drops for a moment. A
343
+ switch that is refused or fails is reported and decided again on the next check. Checks
344
+ never overlap; a check that takes longer than the interval is followed by the next one at
345
+ once. An account whose lookup a provider refused (HTTP 429) or whose lookup failed is not
346
+ asked about again until the time the provider named, or after a delay that doubles from one
347
+ interval up to half an hour, and the account's last reading stands in meanwhile while it is
348
+ less than ten minutes old. Claude's profile is read at most once an hour per account while
349
+ watching; usage is always live.
350
+
351
+ One watch runs per `AUTHSWITCH_HOME`: a second one exits with 1 and names the running pid,
352
+ and a lock left behind by a watch that died is taken over. Ctrl-C or `SIGTERM` cancels the
353
+ lookups in flight, releases the lock and exits with 0. `--once` exits with 0, or 1 when a
354
+ switch it attempted did not complete. `--json` writes one event per line
355
+ (`start`, `tick`, `switch`, `error`) with accounts, readings and reasons, and no credential.
356
+
254
357
  ### Management dashboard
255
358
 
256
359
  `authswitch --tui` opens a resizable account table with subscription, usage and
@@ -258,7 +361,7 @@ reset availability, usage bars for the lead window and the next general window,
258
361
  scrollable account details and an activity log.
259
362
  With multiple registered adapters, a harness selector switches the view; account
260
363
  selection and mutations always belong to the displayed harness. Status loads
261
- incrementally through each account's read-only API, without activating it.
364
+ incrementally through each account's status API, without activating it.
262
365
 
263
366
  | Key | Action |
264
367
  | --- | --- |
@@ -307,6 +410,35 @@ authswitch codex use
307
410
 
308
411
  The account argument also accepts the directory slug, the local part, or a unique prefix, so `authswitch codex use alice` resolves to `alice@example.com` when that is unambiguous. It is deliberately not a substring match — this argument decides which credential becomes active, and an incidental substring hit should never pick an account you did not name.
309
412
 
413
+ So you know what you switch to, every harness's switch names the account with a
414
+ badge of its type and its current usage:
415
+
416
+ ```
417
+ ? Which Claude Code account should be active?
418
+ ❯ phil@example.com (active) (type:Max) (5h:12%) (w:75%)
419
+ alice@example.com (type:Pro, unverified) (w:40%)
420
+ ...
421
+ Now active: alice@example.com (type:Pro, unverified) (w:40%)
422
+ ```
423
+
424
+ The badge appears in the account picker of `use`, in the dashboard's switch
425
+ confirmation and on the `Now active:` line that reports a completed switch (in the
426
+ dashboard, its activity log). `type` is the account type as `limits` shows it; `5h`
427
+ and `w` are the account's five-hour and weekly usage windows, rounded to whole
428
+ percent -- except that a window the provider reports below 100% never reads as the
429
+ exhausted `100%`. Feature limits never appear in it, and when several account windows share a
430
+ period without the provider picking one of them, that part is left out rather than
431
+ guessed.
432
+
433
+ Usage is read live and once per command. The picker looks up the offered
434
+ accounts, at most four at a time, before it asks; a switch named on the command line
435
+ switches first and then looks up only its target for the `Now active:` line. Whatever
436
+ has not answered after 2.5 seconds is left out and its requests are cancelled, so the
437
+ command still exits at once. Every part the provider did not report is omitted, and
438
+ with none known the account is named alone. Reading usage never delays the switch
439
+ beyond that deadline and never fails it. The dashboard reuses the status it already
440
+ shows and makes no extra lookup.
441
+
310
442
  ## What a switch actually does
311
443
 
312
444
  `use` runs as one sequence, and steps 1 to 4 stop at the first failure:
@@ -333,86 +465,159 @@ by an interrupted write.
333
465
  ## Account status
334
466
 
335
467
  `authswitch list` lists every registered harness; `authswitch codex list` limits the
336
- output to Codex. Each section includes saved accounts and an identifiable active
337
- login even if it has never been saved. `*` marks the active account, and the saved
338
- marker checks the credential itself rather than just the existence of metadata.
339
-
340
- The overview compares login state, plan provenance, lead-window usage (the
341
- provider's headline window, otherwise weekly), earned resets and that window's
342
- reset countdown. Countdowns use days, hours and minutes, such as
343
- `7d 5h 6min`;
344
- `<1min` means less than a minute remains and `due` means the reported deadline has
345
- passed, without claiming the service has refreshed the quota. All countdowns share
346
- the same snapshot time.
347
- Usage and countdown refer to the same window. When the provider picks a headline
348
- window (Claude does, for example its Fable weekly limit), that window leads.
349
- Otherwise the
350
- general account window leads, preferring weekly; if no weekly window is reported,
351
- the longest general window is shown with its actual duration. Other exhausted
352
- general windows retain their own reset warning. A general window is named by its
353
- period (`Weekly`, `5h`) unless another shown general window has the same period; a
354
- feature limit, or a general window sharing its period, is named by its label
355
- (`Fable weekly: 100% used`, `Claude weekly (weekly_oauth) exhausted`). Feature
356
- limits such as Spark and code review have a separate table and determine the
357
- summary only when the provider picks one as its headline. The TUI uses the same
358
- selection and names.
359
- Window durations come from the provider, not from the plan name or
360
- primary/secondary position; a Pro account with only a general weekly window gets
361
- no invented 5-hour limit.
362
-
363
- Email addresses identify Codex accounts throughout the overview, usage/reset
364
- schedules, earned reset expiry details, grouped credits and activity metrics,
365
- saved-login notes and availability/actions tables. Other harnesses use their
366
- account labels. When accounts of one harness share a label, every table numbers
367
- the later ones (`alice@example.com (2)`), so each account keeps its own column in
368
- the metric comparisons and reads the same everywhere. Provider facts already
369
- represented by structured fields are not repeated. Narrow terminals use compact
370
- account sections. Missing
371
- data stays Unavailable or Not reported; known zeroes remain zero. Stored plans are
372
- marked unverified. Renewal/cancellation dates are shown only when an adapter can
373
- provide live billing data. Codex uses the desktop app's account-check endpoint for
374
- automatic renewal, explicit renewal/cancellation dates and subscription expiry.
375
-
376
- Tables that list an account on several rows (the usage/reset schedule,
377
- feature-specific limits, earned reset details, and availability/actions) draw a
378
- divider between one account and the next. The divider follows the account id, not
379
- the email, so two accounts that share an address stay separate blocks, and the
380
- harness-level availability row is a block of its own. The overview and the metric
381
- comparisons show each account once and have no dividers. Each `list` section covers
382
- one harness, so no colour line is drawn. Narrow terminals separate the same blocks
383
- with a rule line.
468
+ output to Codex. Harnesses appear in the order `limits` uses, by provider name. Each
469
+ section includes saved accounts and an identifiable active login even if it has
470
+ never been saved, and draws up to four tables under the harness's name:
471
+
472
+ ```
473
+ Codex
474
+ ┌───────────────────┬──────────────────┬────────────────────┬─────────────┬───────────────┐
475
+ │ Account │ Status │ Usage │ Renewal │ Earned resets │
476
+ ├───────────────────┼──────────────────┼────────────────────┼─────────────┼───────────────┤
477
+ │ alice@example.com │ active, saved │ weekly 12% · 3d 4h │ 2026-10-08 │ 0 │
478
+ │ Pro │ 2026-09-17 13:32 │ 5h 37% · 2h 13m │ auto-renews │ │
479
+ ├───────────────────┼──────────────────┼────────────────────┼─────────────┼───────────────┤
480
+ │ bob@example.com │ saved │ usage n/a │ n/a │ n/a │
481
+ │ Plus, unverified │ 2026-09-14 11:32 │ │ │ │
482
+ └───────────────────┴──────────────────┴────────────────────┴─────────────┴───────────────┘
483
+
484
+ ┌───────────────────┬───────────────────────┬────────┬───────────┐
485
+ │ Account │ Limit type │ Used % │ Resets in │
486
+ ├───────────────────┼───────────────────────┼────────┼───────────┤
487
+ │ alice@example.com │ Codex secondary │ 12% │ 3d 4h │
488
+ │ Pro │ Codex primary │ 37% │ 2h 13m │
489
+ │ │ Code review secondary │ 100% │ 3h 59m │
490
+ └───────────────────┴───────────────────────┴────────┴───────────┘
491
+
492
+ ┌────────────────┬───────────────────┐
493
+ │ Metric │ alice@example.com │
494
+ ├────────────────┴───────────────────┤
495
+ │ Saved login │
496
+ │ remote-control │ 1 │
497
+ └────────────────┴───────────────────┘
498
+
499
+ ┌─────────────────┬─────────────────────────────────────────────────────────┐
500
+ │ Account │ Problem │
501
+ ├─────────────────┼─────────────────────────────────────────────────────────┤
502
+ │ bob@example.com │ Subscription and limits: Login expired or was rejected. │
503
+ └─────────────────┴─────────────────────────────────────────────────────────┘
504
+ ```
505
+
506
+ 1. **Accounts** -- one row per account, divided from the next. **Account** is the
507
+ label (bold when active) with the account type beneath it, as in `limits`, and,
508
+ for a harness with several credential slots, the slot (`openai · Pro`).
509
+ **Status** says whether the login is active and saved, with the local date and
510
+ time of its last save beneath; an active login without a save reads `not saved`.
511
+ **Usage** holds one compact line per headline window, `<window> <used>% · <reset
512
+ countdown>`, lead window first as described below. **Renewal** is the same cell as
513
+ in `limits`, and a harness whose provider never reports billing dates, such as Claude
514
+ Code, gets one note beneath the table instead of an unexplained `n/a` per account.
515
+ **Earned resets** appears only when an account of the harness reports earned reset
516
+ credits: the available count, then one line per reported credit.
517
+ 2. **Limits** -- the `limits` table of this harness, without its Provider and
518
+ Renewal columns, for the accounts that report windows.
519
+ 3. **Details** -- one comparison of every remaining provider fact: **Metric** and
520
+ one column per account, split into pages when the terminal is too narrow, with a
521
+ full-width row per section. Facts already shown above are left out: those that
522
+ restate a displayed structured field, and one that only repeats the account's own
523
+ label. Dated rows, such as Codex's daily token totals, are merged across accounts
524
+ and keep the newest seven days. A sentence that every account of its section
525
+ states identically, such as Claude's note that it reports no billing dates, is
526
+ printed once beneath the table. Saved-login notes (Codex remote-control pairing,
527
+ account IDs) form the **Saved login** section; a note that only names the
528
+ account's credential slot is left out, because the Accounts table names it.
529
+ 4. **Problems** -- a login changed outside authswitch first (`Active login changed
530
+ outside authswitch: expected alice@example.com, now bob@example.com (not saved) —
531
+ authswitch claude stash --keep saves it`), then every account's problems, then the
532
+ harness's own. Problems of one account that share a message are merged with their
533
+ topics (`Profile, usage:` above the message), and accounts with the same problems
534
+ share one block, whose **Account** cell names them all on one line.
535
+
536
+ Human output never prints a raw ISO timestamp: dates and times are local
537
+ (`2026-10-08`, `2026-09-17 13:32`), and a timestamp inside provider text is rewritten
538
+ the same way. `list --json` keeps every original value. A note beneath a table wraps to
539
+ the terminal, with its continuation lines under the note's prefix.
540
+
541
+ The lead window is the provider's headline window, otherwise weekly. Countdowns use
542
+ two units, such as `3d 4h`; `due` means the reported deadline has passed, without
543
+ claiming the service has refreshed the quota, and `not scheduled` means the provider
544
+ reported no reset. All countdowns share the same snapshot time. When the provider
545
+ picks a headline window (Claude does, for example its Fable weekly limit), that
546
+ window leads. Otherwise the general account windows lead, weekly first. A general
547
+ window is named by its period (`weekly`, `5h`) unless another shown general window
548
+ has the same period; a feature limit, or a general window sharing its period, is
549
+ named by its label (`Fable weekly 100% · 2d`). Feature limits such as Spark and code
550
+ review appear in the Limits table and lead the usage cell only when the provider
551
+ picks one as its headline. The TUI uses the same selection and names. Window
552
+ durations come from the provider, not from the plan name or primary/secondary
553
+ position; a Pro account with only a general weekly window gets no invented 5-hour
554
+ limit.
555
+
556
+ When accounts of one harness share a label, every table numbers the later ones
557
+ (`alice@example.com (2)`), so each account keeps its own column in the Details
558
+ comparison and reads the same everywhere. Missing data stays `n/a`; known zeroes
559
+ remain zero. Plans read as in `limits` and the switch badge (`Max`), and stored plans
560
+ are marked unverified (`Plus, unverified`). Renewal dates are shown only when an
561
+ adapter can provide live billing data. Codex uses the desktop app's account-check
562
+ endpoint for automatic renewal, explicit renewal/cancellation dates and subscription
563
+ expiry.
564
+
565
+ The layout follows the terminal width. From 120 columns every Accounts column stands
566
+ alone; from 100 the earned resets join the usage cell; from 60 the status joins the
567
+ account cell; below 60 one Status cell holds status, usage, renewal and earned resets;
568
+ below 40 every table becomes labelled lines, separated by a rule line per block. Each
569
+ `list` section covers one harness, so no colour line is drawn.
384
570
 
385
571
  ### Condensed views
386
572
 
387
- `authswitch limits` answers "how much is left, and when does it come back" in one
388
- table. It is one row per account **and** limit type, sorted by provider and then by
389
- account email with each harness and each account kept together, and a two-unit countdown (`2h 13m`, `3d 4h`, `45m`, `<1m`, `due`):
573
+ `authswitch limits` answers "how much is left, when does it come back, and when does
574
+ the subscription renew" in one table. It is one row per account **and** limit type,
575
+ sorted by provider and then by account email with each harness and each account kept
576
+ together, with a two-unit countdown (`2h 13m`, `3d 4h`, `45m`, `<1m`, `due`) and the
577
+ account's next billing event:
390
578
 
391
579
  ```
392
580
  Account limits
393
- ┌──────────────────────┬─────────────────────────┬───────────────────────┬────────┬───────────────┐
394
- │ Provider │ Account │ Limit type │ Used % │ Resets in │
395
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
396
- ┃ Claude Code │ phil@example.com │ Claude weekly │ 75% │ 4d 3h │
397
- ┃ Claude Code │ phil@example.com │ Claude five-hour │ 12% │ 1h 2m │
398
- ┃ Claude Code │ phil@example.com │ Fable weekly │ 88% │ 2d 5h │
399
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
400
- ┃ Codex │ alice@example.com │ Codex secondary │ 12% │ 3d 4h │
401
- ┃ Codex │ alice@example.com │ Codex primary │ 37% │ 2h 13m │
402
- ┃ Codex │ alice@example.com │ Code review secondary │ 100% │ 3h 59m │
403
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
404
- ┃ Codex │ bob@example.com │ n/a │ n/a │ n/a │
405
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
406
- ┃ OpenCode / anthropic │ anthropic API key 20e75 │ n/a │ n/a │ n/a │
407
- ┃ │ 1c3b707 │ │ │ │
408
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
409
- ┃ OpenCode / openai │ alice@example.com │ Codex primary │ 0% │ not scheduled │
410
- └──────────────────────┴─────────────────────────┴───────────────────────┴────────┴───────────────┘
581
+ ┌──────────────────┬───────────────────┬───────────────────┬────────┬───────────────┬─────────────┐
582
+ │ Provider │ Account │ Limit type │ Used % │ Resets in │ Renewal │
583
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
584
+ ┃ Claude Code │ phil@example.com │ Claude weekly │ 75% │ 4d 3h │ n/a │
585
+ ┃ │ Max │ Claude five-hour │ 12% │ 1h 2m │ │
586
+ ┃ │ │ Fable weekly │ 88% │ 2d 5h │ │
587
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
588
+ ┃ Codex │ alice@example.com │ Codex secondary │ 12% │ 3d 4h │ 2026-10-08 │
589
+ ┃ │ Pro │ Codex primary │ 37% │ 2h 13m │ auto-renews │
590
+ ┃ │ │ Code review secon │ 100% │ 3h 59m │ │
591
+ ┃ │ │ dary │ │ │ │
592
+ ┃ ├───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
593
+ ┃ │ bob@example.com │ n/a │ n/a │ n/a │ n/a │
594
+ ┃ │ Plus, unverified │ │ │ │ │
595
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
596
+ ┃ OpenCode / anthr │ anthropic API key │ n/a │ n/a │ n/a │ n/a │
597
+ ┃ opic │ 20e751c3b707 │ │ │ │ │
598
+ ┃ │ n/a │ │ │ │ │
599
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
600
+ ┃ OpenCode / opena │ alice@example.com │ Codex primary │ 0% │ not scheduled │ 2026-10-02 │
601
+ ┃ i │ Pro │ │ │ │ cancels │
602
+ └──────────────────┴───────────────────┴───────────────────┴────────┴───────────────┴─────────────┘
411
603
 
412
604
  n/a — Codex · bob@example.com: Subscription and limits: Login expired or was rejected.
413
- n/a — OpenCode / anthropic · anthropic API key 20e751c3b707: This provider login does not expose a supported subscription or quota API.
605
+ n/a — OpenCode / anthropic · anthropic API key 20e751c3b707: This provider login does not expose a
606
+ supported subscription or quota API.
607
+ n/a — Claude Code: renewal dates are not exposed by Claude's account API.
414
608
  ```
415
609
 
610
+ The table fits 100 columns; at that width long provider names, account labels and
611
+ limit types wrap inside their cells.
612
+
613
+ **Renewal** spans the rows of its account with two lines: the local calendar date,
614
+ and beneath it `auto-renews` when the provider says the subscription renews then, or
615
+ `cancels` when it ends then -- a scheduled cancellation, or an expiry with automatic
616
+ renewal off. Anything else reads `n/a`: the date comes only from live billing data,
617
+ never from a plan, a quota reset, account creation or token expiry. A harness whose
618
+ provider never reports billing dates, such as Claude Code, gets one footnote instead
619
+ of an unexplained `n/a` per account.
620
+
416
621
  A number is never invented. An account whose provider exposes no quota API, whose
417
622
  lookup failed, or whose login cannot be read shows `n/a` in **Used %** and
418
623
  **Resets in**, and the footnote under the table says why — one line per distinct
@@ -427,6 +632,14 @@ session and the weekly total keeps its kind in parentheses
427
632
  (`Claude weekly (weekly_oauth)`). The provider column names the harness, qualified by
428
633
  the credential slot when a harness owns several (`OpenCode / openai`).
429
634
 
635
+ Each provider and each account is named once. The **Provider** cell spans all rows of
636
+ its provider, and the **Account** cell spans the rows of its account with two lines:
637
+ the account label, and beneath it, dimmed, the **account type** — the provider's own
638
+ plan name with its first letter capitalised (`Max`, `Pro`, `Prolite`). A type read
639
+ from the stored login because the live lookup failed is marked `unverified` (`Plus, unverified`), and
640
+ an account whose provider reports no plan, such as an API-key login, shows `n/a`. A
641
+ provider row without an account has no type line.
642
+
430
643
  **Used %** is coloured by the provider's own reading when it reports one: orange for
431
644
  `warning`, red for `critical`, uncoloured for `normal`. Without a reading, `n/a` is
432
645
  orange and an exhausted window (100% or more) is red. Colour is omitted when
@@ -435,7 +648,8 @@ columns.
435
648
 
436
649
  Rows are grouped in two independent ways. A divider separates one account from the
437
650
  next, so an account's limit types read as one block; a provider row without an
438
- account is a block of its own. A line along the left edge (`┃`) marks the harness:
651
+ account is a block of its own. Inside a provider's span the divider starts at the
652
+ **Account** column, and a change of provider draws a full rule. A line along the left edge (`┃`) marks the harness:
439
653
  all its rows share one colour, including every credential slot of an OpenCode
440
654
  installation, and the next harness takes the next colour (cyan, orange, green, pink,
441
655
  blue, then again from the start). Without colour the `┃` edge still shows where the
@@ -443,8 +657,9 @@ data rows are. Groups follow harness and account ids, never labels: two accounts
443
657
  that share an email are separate blocks, ordered by account id, and two harnesses
444
658
  that share a label are ordered by harness id. A harness is placed by its first
445
659
  provider name, so no other provider sorts between its slots. Below 40 columns the
446
- table becomes labelled lines; a rule line separates the accounts there, and no
447
- colour line is drawn.
660
+ table becomes labelled lines; every row repeats its provider and its account with
661
+ the type as a second line, a rule line separates the accounts, and no colour line is
662
+ drawn.
448
663
 
449
664
  `authswitch active` answers "what am I logged in as" for every harness at once:
450
665
 
@@ -460,7 +675,8 @@ Active accounts
460
675
  ┃ OpenCode / openai │ alice@example.com │ not saved │ credential file │
461
676
  └──────────────────────┴────────────────────────────────┴────────────┴─────────────────┘
462
677
 
463
- note — OpenCode / openai · alice@example.com: This active login is not saved yet; authswitch cannot restore it after a switch.
678
+ note — OpenCode / openai · alice@example.com: This active login is not saved yet; authswitch cannot
679
+ restore it after a switch.
464
680
  ```
465
681
 
466
682
  **Source** is where the shown login comes from: `credential file` is the harness's
@@ -478,19 +694,26 @@ finds a different account in that slot, the row's source reads
478
694
  `credential file (changed)` and a note explains it:
479
695
 
480
696
  ```
481
- note — Claude Code · bob@example.test: The credential file changed since the last
482
- switch (likely a running instance refreshed the previous login). It now holds
483
- bob@example.test, which is saved. Run authswitch claude use alice@example.test again.
697
+ note — Claude Code · bob@example.test: The active login changed outside authswitch since the last
698
+ switch (for example a new login). It now holds bob@example.test, which is saved; authswitch
699
+ claude use alice@example.test switches back.
484
700
  ```
485
701
 
702
+ For a harness that does not pick up swaps live, such as OpenCode, the sentence also
703
+ names the other cause: a running instance that wrote its previous login back. An
704
+ unsaved current login adds how to keep it (`authswitch <harness> stash --keep saves
705
+ it`).
706
+
486
707
  `authswitch <harness> current` prints the same sentence. The same account with a
487
708
  rotated token is that account refreshing itself and is not reported; re-running the
488
709
  switch re-arms the check against the file's current contents.
489
710
 
490
711
  Both commands accept a harness qualifier (`authswitch claude limits`) and `--json`.
491
- Both are read-only: they never activate, save or clear a login, they need no harness
492
- stopped, and every provider lookup is bounded by the same 10-second timeout and
493
- isolated per account as `authswitch list`.
712
+ Neither changes which account is in use: they never activate, switch or clear a login and
713
+ they need no harness stopped. Like `list`, they may renew a saved Claude login's own tokens
714
+ when its access token has expired, because otherwise its usage cannot be read at all. Every
715
+ provider lookup is bounded by the same 10-second timeout and isolated per account as
716
+ `authswitch list`.
494
717
 
495
718
  ### JSON output
496
719
 
@@ -502,7 +725,12 @@ arguments to these commands exit with 2.
502
725
 
503
726
  `limits --json` emits `IAccountLimits` and `active --json` emits `IActiveAccounts`:
504
727
  the same rows as the tables, plus the machine-readable fields the tables condense —
505
- `accountId`, `slotId`, `scope`, `windowSeconds`, the ISO `resetAt` beside the human
728
+ `accountId`, `slotId`, `accountType` (`{ plan, source }` with the adapter's plan
729
+ name, such as `max` or `pro`, sanitised but not display-formatted, and `source` `live`
730
+ or `stored`; null when no type is known or the row names no account; both documents
731
+ carry it), the limits rows' `billing` (the account's reported `hasActiveSubscription`,
732
+ `autoRenew` and ISO `renewsAt`, `cancelsAt` and `expiresAt`, each omitted when not
733
+ reported; null when the provider reported none or the row names no account), `scope`, `windowSeconds`, the ISO `resetAt` beside the human
506
734
  `resetsIn`, the provider's `severity` (`normal`, `warning`, `critical`, or null when
507
735
  it gave none) and `headline` (true for the window the provider picks for a
508
736
  single-value summary), the ISO `savedAt` beside the human `savedAgo`, the per-row
@@ -563,9 +791,9 @@ The returned usage plan is not a billing status. Billing dates come from the
563
791
  separate account-check response, never from a quota reset, token expiry or stored
564
792
  plan. Automatic renewal is on only when the provider says it will renew and no
565
793
  cancellation date is scheduled. A non-renewing account without a cancellation date
566
- does not acquire an invented date. The expiry of an entitlement is displayed
567
- separately. When renewal is off, a returned renewal timestamp is labelled Renewal
568
- boundary rather than promising a future charge.
794
+ or expiry does not acquire an invented date. When renewal is off, a returned renewal
795
+ timestamp is never shown as a renewal; the Renewal cell shows the expiry as the day
796
+ the subscription ends. JSON keeps every returned field.
569
797
 
570
798
  The billing request uses a verified Chromium-compatible User-Agent because the
571
799
  service challenged the generic authswitch User-Agent in controlled comparisons.
@@ -595,7 +823,19 @@ process.exitCode = await cli.run(process.argv.slice(2));
595
823
 
596
824
  Each adapter owns discovery, identity matching, save verification, credential
597
825
  storage, switching and process lifecycle, live status APIs, and diagnostics.
598
- `readState()` and `readAccountStatus()` must be read-only. `switchAccount()` must
826
+ `readState()` and `readAccountStatus()` must never activate, switch or clear a login.
827
+ `readAccountStatus()` may renew the tokens of the saved copy it is reading, when the provider
828
+ gives it no other way to answer (the Claude Code adapter does, under the store's lock, for
829
+ saved and inactive logins only); it must never touch the active login's credential. `readAccountStatus(accountId,
830
+ options)` receives an optional `IHarnessStatusOptions` whose `signal` cancels the
831
+ lookup's requests; authswitch aborts it when a status is no longer wanted, such as a
832
+ switch badge past its deadline or a closed dashboard, so an adapter should pass it to
833
+ every request it makes. Its optional `session` belongs to a caller that reads the same
834
+ accounts repeatedly, such as `authswitch watch`: `session.reuse(key, maxAgeMs, lookup)`
835
+ returns the value that key resolved to less than `maxAgeMs` ago, so an adapter can stop
836
+ repeating a lookup whose answer rarely changes (the Claude adapter reuses its profile for
837
+ an hour). Keys are the adapter's own and carry no secret; a rejected lookup is not
838
+ remembered, and outside a session every lookup is live. `switchAccount()` must
599
839
  preserve and verify the outgoing credential without relying on the interactive
600
840
  save prompt. `removeAccount()` removes only the saved copy. Status facts carry
601
841
  provider-specific labels and units; unsupported capabilities are explicit. Adapter
@@ -629,7 +869,7 @@ count; if it fails, the limits response can still supply availability.
629
869
 
630
870
  Each usage window may set `scope: 'account' | 'feature'` (default `account`).
631
871
  Adapters mark model-specific or feature-specific quotas as `feature`; those quotas
632
- appear in the feature tables, JSON and details and lead the account summary only
872
+ appear in the Limits table, JSON and details and lead the account summary only
633
873
  when the provider marks one as its headline (see below). Weekly windows are identified by `durationSeconds: 604800`;
634
874
  neither window labels nor plan names are parsed to infer limits.
635
875
 
@@ -651,7 +891,21 @@ Facts may supply a `section` label for grouped comparison and a `summaryKey`
651
891
  (`subscription`, `billing`, `usageWindows`, `resets`, or `resetDetails`) when the
652
892
  fact duplicates a structured field. The list hides such a fact only if that field
653
893
  is displayed; JSON retains every fact. Adapters without this metadata continue to
654
- display their facts in Additional information.
894
+ display their facts in Additional information. A fact label ending in an ISO date
895
+ (`Tokens 2026-09-15`) marks a daily series, which the list merges across accounts.
896
+
897
+ Three optional adapter declarations shape the shared views. `liveSwap: true` says that
898
+ running instances pick up a credential swap on their next request: the CLI and TUI
899
+ then offer no stop before a switch (an explicit `--stop` or `--force-stop` is still
900
+ honoured through `processes`), outcomes carry no restart advice, and the credential
901
+ drift sentence names only a new login as the cause. `renewalUnavailableReason` is a
902
+ lower-case clause for a provider that never reports renewal dates; `list` and `limits`
903
+ print it once as `<label>: <reason>`. `autoSwitch: true` says that every saved account's
904
+ usage can be read and that a switch needs no session of the user's stopped, which is what
905
+ `authswitch watch` requires before it switches a harness by itself; a helper process the
906
+ adapter manages itself may still be restarted by its own switch, as Codex' app-server is. The built-in Claude Code adapter declares
907
+ all three; Codex declares `autoSwitch: true` with `liveSwap: false`, and OpenCode declares
908
+ neither.
655
909
 
656
910
  With multiple adapters, the guide first asks which harness to manage and provides
657
911
  an action to choose another. Unqualified mutation commands prompt for a harness;
@@ -678,7 +932,7 @@ Account identity comes from the `id_token` inside the credential — the email c
678
932
 
679
933
  **Other Codex clients keep running.** Stopping the managed app-server does not stop an editor extension that spawned its own. Close it, or expect it to keep using the credential it already loaded.
680
934
 
681
- **A running OpenCode or Claude Code can undo a switch.** It holds the previous login in memory and rewrites the credential file at its next token refresh. Authswitch lists those instances, offers to stop them, warns when any keep running, and detects the overwrite afterwards — but it cannot prevent it while they run. Restart them after a switch.
935
+ **A running OpenCode can undo a switch.** It holds the previous login in memory and rewrites the credential file at its next token refresh. Authswitch lists those instances, offers to stop them, warns when any keep running, and detects the overwrite afterwards — but it cannot prevent it while they run. Restart them after a switch. A running Claude Code picks the switch up instead; its guarantee rests on the behaviour and lock protocol verified against Claude Code 2.1.273, and `authswitch claude active` still reports a login that changed outside authswitch.
682
936
 
683
937
  **The Codex stash is keyed by email.** Two Codex workspaces with the same email
684
938
  cannot both be saved under that key. Their account IDs are distinguished during