@modelprofile.com/authswitch 3.3.0 → 5.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 +52 -15
  3. package/dist_ts/accounts.js +110 -27
  4. package/dist_ts/classes.accountlist.d.ts +0 -25
  5. package/dist_ts/classes.accountlist.js +2 -216
  6. package/dist_ts/classes.claudecodeharness.d.ts +81 -2
  7. package/dist_ts/classes.claudecodeharness.js +208 -14
  8. package/dist_ts/classes.claudecodelocks.d.ts +38 -0
  9. package/dist_ts/classes.claudecodelocks.js +118 -0
  10. package/dist_ts/classes.claudestatus.d.ts +15 -2
  11. package/dist_ts/classes.claudestatus.js +100 -83
  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 +14 -7
  15. package/dist_ts/classes.cli.js +126 -66
  16. package/dist_ts/classes.codexharness.d.ts +4 -0
  17. package/dist_ts/classes.codexharness.js +5 -1
  18. package/dist_ts/classes.codexstatus.d.ts +7 -2
  19. package/dist_ts/classes.codexstatus.js +49 -21
  20. package/dist_ts/classes.credentialstore.d.ts +43 -2
  21. package/dist_ts/classes.credentialstore.js +60 -13
  22. package/dist_ts/classes.fileharness.d.ts +45 -21
  23. package/dist_ts/classes.fileharness.js +64 -26
  24. package/dist_ts/classes.limits.d.ts +46 -7
  25. package/dist_ts/classes.limits.js +94 -36
  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 +4 -0
  31. package/dist_ts/classes.opencodeharness.js +8 -4
  32. package/dist_ts/classes.operations.js +3 -2
  33. package/dist_ts/classes.tui.js +4 -3
  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 +60 -11
  49. package/dist_ts/interfaces.list.d.ts +3 -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 +349 -115
  58. package/ts/00_commitinfo_data.ts +3 -3
  59. package/ts/accounts.ts +122 -35
  60. package/ts/classes.accountlist.ts +2 -219
  61. package/ts/classes.claudecodeharness.ts +199 -13
  62. package/ts/classes.claudecodelocks.ts +133 -0
  63. package/ts/classes.claudestatus.ts +100 -65
  64. package/ts/classes.claudetokenrefresh.ts +85 -0
  65. package/ts/classes.cli.ts +114 -54
  66. package/ts/classes.codexharness.ts +4 -0
  67. package/ts/classes.codexstatus.ts +40 -18
  68. package/ts/classes.credentialstore.ts +78 -10
  69. package/ts/classes.fileharness.ts +76 -34
  70. package/ts/classes.limits.ts +110 -36
  71. package/ts/classes.listrenderer.ts +328 -0
  72. package/ts/classes.login.ts +1 -1
  73. package/ts/classes.opencodeharness.ts +7 -3
  74. package/ts/classes.operations.ts +2 -1
  75. package/ts/classes.tui.ts +3 -2
  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 +60 -5
  84. package/ts/interfaces.list.ts +3 -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,43 @@ 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 -- a rejected
245
+ grant says so and says what fixes it (below), and a service failure, an unreachable service
246
+ or an unsupported response leave the saved login untouched. Requests carry `User-Agent:
247
+ authswitch/<version>`, and HTTP 429 is reported as rate limiting with the service's retry
248
+ time -- never as an exhausted or expired account.
249
+
250
+ Claude Code also rotates the refresh token of the login it **is** running on, so reading the
251
+ accounts (`list`, `limits`, `active`, `current`, `watch`, the dashboard) mirrors that login
252
+ into its own saved record whenever the two differ. Without it a login replaced outside
253
+ authswitch -- a `/login` in Claude Code, a new device login -- left the stash holding the
254
+ token from before the rotation, which the sign-in service can refuse: it did for two saved
255
+ logins on 2026-09-18. The record keeps its account, its identity and its save time, Claude
256
+ Code's own files are only ever read, and a login with no saved record of its own is never
257
+ saved implicitly; `stash` is what saves a login. An installation whose native login
258
+ authswitch cannot read mirrors nothing, and a read waits at most a second for a lock a
259
+ switch or Claude Code holds before leaving the saved copy to the next read.
260
+
261
+ A grant the sign-in service refused is remembered in the saved record itself, as a hash of
262
+ the refresh token -- never the token -- so a new authswitch run, a restarted watch or a
263
+ second dashboard does not replay a dead login. Saving that login again clears it, as does
264
+ any refresh that replaces the saved credential. Such a login reports `Login expired or was
265
+ rejected. Log in again with Claude Code and save it. Claude Code rotates a login's refresh
266
+ token as it uses it, and the service can refuse the older one a saved copy still holds once
267
+ that login has been replaced outside authswitch; authswitch claude stash --keep saves the
268
+ new one.`
210
269
 
211
270
  OpenCode and Claude saved records are separate owner-only files under
212
271
  `~/.authswitch/opencode` and `~/.authswitch/claude`. Emails are accepted as account
@@ -251,6 +310,70 @@ inference failure exits with 1, invalid arguments or unsupported harnesses with
251
310
  2, and an interrupted prompt with 130. Check `authswitch list` before repeating
252
311
  an uncertain request. `--json` remains a list-only option.
253
312
 
313
+ ### Watching usage and switching automatically
314
+
315
+ `authswitch watch` reads every account's usage on an interval and switches a harness to a
316
+ better saved account when the one in use runs out:
317
+
318
+ ```bash
319
+ authswitch watch # every harness that supports it, every 2m, at 95%
320
+ authswitch claude watch # one harness (also: authswitch watch claude)
321
+ authswitch watch --interval 5m --threshold 90 # check less often, switch earlier
322
+ authswitch watch --dry-run # report decisions, change nothing
323
+ authswitch watch --once # one check, then exit
324
+ authswitch watch --json # one JSON event per line
325
+ ```
326
+
327
+ ```
328
+ 14:02:07 Watching Claude Code, Codex every 2m; switching at 95%.
329
+ 14:02:07 Running Codex sessions keep the login they loaded; new sessions use the switched one.
330
+ 14:02:09 Claude Code alice@example.com (type:Max) (5h:17%) (w:4%) ok
331
+ 14:02:10 Codex switched bob@example.com → carol@example.com (type:Pro) (5h:3%) (w:12%) — Codex weekly 96% ≥ 95%
332
+ 14:04:09 Claude Code alice@example.com (type:Max) (5h:21%) (w:4%) ok
333
+ 14:04:10 Codex carol@example.com (type:Pro) (5h:4%) (w:12%) ok
334
+ ```
335
+
336
+ Watch covers the harnesses whose adapter declares `autoSwitch`: Claude Code and Codex.
337
+ OpenCode is not watched, because most of its providers report no usage and its lifecycle
338
+ belongs to its supervisor. Naming an unsupported harness exits with 2, as does an interval
339
+ outside 1 minute to 1 day, a threshold outside 50 to 100, or any other argument the command
340
+ cannot act on. `--interval` takes seconds or a unit (`120`, `90s`, `2m`, `1h`, `1d`).
341
+
342
+ **How it decides.** Each check reads the harness's accounts and every account's usage, then
343
+ applies one rule set per harness:
344
+
345
+ - The account in use is *used up* when any of its general account windows has reached the
346
+ threshold. The watch then switches to the saved account whose account windows are all at
347
+ least ten points below the threshold, preferring the one whose longest window resets
348
+ soonest (capacity that expires first is used first), then the least used, then by name.
349
+ - When every other saved account is used up too, it switches to the one that is usable
350
+ again first, and only when that is more than one interval sooner than the account in use.
351
+ - An account limited only on a feature window (one model or surface) is traded for an
352
+ account that is below the margin on *every* window; without one it stays, because it
353
+ still serves everything else.
354
+ - A reading that is unknown, too old, rejected or rate limited never counts as used up and
355
+ never makes an account a target. The line then says so instead of saying `ok`.
356
+
357
+ **What it does and does not do.** A switch goes through the same path as `use`, including
358
+ AGL coordination when AGL manages the login, and never asks for a session of yours to be
359
+ stopped: Claude Code picks the new login up on its next request, and running Codex sessions
360
+ keep the login they loaded while new ones use the switched account (the start line says so
361
+ once). A Codex switch does stop and start Codex' own managed app-server, because `auth.json`
362
+ cannot be rewritten underneath it, so its remote-control connection drops for a moment. A
363
+ switch that is refused or fails is reported and decided again on the next check. Checks
364
+ never overlap; a check that takes longer than the interval is followed by the next one at
365
+ once. An account whose lookup a provider refused (HTTP 429) or whose lookup failed is not
366
+ asked about again until the time the provider named, or after a delay that doubles from one
367
+ interval up to half an hour, and the account's last reading stands in meanwhile while it is
368
+ less than ten minutes old. Claude's profile is read at most once an hour per account while
369
+ watching; usage is always live.
370
+
371
+ One watch runs per `AUTHSWITCH_HOME`: a second one exits with 1 and names the running pid,
372
+ and a lock left behind by a watch that died is taken over. Ctrl-C or `SIGTERM` cancels the
373
+ lookups in flight, releases the lock and exits with 0. `--once` exits with 0, or 1 when a
374
+ switch it attempted did not complete. `--json` writes one event per line
375
+ (`start`, `tick`, `switch`, `error`) with accounts, readings and reasons, and no credential.
376
+
254
377
  ### Management dashboard
255
378
 
256
379
  `authswitch --tui` opens a resizable account table with subscription, usage and
@@ -258,7 +381,7 @@ reset availability, usage bars for the lead window and the next general window,
258
381
  scrollable account details and an activity log.
259
382
  With multiple registered adapters, a harness selector switches the view; account
260
383
  selection and mutations always belong to the displayed harness. Status loads
261
- incrementally through each account's read-only API, without activating it.
384
+ incrementally through each account's status API, without activating it.
262
385
 
263
386
  | Key | Action |
264
387
  | --- | --- |
@@ -322,11 +445,12 @@ The badge appears in the account picker of `use`, in the dashboard's switch
322
445
  confirmation and on the `Now active:` line that reports a completed switch (in the
323
446
  dashboard, its activity log). `type` is the account type as `limits` shows it; `5h`
324
447
  and `w` are the account's five-hour and weekly usage windows, rounded to whole
325
- percent. Feature limits never appear in it, and when several account windows share a
448
+ percent -- except that a window the provider reports below 100% never reads as the
449
+ exhausted `100%`. Feature limits never appear in it, and when several account windows share a
326
450
  period without the provider picking one of them, that part is left out rather than
327
451
  guessed.
328
452
 
329
- Usage is read live, read-only and once per command. The picker looks up the offered
453
+ Usage is read live and once per command. The picker looks up the offered
330
454
  accounts, at most four at a time, before it asks; a switch named on the command line
331
455
  switches first and then looks up only its target for the `Now active:` line. Whatever
332
456
  has not answered after 2.5 seconds is left out and its requests are cancelled, so the
@@ -361,90 +485,167 @@ by an interrupted write.
361
485
  ## Account status
362
486
 
363
487
  `authswitch list` lists every registered harness; `authswitch codex list` limits the
364
- output to Codex. Each section includes saved accounts and an identifiable active
365
- login even if it has never been saved. `*` marks the active account, and the saved
366
- marker checks the credential itself rather than just the existence of metadata.
367
-
368
- The overview compares login state, plan provenance, lead-window usage (the
369
- provider's headline window, otherwise weekly), earned resets and that window's
370
- reset countdown. Countdowns use days, hours and minutes, such as
371
- `7d 5h 6min`;
372
- `<1min` means less than a minute remains and `due` means the reported deadline has
373
- passed, without claiming the service has refreshed the quota. All countdowns share
374
- the same snapshot time.
375
- Usage and countdown refer to the same window. When the provider picks a headline
376
- window (Claude does, for example its Fable weekly limit), that window leads.
377
- Otherwise the
378
- general account window leads, preferring weekly; if no weekly window is reported,
379
- the longest general window is shown with its actual duration. Other exhausted
380
- general windows retain their own reset warning. A general window is named by its
381
- period (`Weekly`, `5h`) unless another shown general window has the same period; a
382
- feature limit, or a general window sharing its period, is named by its label
383
- (`Fable weekly: 100% used`, `Claude weekly (weekly_oauth) exhausted`). Feature
384
- limits such as Spark and code review have a separate table and determine the
385
- summary only when the provider picks one as its headline. The TUI uses the same
386
- selection and names.
387
- Window durations come from the provider, not from the plan name or
388
- primary/secondary position; a Pro account with only a general weekly window gets
389
- no invented 5-hour limit.
390
-
391
- Email addresses identify Codex accounts throughout the overview, usage/reset
392
- schedules, earned reset expiry details, grouped credits and activity metrics,
393
- saved-login notes and availability/actions tables. Other harnesses use their
394
- account labels. When accounts of one harness share a label, every table numbers
395
- the later ones (`alice@example.com (2)`), so each account keeps its own column in
396
- the metric comparisons and reads the same everywhere. Provider facts already
397
- represented by structured fields are not repeated. Narrow terminals use compact
398
- account sections. Missing
399
- data stays Unavailable or Not reported; known zeroes remain zero. Plans read as in
400
- `limits` and the switch badge (`Max`), and stored plans are marked unverified
401
- (`Plus, unverified`). Renewal/cancellation dates are shown only when an adapter can
402
- provide live billing data. Codex uses the desktop app's account-check endpoint for
403
- automatic renewal, explicit renewal/cancellation dates and subscription expiry.
404
-
405
- Tables that list an account on several rows (the usage/reset schedule,
406
- feature-specific limits, earned reset details, and availability/actions) draw a
407
- divider between one account and the next. The divider follows the account id, not
408
- the email, so two accounts that share an address stay separate blocks, and the
409
- harness-level availability row is a block of its own. The overview and the metric
410
- comparisons show each account once and have no dividers. Each `list` section covers
411
- one harness, so no colour line is drawn. Narrow terminals separate the same blocks
412
- with a rule line.
488
+ output to Codex. Harnesses appear in the order `limits` uses, by provider name. Each
489
+ section includes saved accounts and an identifiable active login even if it has
490
+ never been saved, and draws up to four tables under the harness's name:
491
+
492
+ ```
493
+ Codex
494
+ ┌───────────────────┬──────────────────┬────────────────────┬─────────────┬───────────────┐
495
+ │ Account │ Status │ Usage │ Renewal │ Earned resets │
496
+ ├───────────────────┼──────────────────┼────────────────────┼─────────────┼───────────────┤
497
+ │ alice@example.com │ active, saved │ weekly 12% · 3d 4h │ 2026-10-08 │ 0 │
498
+ │ Pro │ 2026-09-17 13:32 │ 5h 37% · 2h 13m │ auto-renews │ │
499
+ ├───────────────────┼──────────────────┼────────────────────┼─────────────┼───────────────┤
500
+ │ bob@example.com │ saved │ usage n/a │ n/a │ n/a │
501
+ │ Plus, unverified │ 2026-09-14 11:32 │ │ │ │
502
+ └───────────────────┴──────────────────┴────────────────────┴─────────────┴───────────────┘
503
+
504
+ ┌───────────────────┬────────────────────┬────────┬───────────┐
505
+ │ Account │ Limit type │ Used % │ Resets in │
506
+ ├───────────────────┼────────────────────┼────────┼───────────┤
507
+ │ alice@example.com │ Codex weekly │ 12% │ 3d 4h │
508
+ │ Pro │ Codex five-hour │ 37% │ 2h 13m │
509
+ │ │ Code review weekly │ 100% │ 3h 59m │
510
+ └───────────────────┴────────────────────┴────────┴───────────┘
511
+
512
+ ┌────────────────┬───────────────────┐
513
+ │ Metric │ alice@example.com │
514
+ ├────────────────┴───────────────────┤
515
+ │ Saved login │
516
+ │ remote-control │ 1 │
517
+ └────────────────┴───────────────────┘
518
+
519
+ ┌─────────────────┬─────────────────────────────────────────────────────────┐
520
+ │ Account │ Problem │
521
+ ├─────────────────┼─────────────────────────────────────────────────────────┤
522
+ │ bob@example.com │ Subscription and limits: Login expired or was rejected. │
523
+ └─────────────────┴─────────────────────────────────────────────────────────┘
524
+ ```
525
+
526
+ 1. **Accounts** -- one row per account, divided from the next. **Account** is the
527
+ label (bold when active) with the account type beneath it, as in `limits`, and,
528
+ for a harness with several credential slots, the slot (`openai · Pro`).
529
+ **Status** says whether the login is active and saved, with the local date and
530
+ time of its last save beneath; an active login without a save reads `not saved`.
531
+ **Usage** holds one compact line per headline window, `<window> <used>% · <reset
532
+ countdown>`, lead window first as described below. **Renewal** is the same cell as
533
+ in `limits`, and a harness whose provider never reports billing dates, such as Claude
534
+ Code, gets one note beneath the table instead of an unexplained `n/a` per account.
535
+ **Earned resets** appears only when an account of the harness reports earned reset
536
+ credits: the available count, then one line per reported credit.
537
+ 2. **Limits** -- the `limits` table of this harness, without its Provider and
538
+ Renewal columns, for the accounts that report windows.
539
+ 3. **Details** -- one comparison of every remaining provider fact: **Metric** and
540
+ one column per account, split into pages when the terminal is too narrow, with a
541
+ full-width row per section. Facts already shown above are left out: those that
542
+ restate a displayed structured field, and one that only repeats the account's own
543
+ label. Dated rows, such as Codex's daily token totals, are merged across accounts
544
+ and keep the newest seven days. A sentence that every account of its section
545
+ states identically, such as Claude's note that it reports no billing dates, is
546
+ printed once beneath the table. Saved-login notes (Codex remote-control pairing,
547
+ account IDs) form the **Saved login** section; a note that only names the
548
+ account's credential slot is left out, because the Accounts table names it.
549
+ 4. **Problems** -- a login changed outside authswitch first (`Active login changed
550
+ outside authswitch: expected alice@example.com, now bob@example.com (not saved) —
551
+ authswitch claude stash --keep saves it`), then every account's problems, then the
552
+ harness's own. Problems of one account that share a message are merged with their
553
+ topics (`Profile, usage:` above the message), and accounts with the same problems
554
+ share one block, whose **Account** cell names them all on one line.
555
+
556
+ Human output never prints a raw ISO timestamp: dates and times are local
557
+ (`2026-10-08`, `2026-09-17 13:32`), and a timestamp inside provider text is rewritten
558
+ the same way. `list --json` keeps every original value. A note beneath a table wraps to
559
+ the terminal, with its continuation lines under the note's prefix.
560
+
561
+ The lead window is the provider's headline window, otherwise weekly. Countdowns use
562
+ two units, such as `3d 4h`; `due` means the reported deadline has passed, without
563
+ claiming the service has refreshed the quota, and `not scheduled` means the provider
564
+ reported no reset. All countdowns share the same snapshot time. When the provider
565
+ picks a headline window (Claude does, for example its Fable weekly limit), that
566
+ window leads. Otherwise the general account windows lead, weekly first. A general
567
+ window is named by its period (`weekly`, `5h`) unless another shown general window
568
+ has the same period; a feature limit, or a general window sharing its period, is
569
+ named by its label (`Fable weekly 100% · 2d`). Feature limits such as Spark and code
570
+ review appear in the Limits table and lead the usage cell only when the provider
571
+ picks one as its headline. The TUI uses the same selection and names. Window
572
+ durations come from the provider, not from the plan name or the slot a window
573
+ arrives in; a Pro account with only a general weekly window gets no invented 5-hour
574
+ limit.
575
+
576
+ Every window label names the window by how long it is -- `Codex weekly`, `Codex
577
+ five-hour`, `Claude weekly`, `Code review weekly` -- so the same length reads the
578
+ same in every harness. The named lengths are `five-hour`, `daily`, `weekly`,
579
+ `monthly` and `annual`, the same set Codex names its own windows with, and its
580
+ primary/secondary slot is shown (`Codex weekly (secondary)`) only where two windows
581
+ of one length would otherwise share a label. A length with no name of its own keeps
582
+ its compact duration (`Codex 90m`) instead of reading as a period it is not.
583
+
584
+ When accounts of one harness share a label, every table numbers the later ones
585
+ (`alice@example.com (2)`), so each account keeps its own column in the Details
586
+ comparison and reads the same everywhere. Missing data stays `n/a`; known zeroes
587
+ remain zero. Plans read as in `limits` and the switch badge (`Max`), and stored plans
588
+ are marked unverified (`Plus, unverified`). Renewal dates are shown only when an
589
+ adapter can provide live billing data. Codex uses the desktop app's account-check
590
+ endpoint for automatic renewal, explicit renewal/cancellation dates and subscription
591
+ expiry.
592
+
593
+ The layout follows the terminal width. From 120 columns every Accounts column stands
594
+ alone; from 100 the earned resets join the usage cell; from 60 the status joins the
595
+ account cell; below 60 one Status cell holds status, usage, renewal and earned resets;
596
+ below 40 every table becomes labelled lines, separated by a rule line per block. Each
597
+ `list` section covers one harness, so no colour line is drawn.
413
598
 
414
599
  ### Condensed views
415
600
 
416
- `authswitch limits` answers "how much is left, and when does it come back" in one
417
- table. It is one row per account **and** limit type, sorted by provider and then by
418
- account email with each harness and each account kept together, and a two-unit countdown (`2h 13m`, `3d 4h`, `45m`, `<1m`, `due`):
601
+ `authswitch limits` answers "how much is left, when does it come back, and when does
602
+ the subscription renew" in one table. It is one row per account **and** limit type,
603
+ sorted by provider and then by account email with each harness and each account kept
604
+ together, with a two-unit countdown (`2h 13m`, `3d 4h`, `45m`, `<1m`, `due`) and the
605
+ account's next billing event:
419
606
 
420
607
  ```
421
608
  Account limits
422
- ┌──────────────────────┬─────────────────────────┬───────────────────────┬────────┬───────────────┐
423
- │ Provider │ Account │ Limit type │ Used % │ Resets in │
424
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
425
- ┃ Claude Code │ phil@example.com │ Claude weekly │ 75% │ 4d 3h │
426
- ┃ │ Max │ Claude five-hour │ 12% │ 1h 2m │
427
- ┃ │ │ Fable weekly │ 88% │ 2d 5h │
428
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
429
- ┃ Codex │ alice@example.com │ Codex secondary │ 12% │ 3d 4h │
430
- ┃ │ Pro │ Codex primary │ 37% │ 2h 13m │
431
- ┃ │ │ Code review secondary │ 100% │ 3h 59m │
432
- ┃ ├─────────────────────────┼───────────────────────┼────────┼───────────────┤
433
- ┃ │ bob@example.com │ n/a │ n/a │ n/a │
434
- ┃ │ Plus, unverified │ │ │ │
435
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
436
- ┃ OpenCode / anthropic │ anthropic API key 20e75 │ n/a │ n/a │ n/a │
437
- ┃ │ 1c3b707 │ │ │ │
438
- ┃ │ n/a │ │ │ │
439
- ├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
440
- ┃ OpenCode / openai │ alice@example.com │ Codex primary │ 0% │ not scheduled │
441
- ┃ │ Pro │ │ │ │
442
- └──────────────────────┴─────────────────────────┴───────────────────────┴────────┴───────────────┘
609
+ ┌──────────────────┬───────────────────┬───────────────────┬────────┬───────────────┬─────────────┐
610
+ │ Provider │ Account │ Limit type │ Used % │ Resets in │ Renewal │
611
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
612
+ ┃ Claude Code │ phil@example.com │ Claude weekly │ 75% │ 4d 3h │ n/a │
613
+ ┃ │ Max │ Claude five-hour │ 12% │ 1h 2m │ │
614
+ ┃ │ │ Fable weekly │ 88% │ 2d 5h │ │
615
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
616
+ ┃ Codex │ alice@example.com │ Codex weekly │ 12% │ 3d 4h │ 2026-10-08 │
617
+ ┃ │ Pro │ Codex five-hour │ 37% │ 2h 13m │ auto-renews │
618
+ ┃ │ │ Code review weekl │ 100% │ 3h 59m │ │
619
+ ┃ │ │ y │ │ │ │
620
+ ┃ ├───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
621
+ ┃ │ bob@example.com │ n/a │ n/a │ n/a │ n/a │
622
+ ┃ │ Plus, unverified │ │ │ │ │
623
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
624
+ ┃ OpenCode / anthr │ anthropic API key │ n/a │ n/a │ n/a │ n/a │
625
+ ┃ opic │ 20e751c3b707 │ │ │ │ │
626
+ ┃ │ n/a │ │ │ │ │
627
+ ├──────────────────┼───────────────────┼───────────────────┼────────┼───────────────┼─────────────┤
628
+ ┃ OpenCode / opena │ alice@example.com │ Codex five-hour │ 0% │ not scheduled │ 2026-10-02 │
629
+ ┃ i │ Pro │ │ │ │ cancels │
630
+ └──────────────────┴───────────────────┴───────────────────┴────────┴───────────────┴─────────────┘
443
631
 
444
632
  n/a — Codex · bob@example.com: Subscription and limits: Login expired or was rejected.
445
- n/a — OpenCode / anthropic · anthropic API key 20e751c3b707: This provider login does not expose a supported subscription or quota API.
633
+ n/a — OpenCode / anthropic · anthropic API key 20e751c3b707: This provider login does not expose a
634
+ supported subscription or quota API.
635
+ n/a — Claude Code: renewal dates are not exposed by Claude's account API.
446
636
  ```
447
637
 
638
+ The table fits 100 columns; at that width long provider names, account labels and
639
+ limit types wrap inside their cells.
640
+
641
+ **Renewal** spans the rows of its account with two lines: the local calendar date,
642
+ and beneath it `auto-renews` when the provider says the subscription renews then, or
643
+ `cancels` when it ends then -- a scheduled cancellation, or an expiry with automatic
644
+ renewal off. Anything else reads `n/a`: the date comes only from live billing data,
645
+ never from a plan, a quota reset, account creation or token expiry. A harness whose
646
+ provider never reports billing dates, such as Claude Code, gets one footnote instead
647
+ of an unexplained `n/a` per account.
648
+
448
649
  A number is never invented. An account whose provider exposes no quota API, whose
449
650
  lookup failed, or whose login cannot be read shows `n/a` in **Used %** and
450
651
  **Resets in**, and the footnote under the table says why — one line per distinct
@@ -502,7 +703,8 @@ Active accounts
502
703
  ┃ OpenCode / openai │ alice@example.com │ not saved │ credential file │
503
704
  └──────────────────────┴────────────────────────────────┴────────────┴─────────────────┘
504
705
 
505
- note — OpenCode / openai · alice@example.com: This active login is not saved yet; authswitch cannot restore it after a switch.
706
+ note — OpenCode / openai · alice@example.com: This active login is not saved yet; authswitch cannot
707
+ restore it after a switch.
506
708
  ```
507
709
 
508
710
  **Source** is where the shown login comes from: `credential file` is the harness's
@@ -520,19 +722,27 @@ finds a different account in that slot, the row's source reads
520
722
  `credential file (changed)` and a note explains it:
521
723
 
522
724
  ```
523
- note — Claude Code · bob@example.test: The credential file changed since the last
524
- switch (likely a running instance refreshed the previous login). It now holds
525
- bob@example.test, which is saved. Run authswitch claude use alice@example.test again.
725
+ note — Claude Code · bob@example.test: The active login changed outside authswitch since the last
726
+ switch (for example a new login). It now holds bob@example.test, which is saved; authswitch
727
+ claude use alice@example.test switches back.
526
728
  ```
527
729
 
730
+ For a harness that does not pick up swaps live, such as OpenCode, the sentence also
731
+ names the other cause: a running instance that wrote its previous login back. An
732
+ unsaved current login adds how to keep it (`authswitch <harness> stash --keep saves
733
+ it`).
734
+
528
735
  `authswitch <harness> current` prints the same sentence. The same account with a
529
- rotated token is that account refreshing itself and is not reported; re-running the
530
- switch re-arms the check against the file's current contents.
736
+ rotated token is that account refreshing itself and is not reported; for Claude Code the
737
+ rotation is copied into that account's saved record, and re-running the switch re-arms the
738
+ check against the file's current contents.
531
739
 
532
740
  Both commands accept a harness qualifier (`authswitch claude limits`) and `--json`.
533
- Both are read-only: they never activate, save or clear a login, they need no harness
534
- stopped, and every provider lookup is bounded by the same 10-second timeout and
535
- isolated per account as `authswitch list`.
741
+ Neither changes which account is in use: they never activate, switch or clear a login and
742
+ they need no harness stopped. Like `list`, they may renew a saved Claude login's own tokens
743
+ when its access token has expired, because otherwise its usage cannot be read at all. Every
744
+ provider lookup is bounded by the same 10-second timeout and isolated per account as
745
+ `authswitch list`.
536
746
 
537
747
  ### JSON output
538
748
 
@@ -547,7 +757,9 @@ the same rows as the tables, plus the machine-readable fields the tables condens
547
757
  `accountId`, `slotId`, `accountType` (`{ plan, source }` with the adapter's plan
548
758
  name, such as `max` or `pro`, sanitised but not display-formatted, and `source` `live`
549
759
  or `stored`; null when no type is known or the row names no account; both documents
550
- carry it), `scope`, `windowSeconds`, the ISO `resetAt` beside the human
760
+ carry it), the limits rows' `billing` (the account's reported `hasActiveSubscription`,
761
+ `autoRenew` and ISO `renewsAt`, `cancelsAt` and `expiresAt`, each omitted when not
762
+ reported; null when the provider reported none or the row names no account), `scope`, `windowSeconds`, the ISO `resetAt` beside the human
551
763
  `resetsIn`, the provider's `severity` (`normal`, `warning`, `critical`, or null when
552
764
  it gave none) and `headline` (true for the window the provider picks for a
553
765
  single-value summary), the ISO `savedAt` beside the human `savedAgo`, the per-row
@@ -582,13 +794,13 @@ accept schema version 2 and handle null reset timestamps. Scripts should qualify
582
794
  mutation commands with `codex`, `opencode` or `claude`, since all three now register
583
795
  by default. Saved Codex credentials retain their existing format.
584
796
 
585
- For each ChatGPT login, Codex status includes the live plan, primary and secondary
586
- usage windows with remaining percentages and UTC reset times, additional limits
797
+ For each ChatGPT login, Codex status includes the live plan, every usage window the
798
+ service reports with remaining percentages and reset times, additional limits
587
799
  such as code review, credit availability and balance, spend limits when returned,
588
800
  earned reset availability and expiry details, lifetime and peak daily tokens,
589
801
  activity streaks, and the latest seven reported daily token totals.
590
802
 
591
- The Codex adapter implements the read-only GET requests used by Codex 0.154.0 at
803
+ The Codex adapter implements the read-only GET requests used by Codex 0.155.0 at
592
804
  `chatgpt.com/backend-api/wham`: `usage`, `rate-limit-reset-credits`, and `profiles/me`.
593
805
  Billing uses `chatgpt.com/backend-api/accounts/check/v4-2023-04-27`, verified against
594
806
  the official desktop bundle 26.908.40834. It selects the exact credential account;
@@ -608,9 +820,9 @@ The returned usage plan is not a billing status. Billing dates come from the
608
820
  separate account-check response, never from a quota reset, token expiry or stored
609
821
  plan. Automatic renewal is on only when the provider says it will renew and no
610
822
  cancellation date is scheduled. A non-renewing account without a cancellation date
611
- does not acquire an invented date. The expiry of an entitlement is displayed
612
- separately. When renewal is off, a returned renewal timestamp is labelled Renewal
613
- boundary rather than promising a future charge.
823
+ or expiry does not acquire an invented date. When renewal is off, a returned renewal
824
+ timestamp is never shown as a renewal; the Renewal cell shows the expiry as the day
825
+ the subscription ends. JSON keeps every returned field.
614
826
 
615
827
  The billing request uses a verified Chromium-compatible User-Agent because the
616
828
  service challenged the generic authswitch User-Agent in controlled comparisons.
@@ -640,11 +852,19 @@ process.exitCode = await cli.run(process.argv.slice(2));
640
852
 
641
853
  Each adapter owns discovery, identity matching, save verification, credential
642
854
  storage, switching and process lifecycle, live status APIs, and diagnostics.
643
- `readState()` and `readAccountStatus()` must be read-only. `readAccountStatus(accountId,
855
+ `readState()` and `readAccountStatus()` must never activate, switch or clear a login.
856
+ `readAccountStatus()` may renew the tokens of the saved copy it is reading, when the provider
857
+ gives it no other way to answer (the Claude Code adapter does, under the store's lock, for
858
+ saved and inactive logins only); it must never touch the active login's credential. `readAccountStatus(accountId,
644
859
  options)` receives an optional `IHarnessStatusOptions` whose `signal` cancels the
645
860
  lookup's requests; authswitch aborts it when a status is no longer wanted, such as a
646
861
  switch badge past its deadline or a closed dashboard, so an adapter should pass it to
647
- every request it makes. `switchAccount()` must
862
+ every request it makes. Its optional `session` belongs to a caller that reads the same
863
+ accounts repeatedly, such as `authswitch watch`: `session.reuse(key, maxAgeMs, lookup)`
864
+ returns the value that key resolved to less than `maxAgeMs` ago, so an adapter can stop
865
+ repeating a lookup whose answer rarely changes (the Claude adapter reuses its profile for
866
+ an hour). Keys are the adapter's own and carry no secret; a rejected lookup is not
867
+ remembered, and outside a session every lookup is live. `switchAccount()` must
648
868
  preserve and verify the outgoing credential without relying on the interactive
649
869
  save prompt. `removeAccount()` removes only the saved copy. Status facts carry
650
870
  provider-specific labels and units; unsupported capabilities are explicit. Adapter
@@ -678,7 +898,7 @@ count; if it fails, the limits response can still supply availability.
678
898
 
679
899
  Each usage window may set `scope: 'account' | 'feature'` (default `account`).
680
900
  Adapters mark model-specific or feature-specific quotas as `feature`; those quotas
681
- appear in the feature tables, JSON and details and lead the account summary only
901
+ appear in the Limits table, JSON and details and lead the account summary only
682
902
  when the provider marks one as its headline (see below). Weekly windows are identified by `durationSeconds: 604800`;
683
903
  neither window labels nor plan names are parsed to infer limits.
684
904
 
@@ -700,7 +920,21 @@ Facts may supply a `section` label for grouped comparison and a `summaryKey`
700
920
  (`subscription`, `billing`, `usageWindows`, `resets`, or `resetDetails`) when the
701
921
  fact duplicates a structured field. The list hides such a fact only if that field
702
922
  is displayed; JSON retains every fact. Adapters without this metadata continue to
703
- display their facts in Additional information.
923
+ display their facts in Additional information. A fact label ending in an ISO date
924
+ (`Tokens 2026-09-15`) marks a daily series, which the list merges across accounts.
925
+
926
+ Three optional adapter declarations shape the shared views. `liveSwap: true` says that
927
+ running instances pick up a credential swap on their next request: the CLI and TUI
928
+ then offer no stop before a switch (an explicit `--stop` or `--force-stop` is still
929
+ honoured through `processes`), outcomes carry no restart advice, and the credential
930
+ drift sentence names only a new login as the cause. `renewalUnavailableReason` is a
931
+ lower-case clause for a provider that never reports renewal dates; `list` and `limits`
932
+ print it once as `<label>: <reason>`. `autoSwitch: true` says that every saved account's
933
+ usage can be read and that a switch needs no session of the user's stopped, which is what
934
+ `authswitch watch` requires before it switches a harness by itself; a helper process the
935
+ adapter manages itself may still be restarted by its own switch, as Codex' app-server is. The built-in Claude Code adapter declares
936
+ all three; Codex declares `autoSwitch: true` with `liveSwap: false`, and OpenCode declares
937
+ neither.
704
938
 
705
939
  With multiple adapters, the guide first asks which harness to manage and provides
706
940
  an action to choose another. Unqualified mutation commands prompt for a harness;
@@ -727,7 +961,7 @@ Account identity comes from the `id_token` inside the credential — the email c
727
961
 
728
962
  **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.
729
963
 
730
- **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.
964
+ **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.
731
965
 
732
966
  **The Codex stash is keyed by email.** Two Codex workspaces with the same email
733
967
  cannot both be saved under that key. Their account IDs are distinguished during