@modelprofile.com/authswitch 2.1.2

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 (95) hide show
  1. package/.smartconfig.json +34 -0
  2. package/cli.js +21 -0
  3. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  4. package/dist_ts/00_commitinfo_data.js +9 -0
  5. package/dist_ts/accounts.d.ts +31 -0
  6. package/dist_ts/accounts.js +74 -0
  7. package/dist_ts/classes.accountlist.d.ts +17 -0
  8. package/dist_ts/classes.accountlist.js +214 -0
  9. package/dist_ts/classes.claudecodeharness.d.ts +34 -0
  10. package/dist_ts/classes.claudecodeharness.js +123 -0
  11. package/dist_ts/classes.claudestatus.d.ts +8 -0
  12. package/dist_ts/classes.claudestatus.js +139 -0
  13. package/dist_ts/classes.cli.d.ts +31 -0
  14. package/dist_ts/classes.cli.js +584 -0
  15. package/dist_ts/classes.codexauth.d.ts +35 -0
  16. package/dist_ts/classes.codexauth.js +184 -0
  17. package/dist_ts/classes.codexdaemon.d.ts +38 -0
  18. package/dist_ts/classes.codexdaemon.js +122 -0
  19. package/dist_ts/classes.codexharness.d.ts +35 -0
  20. package/dist_ts/classes.codexharness.js +199 -0
  21. package/dist_ts/classes.codexhome.d.ts +25 -0
  22. package/dist_ts/classes.codexhome.js +60 -0
  23. package/dist_ts/classes.codexpreuse.d.ts +8 -0
  24. package/dist_ts/classes.codexpreuse.js +90 -0
  25. package/dist_ts/classes.codexstatus.d.ts +20 -0
  26. package/dist_ts/classes.codexstatus.js +304 -0
  27. package/dist_ts/classes.codexswitcher.d.ts +61 -0
  28. package/dist_ts/classes.codexswitcher.js +226 -0
  29. package/dist_ts/classes.credentialstore.d.ts +32 -0
  30. package/dist_ts/classes.credentialstore.js +115 -0
  31. package/dist_ts/classes.fileharness.d.ts +47 -0
  32. package/dist_ts/classes.fileharness.js +119 -0
  33. package/dist_ts/classes.login.d.ts +42 -0
  34. package/dist_ts/classes.login.js +102 -0
  35. package/dist_ts/classes.opencodeharness.d.ts +34 -0
  36. package/dist_ts/classes.opencodeharness.js +125 -0
  37. package/dist_ts/classes.operations.d.ts +44 -0
  38. package/dist_ts/classes.operations.js +97 -0
  39. package/dist_ts/classes.remotecontrol.d.ts +46 -0
  40. package/dist_ts/classes.remotecontrol.js +169 -0
  41. package/dist_ts/classes.service.d.ts +47 -0
  42. package/dist_ts/classes.service.js +142 -0
  43. package/dist_ts/classes.stashstore.d.ts +63 -0
  44. package/dist_ts/classes.stashstore.js +192 -0
  45. package/dist_ts/classes.tui.d.ts +11 -0
  46. package/dist_ts/classes.tui.js +195 -0
  47. package/dist_ts/formatting.d.ts +8 -0
  48. package/dist_ts/formatting.js +13 -0
  49. package/dist_ts/helpers.d.ts +39 -0
  50. package/dist_ts/helpers.js +92 -0
  51. package/dist_ts/index.d.ts +22 -0
  52. package/dist_ts/index.js +27 -0
  53. package/dist_ts/interfaces.d.ts +145 -0
  54. package/dist_ts/interfaces.harness.d.ts +130 -0
  55. package/dist_ts/interfaces.harness.js +2 -0
  56. package/dist_ts/interfaces.js +2 -0
  57. package/dist_ts/interfaces.list.d.ts +19 -0
  58. package/dist_ts/interfaces.list.js +2 -0
  59. package/dist_ts/plugins.d.ts +14 -0
  60. package/dist_ts/plugins.js +18 -0
  61. package/dist_ts/preuse.d.ts +6 -0
  62. package/dist_ts/preuse.js +13 -0
  63. package/license.md +21 -0
  64. package/package.json +53 -0
  65. package/readme.md +502 -0
  66. package/ts/00_commitinfo_data.ts +8 -0
  67. package/ts/accounts.ts +78 -0
  68. package/ts/classes.accountlist.ts +211 -0
  69. package/ts/classes.claudecodeharness.ts +111 -0
  70. package/ts/classes.claudestatus.ts +99 -0
  71. package/ts/classes.cli.ts +479 -0
  72. package/ts/classes.codexauth.ts +199 -0
  73. package/ts/classes.codexdaemon.ts +137 -0
  74. package/ts/classes.codexharness.ts +233 -0
  75. package/ts/classes.codexhome.ts +68 -0
  76. package/ts/classes.codexpreuse.ts +76 -0
  77. package/ts/classes.codexstatus.ts +257 -0
  78. package/ts/classes.codexswitcher.ts +272 -0
  79. package/ts/classes.credentialstore.ts +101 -0
  80. package/ts/classes.fileharness.ts +120 -0
  81. package/ts/classes.login.ts +103 -0
  82. package/ts/classes.opencodeharness.ts +114 -0
  83. package/ts/classes.operations.ts +101 -0
  84. package/ts/classes.remotecontrol.ts +208 -0
  85. package/ts/classes.service.ts +152 -0
  86. package/ts/classes.stashstore.ts +223 -0
  87. package/ts/classes.tui.ts +138 -0
  88. package/ts/formatting.ts +14 -0
  89. package/ts/helpers.ts +109 -0
  90. package/ts/index.ts +28 -0
  91. package/ts/interfaces.harness.ts +112 -0
  92. package/ts/interfaces.list.ts +19 -0
  93. package/ts/interfaces.ts +158 -0
  94. package/ts/plugins.ts +20 -0
  95. package/ts/preuse.ts +15 -0
package/readme.md ADDED
@@ -0,0 +1,502 @@
1
+ # @modelprofile.com/authswitch
2
+
3
+ Manage Codex, OpenCode and Claude Code accounts, switch interactively, and inspect live subscription, usage and reset information. Codex switching also preserves ChatGPT remote-control pairings.
4
+
5
+ ## Issue Reporting and Security
6
+
7
+ For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
8
+
9
+ ## The problem
10
+
11
+ The Codex CLI holds exactly one active credential. Logging in as a second account overwrites the first, so working across two accounts means re-running the browser login every time.
12
+
13
+ Remote control makes that worse. A ChatGPT client pairs with the local Codex app-server, and the pairing is recorded in Codex' state database keyed by the account id. Swap the credential naively and the running app-server keeps serving the old identity until it is restarted, and the incoming account can come back without the enrollment that made it reachable from the ChatGPT app.
14
+
15
+ `authswitch` keeps a stash of credentials keyed by account email, captures each account's remote-control enrollments next to its credential, and puts both back together.
16
+
17
+ ## Install
18
+
19
+ This package is published to the private Verdaccio registry, not to npmjs, so the scope has to be mapped first:
20
+
21
+ ```bash
22
+ pnpm config set @modelprofile.com:registry https://verdaccio.lossless.digital
23
+ ```
24
+
25
+ ```bash
26
+ pnpm install -g @modelprofile.com/authswitch
27
+ ```
28
+
29
+ Node.js 24 or newer is required.
30
+
31
+ ## Usage
32
+
33
+ Run `authswitch`, `authswitch -i`, or `authswitch --interactive` for an interactive guide. Use the arrow keys and
34
+ Enter to switch accounts, save the current login, list accounts and live status, check remote
35
+ control, or remove a saved account. The guide shows the current account and returns
36
+ to the menu after each successful action; choose **Back** in a submenu or **Exit** to finish.
37
+
38
+ Before showing the actions, the guide checks whether the current credential has a
39
+ matching, readable saved copy. A new or refreshed login gets an offer to save it
40
+ while keeping it active. The switch action checks again before offering targets;
41
+ declining that save cancels the switch. Scripts still preserve the outgoing login
42
+ automatically through the harness adapter.
43
+
44
+ When saving, choose whether to keep the account active or clear the active login so
45
+ you can run `codex login` for another account. Removing a saved account requires
46
+ confirmation and leaves the active login in place.
47
+
48
+ The guide requires an interactive terminal. With no arguments, piped output and CI
49
+ runs show help instead; explicitly requesting `-i`, `--interactive`, or `--tui`
50
+ without an interactive terminal fails. Explicit commands remain available for scripts:
51
+
52
+ ```bash
53
+ authswitch # interactive guide
54
+ authswitch -i # explicitly open the guide
55
+ authswitch codex --interactive # guide for one harness
56
+ authswitch --tui # full-screen account management
57
+ authswitch codex --tui # start the dashboard on Codex
58
+ authswitch list # all known accounts across registered harnesses, with live status
59
+ authswitch list --json # complete account/status document for scripts
60
+ authswitch codex stash # save the active credential under its account email
61
+ authswitch codex list # Codex accounts, including an unsaved active login, with live status
62
+ authswitch codex use [email] # activate one; prompts when no email is given
63
+ authswitch codex preuse <email> # send the default prompt without activating the account
64
+ authswitch codex current # print the account currently in use
65
+ authswitch codex drop <email> # forget a stash
66
+ authswitch codex doctor # report credential storage and remote-control state
67
+ authswitch opencode list # all OpenCode provider logins and saved accounts
68
+ authswitch opencode stash openai --keep
69
+ authswitch opencode use alice@example.com
70
+ authswitch opencode login openai # device login; save the new account without switching
71
+ authswitch codex login # save a new Codex account without disturbing the current login
72
+ authswitch claude stash --keep # save a Claude Code subscriber login
73
+ authswitch claude list --json
74
+ authswitch claude use alice@example.com
75
+ ```
76
+
77
+ ### OpenCode and Claude Code
78
+
79
+ Both adapters support `current`, `stash`, `use`, `drop`, `doctor`, the guide,
80
+ the management TUI and human/JSON lists. With an AGL installation that supports
81
+ authswitch coordination, the CLI delegates credential changes to AGL. AGL owns
82
+ stopping and restarting its OpenCode runtime. Idle changes need no additional
83
+ restart confirmation; active work requires consent to wait for it to finish.
84
+ Unmanaged native processes must still be exited manually: a terminal session
85
+ cannot safely be reconstructed from its PID. Status lookups remain read-only
86
+ and work while harnesses are running. `preuse` remains a Codex capability.
87
+
88
+ `authswitch codex login` and `authswitch opencode login openai` use the shared
89
+ OpenAI device login flow. They display a verification link and code, then save
90
+ the new account without changing the active native login. Ctrl-C cancels login.
91
+ Select the saved account with `use` when ready to switch. Claude Code and other
92
+ OpenCode providers retain their native login commands.
93
+
94
+ ### Hosted account management
95
+
96
+ `AuthSwitchService` exposes credential-free account lists, login capabilities,
97
+ login prompts, saved-account identifiers and mutation outcomes for authenticated
98
+ hosts such as AGL. Every request has `protocolVersion: 1`. `list`, `login` and
99
+ `mutate` start bounded background operations; `get` polls the returned operation
100
+ ID and `cancel` cancels a pending login. Operations outlive a disconnected client,
101
+ and completed results remain available for ten minutes. Call `close()` during
102
+ host shutdown. The host must authenticate and authorize every request.
103
+
104
+ Login requests identify the harness, provider and supported flow explicitly.
105
+ `AuthSwitchLogin` is the backend login owner and accepts additional provider
106
+ adapters. Its credential-bearing completion is for backend storage only.
107
+ When an enclosing registry owns an injected adapter, construct it with
108
+ `{ disposeProviders: false }` as the second argument and dispose that registry
109
+ after closing the login service.
110
+
111
+ `OpenCodeHarness.importCredential(providerId, credential)` saves an inactive
112
+ login. `activateCredential(providerId, credential)` preserves and verifies the
113
+ outgoing native login before activation; the caller retains ownership of the
114
+ incoming credential's durable storage. Both are backend APIs, never wire payloads.
115
+
116
+ `AuthSwitchOperations` is shared by the command, guide and TUI. Its optional
117
+ coordinator receives only the harness, operation, account ID, consent to wait,
118
+ and a credential-location fingerprint. AGL owns private-controller discovery;
119
+ authswitch uses `agl authswitch --request <json>` without an AGL package dependency.
120
+ A failed or ambiguous transport never falls through to a second local mutation.
121
+ AGL and the CLI must use matching credential locations and compatible versions.
122
+
123
+ OpenCode credentials live in `${XDG_DATA_HOME:-~/.local/share}/opencode/auth.json`.
124
+ Several providers can be active at once. Saving or switching `openai` preserves
125
+ every other provider, and `stash openai` without `--keep` clears only OpenAI.
126
+ When several logins are active, the guide asks which to save; scripts must name
127
+ the provider or account. In the TUI, select an active row before pressing `a` or `c`.
128
+ ChatGPT OAuth accounts display their email and use the same direct status APIs as
129
+ Codex. API keys and opaque provider tokens have no reliable email, so their labels
130
+ contain the provider and a SHA-256 fingerprint. Their quota/billing information is
131
+ explicitly unavailable. `OPENCODE_AUTH_CONTENT` overrides the file and blocks file
132
+ management. Provider configuration and environment credentials remain managed by OpenCode.
133
+
134
+ Claude Code supports subscriber file logins on Linux and Windows. The adapter pairs
135
+ `~/.claude/.credentials.json` with `~/.claude.json`'s `oauthAccount`; with
136
+ `CLAUDE_CONFIG_DIR`, both files are inside that directory. Switching preserves
137
+ unrelated settings and credential entries and clears native account-specific caches.
138
+ A metadata write failure rolls back our writes and keeps the outgoing account saved.
139
+ macOS Keychain switching and environment/API-key/cloud-provider logins are explicitly
140
+ unsupported. A file login shadowed by an environment override cannot be switched.
141
+ User and current-directory project settings are also checked for credential helpers
142
+ and provider environment overrides; helpers are never executed. Authswitch manages
143
+ the local file login, so launch-time flags, managed policies and credentials supplied
144
+ by embedding applications remain under Claude Code's control.
145
+
146
+ Claude's direct OAuth profile and usage lookups report the plan, weekly and five-hour
147
+ usage, model-specific windows, and available extra-usage budget information.
148
+ These endpoints require the `user:profile` scope and may reject expired or restricted
149
+ logins. A null reset timestamp is shown as **Not scheduled**. Billing renewal and
150
+ cancellation dates and earned reset credits are unavailable from these endpoints;
151
+ authswitch does not infer them from account creation or token expiry dates.
152
+
153
+ OpenCode and Claude saved records are separate owner-only files under
154
+ `~/.authswitch/opencode` and `~/.authswitch/claude`. Emails are accepted as account
155
+ references; ambiguous matches report provider-qualified references and full IDs.
156
+
157
+ ### Preusing an account
158
+
159
+ `preuse` sends one text prompt through the selected account. Use it after a reset
160
+ to make the first token-consuming request for the next usage window:
161
+
162
+ ```bash
163
+ authswitch codex preuse alice@example.com
164
+ authswitch codex preuse alice@example.com --prompt "Write 2000 words about strawberries."
165
+ authswitch codex preuse alice@example.com --model gpt-5.5 --prompt "Describe a strawberry in one sentence."
166
+ ```
167
+
168
+ The default prompt is `Write 2000 words about strawberries.` Without an account,
169
+ an interactive terminal shows an account picker. Scripts must specify an account
170
+ and, when multiple harnesses are registered, the harness. Active and saved
171
+ accounts are supported, including an active login that has not been stashed.
172
+
173
+ Codex inference uses the published FlexHarness models/providers packages and the
174
+ selected ChatGPT login's access token and account ID. It selects the account's
175
+ advertised default text model unless `--model` is supplied, preferring a supported
176
+ low reasoning setting when the catalog provides one. API-key logins are rejected
177
+ because they do not have ChatGPT subscription reset windows. Expired credentials
178
+ must be renewed through Codex and saved again.
179
+
180
+ Each invocation consumes quota and sends at most one inference request. It does
181
+ not watch for resets or schedule future requests. No active login is switched,
182
+ credential refreshed or written, app-server restarted, or earned reset consumed.
183
+ The prompt has no tools, files or conversation history. The generated prose is
184
+ drained without being printed or stored; the command reports provider token
185
+ counts and then reads the account's reset schedule. It does not infer that a
186
+ timer started merely from a zero-percent usage reading or a successful response.
187
+
188
+ Ctrl-C cancels the request. Codex preuse has a three-minute deadline, with a
189
+ ten-second model-catalog limit. Failed or interrupted requests can have consumed
190
+ tokens and are never retried automatically. A completed prompt exits with 0 even
191
+ if the subsequent reset lookup fails, reporting that uncertainty separately;
192
+ inference failure exits with 1, invalid arguments or unsupported harnesses with
193
+ 2, and an interrupted prompt with 130. Check `authswitch list` before repeating
194
+ an uncertain request. `--json` remains a list-only option.
195
+
196
+ ### Management dashboard
197
+
198
+ `authswitch --tui` opens a resizable account table with subscription, usage and
199
+ reset availability, usage bars, scrollable account details and an activity log.
200
+ With multiple registered adapters, a harness selector switches the view; account
201
+ selection and mutations always belong to the displayed harness. Status loads
202
+ incrementally through each account's read-only API, without activating it.
203
+
204
+ | Key | Action |
205
+ | --- | --- |
206
+ | Arrows, Home/End, PageUp/PageDown | Select accounts or scroll the focused view |
207
+ | Tab / Shift-Tab | Move focus between harnesses, accounts, tabs and details |
208
+ | Enter | Switch to the selected saved account; activate a harness in its selector |
209
+ | `/`, `s`, `S` | Filter the table, cycle its sort column, reverse sorting |
210
+ | `a` / `c` | Save the current login and keep it active / save and clear it |
211
+ | `d` | Remove the selected saved copy, preserving the active login |
212
+ | `r` / `g` | Refresh status / run harness diagnostics |
213
+ | `q` / Ctrl-C | Close and restore the terminal |
214
+
215
+ Save, switch and removal actions require confirmation. Confirmations start on
216
+ Cancel; use Left/Right or Tab to select the action, then Enter. Switching first
217
+ checks the current credential and offers to save an unsaved login. A declined,
218
+ failed or unverified save prevents switching. Plain character shortcuts remain
219
+ available as text while editing a table filter; Enter keeps the filter and Escape
220
+ clears it. Closing waits for an in-flight operation to finish its cleanup.
221
+
222
+ Interactive mode flags accept an optional harness and cannot be combined with
223
+ commands or another mode flag.
224
+
225
+ ### Getting your accounts into the stash
226
+
227
+ `stash` reads whatever Codex currently has active, files it under the account's email, and empties the active slot so the next login starts clean:
228
+
229
+ ```bash
230
+ authswitch codex stash # -> stashed alice@example.com
231
+ codex login # log in as the second account
232
+ authswitch codex stash # -> stashed bob@example.com
233
+ ```
234
+
235
+ Use `--keep` to take a checkpoint without logging yourself out.
236
+
237
+ ### Switching
238
+
239
+ ```bash
240
+ authswitch codex use alice@example.com
241
+ ```
242
+
243
+ Or with no argument, to pick from a list:
244
+
245
+ ```bash
246
+ authswitch codex use
247
+ ```
248
+
249
+ 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.
250
+
251
+ ## What a switch actually does
252
+
253
+ `use` runs as one sequence, and steps 1 to 4 stop at the first failure:
254
+
255
+ 1. **Stop the app-server.** It holds the state database open, owns the remote-control websocket, and refreshes tokens on its own schedule. Reading or writing `auth.json` while it runs races that refresh — the copy taken could be a token the daemon has already rotated away, and a credential the daemon wrote afterwards would be destroyed unstashed. `authswitch` stops it through Codex' own `codex app-server daemon stop` rather than signalling the process behind Codex' back, and aborts if it will not stop.
256
+ 2. **Check in the outgoing credential.** Only now is the active credential read, stashed under its account email, and read back from the stash to prove it landed. A credential is never overwritten until a copy has been verified on disk. Re-stashing an account that already has a stash refreshes it.
257
+ 3. **Write the incoming credential** to `auth.json`, owner-only, through an exclusively-created temp file and a rename.
258
+ 4. **Restore the incoming account's remote-control enrollments** into the state database. If the state database or its enrollment table is missing, this fails loudly rather than reporting that it restored nothing.
259
+ 5. **Start the app-server again** — but only if it was running to begin with, so the machine's posture is unchanged. This runs even when an earlier step failed: a failed switch must not leave remote control down.
260
+
261
+ `stash` follows the same quiesce-and-resume discipline, for the same reason.
262
+
263
+ If the switch completes but something did not finish — most likely the app-server refusing to come back — the command prints what is outstanding and exits non-zero, so it cannot look like a success to a script.
264
+
265
+ Running `use` for the account that is already active is not a no-op: it restores that account's enrollments, which is the repair path when remote control has stopped working for the current login.
266
+
267
+ Re-selecting the active account keeps its current tokens, including any refresh
268
+ since it was last saved, and updates the verified stash before restoring pairings.
269
+
270
+ `doctor` prints where the credential lives, which state database is in use, every
271
+ enrollment row, whether the app-server is up, and any stash directory left incomplete
272
+ by an interrupted write.
273
+
274
+ ## Account status
275
+
276
+ `authswitch list` lists every registered harness; `authswitch codex list` limits the
277
+ output to Codex. Each section includes saved accounts and an identifiable active
278
+ login even if it has never been saved. `*` marks the active account, and the saved
279
+ marker checks the credential itself rather than just the existence of metadata.
280
+
281
+ The overview compares login state, plan provenance, weekly usage, earned resets and
282
+ the weekly reset countdown. Countdowns use days, hours and minutes, such as
283
+ `7d 5h 6min`;
284
+ `<1min` means less than a minute remains and `due` means the reported deadline has
285
+ passed, without claiming the service has refreshed the quota. All countdowns share
286
+ the same snapshot time.
287
+ Usage and countdown refer to the same general account window, preferring weekly.
288
+ If no weekly window is reported, the longest general window is shown with its
289
+ actual duration. Other exhausted general windows retain their own reset warning.
290
+ Feature limits such as Spark and code review have a separate table and never
291
+ determine the general summary. The TUI uses the same weekly-first selection.
292
+ Window durations come from the provider, not from the plan name or
293
+ primary/secondary position; a Pro account with only a general weekly window gets
294
+ no invented 5-hour limit.
295
+
296
+ Email addresses identify Codex accounts throughout the overview, usage/reset
297
+ schedules, earned reset expiry details, grouped credits and activity metrics,
298
+ saved-login notes and availability/actions tables. Other harnesses use their
299
+ account labels. Provider facts already represented by structured fields are not
300
+ repeated. Narrow terminals use compact account sections. Missing
301
+ data stays Unavailable or Not reported; known zeroes remain zero. Stored plans are
302
+ marked unverified. Renewal/cancellation dates are shown only when an adapter can
303
+ provide live billing data. Codex uses the desktop app's account-check endpoint for
304
+ automatic renewal, explicit renewal/cancellation dates and subscription expiry.
305
+
306
+ ### JSON output
307
+
308
+ `authswitch list --json`, `authswitch codex list --json`, and the `ls` alias write
309
+ one JSON document to stdout, with no color, tables or progress messages. `--json`
310
+ may appear before or after the command and requires `list` or `ls`. It cannot be
311
+ combined with a mutation or interactive mode. Unknown list arguments exit with 2.
312
+
313
+ The exported `IAccountList` contract contains:
314
+
315
+ - `schemaVersion: 2`, `generatedAt` (ISO UTC), and `complete`.
316
+ - `harnesses[]`: `id`, `label`, `loginHint`, `saveUnavailableReason`, `accounts`, and
317
+ harness-level `problems` (distinguishing failed discovery from an empty list).
318
+ - Each account's opaque `id`, `label`, `isActive`, verified `isStashed`, `savedAt`,
319
+ `details`, and `status` containing all labelled `facts`, `problems`, and optional
320
+ typed `summary` fields. Missing fields remain omitted, not replaced by zero.
321
+
322
+ JSON retains full fact strings and absolute ISO reset/expiry timestamps. It exports
323
+ the credential-free adapter contract, never credentials, raw HTTP responses or
324
+ harness internals. A partial result still produces valid JSON and exits with 1;
325
+ successful and empty results exit with 0. `complete` describes lookup success,
326
+ not whether the provider exposes every possible metric.
327
+
328
+ Version 2 of this JSON schema allows `usageWindows[].resetAt` to be null and adds
329
+ optional account `slotId` fields. Consumers migrating from authswitch 1.x must
330
+ accept schema version 2 and handle null reset timestamps. Scripts should qualify
331
+ mutation commands with `codex`, `opencode` or `claude`, since all three now register
332
+ by default. Saved Codex credentials retain their existing format.
333
+
334
+ For each ChatGPT login, Codex status includes the live plan, primary and secondary
335
+ usage windows with remaining percentages and UTC reset times, additional limits
336
+ such as code review, credit availability and balance, spend limits when returned,
337
+ earned reset availability and expiry details, lifetime and peak daily tokens,
338
+ activity streaks, and the latest seven reported daily token totals.
339
+
340
+ The Codex adapter implements the read-only GET requests used by Codex 0.154.0 at
341
+ `chatgpt.com/backend-api/wham`: `usage`, `rate-limit-reset-credits`, and `profiles/me`.
342
+ Billing uses `chatgpt.com/backend-api/accounts/check/v4-2023-04-27`, verified against
343
+ the official desktop bundle 26.908.40834. It selects the exact credential account;
344
+ another workspace's default entry cannot supply its billing information.
345
+ Each request uses that account's access token and account ID. For the active
346
+ account it reads the current credential, which may be newer than the stash.
347
+ Listing never switches accounts, starts or stops an app-server, refreshes tokens,
348
+ writes credentials, stores status results, or consumes an earned reset.
349
+
350
+ These backend contracts come from Codex's source and can change. Missing fields,
351
+ expired logins, denied requests, timeouts, and unsupported responses are shown
352
+ explicitly; they are never reported as zero usage. Other accounts and successful
353
+ sections still appear, and a failed status lookup makes the command exit nonzero.
354
+ An expired saved login must be renewed through Codex and saved again.
355
+
356
+ The returned usage plan is not a billing status. Billing dates come from the
357
+ separate account-check response, never from a quota reset, token expiry or stored
358
+ plan. Automatic renewal is on only when the provider says it will renew and no
359
+ cancellation date is scheduled. A non-renewing account without a cancellation date
360
+ does not acquire an invented date. The expiry of an entitlement is displayed
361
+ separately. When renewal is off, a returned renewal timestamp is labelled Renewal
362
+ boundary rather than promising a future charge.
363
+
364
+ The billing request uses a verified Chromium-compatible User-Agent because the
365
+ service challenged the generic authswitch User-Agent in controlled comparisons.
366
+ This is a fixed HTTP request profile and requires no local browser installation. Other
367
+ Codex status endpoints retain the authswitch User-Agent. If the billing endpoint
368
+ still returns a Cloudflare verification challenge, that is reported explicitly as
369
+ unavailable billing and a partial result, without treating it as a rejected login.
370
+ Authswitch does not
371
+ import browser cookies, replay desktop integrity state, refresh credentials or
372
+ switch accounts to bypass the challenge. A plan from the stored login remains
373
+ unverified when the live usage request fails. API-key logins and keyring credentials
374
+ cannot provide these ChatGPT subscription metrics.
375
+
376
+ ## Harness adapters
377
+
378
+ The CLI operates on `IAuthHarness`, with opaque account IDs scoped to each harness.
379
+ Codex, OpenCode and Claude Code are built-in adapters. Adding an adapter does not require
380
+ changing command routing, save prompts, account pickers or status rendering:
381
+
382
+ ```typescript
383
+ import { AuthSwitchCli, CodexHarness } from '@modelprofile.com/authswitch';
384
+ import { AnotherHarness } from './anotherharness.js'; // your IAuthHarness implementation
385
+
386
+ const cli = new AuthSwitchCli([new CodexHarness(), new AnotherHarness()]);
387
+ process.exitCode = await cli.run(process.argv.slice(2));
388
+ ```
389
+
390
+ Each adapter owns discovery, identity matching, save verification, credential
391
+ storage, switching and process lifecycle, live status APIs, and diagnostics.
392
+ `readState()` and `readAccountStatus()` must be read-only. `switchAccount()` must
393
+ preserve and verify the outgoing credential without relying on the interactive
394
+ save prompt. `removeAccount()` removes only the saved copy. Status facts carry
395
+ provider-specific labels and units; unsupported capabilities are explicit. Adapter
396
+ labels, details, facts and outcomes contain plain text; smartconsole owns terminal
397
+ formatting for both the guide and dashboard.
398
+ Credentials, email assumptions and Codex enrollment metadata stay out of the
399
+ shared interface. The existing `CodexSwitcher` API and single-switcher CLI
400
+ constructor remain available.
401
+
402
+ An account's optional `slotId` identifies the credential it replaces. Accounts in
403
+ different slots may be active simultaneously. `saveCurrent({ keepActive, accountId })`
404
+ must target the selected active account; omitting `accountId` is allowed only when
405
+ the adapter has one unambiguous active login. `usageWindows[].resetAt` may be null
406
+ when the provider explicitly reports no scheduled reset; missing windows stay omitted.
407
+
408
+ Adapters may implement `preuseAccount(accountId, options)` as an optional
409
+ quota-consuming capability. `IHarnessPreuseOptions` carries the prompt, optional
410
+ model identifier and cancellation signal; `IHarnessPreuseResult` returns the
411
+ selected model and optional input/output/total token counts. The adapter owns
412
+ credential selection and inference and must leave the active login unchanged.
413
+ Unsupported adapters reject the command before account selection. Prompt results
414
+ never contain credentials or raw provider responses.
415
+
416
+ Adapters can also return an optional `IHarnessAccountStatus.summary` with a
417
+ subscription plan and its live/stored provenance, labelled usage windows with
418
+ duration, used percentage and UTC reset time, and available reset credits with
419
+ optional expiry details. This harness-neutral data supports shared presentation
420
+ without parsing provider-specific fact strings. Missing data stays unknown;
421
+ explicit zeroes remain zero. A successful reset-detail lookup supplies the summary
422
+ count; if it fails, the limits response can still supply availability.
423
+
424
+ Each usage window may set `scope: 'account' | 'feature'` (default `account`).
425
+ Adapters mark model-specific or feature-specific quotas as `feature` so those
426
+ quotas remain available in JSON and details without distorting the general
427
+ account summary. Weekly windows are identified by `durationSeconds: 604800`;
428
+ neither window labels nor plan names are parsed to infer limits.
429
+
430
+ Optional `summary.billing` contains provider-reported `hasActiveSubscription`,
431
+ `autoRenew`, `renewsAt`, `cancelsAt`, and `expiresAt`, independent of the plan name.
432
+ Dates are ISO UTC and omitted
433
+ when unknown. They must come from a live billing source; entitlement expiry alone
434
+ does not establish renewal or cancellation. Codex omits this field if its billing
435
+ lookup fails and supplies a corresponding problem.
436
+
437
+ Facts may supply a `section` label for grouped comparison and a `summaryKey`
438
+ (`subscription`, `billing`, `usageWindows`, `resets`, or `resetDetails`) when the
439
+ fact duplicates a structured field. The list hides such a fact only if that field
440
+ is displayed; JSON retains every fact. Adapters without this metadata continue to
441
+ display their facts in Additional information.
442
+
443
+ With multiple adapters, the guide first asks which harness to manage and provides
444
+ an action to choose another. Unqualified mutation commands prompt for a harness;
445
+ scripts must qualify them. Account references never resolve across harnesses.
446
+
447
+ ## Where things are stored
448
+
449
+ | What | Where |
450
+ | --- | --- |
451
+ | Stashed credentials | `~/.authswitch/codex/<email>/auth.json` (mode 0600, in a 0700 directory) |
452
+ | Stash metadata and enrollments | `~/.authswitch/codex/<email>/stash.json` |
453
+ | Codex' active credential | `$CODEX_HOME/auth.json`, default `~/.codex/auth.json` |
454
+ | Codex' remote-control enrollments | the `remote_control_enrollments` table in `$CODEX_HOME/state_<n>.sqlite` |
455
+
456
+ `CODEX_HOME` and `AUTHSWITCH_HOME` both override their defaults, which is also how the test suite runs against a throwaway directory. `AUTHSWITCH_CODEX_BIN` points at a specific `codex` executable when it is not on `PATH`, and `NO_COLOR` turns off colouring (as does piping the output anywhere that is not a terminal).
457
+
458
+ Account identity comes from the `id_token` inside the credential — the email claim names the stash, the account id ties it to its enrollments. The token is decoded, never verified, and never printed: `authswitch` only ever reports emails, plan names and server names.
459
+
460
+ ## Limits
461
+
462
+ **Keyring-backed credentials cannot be switched.** Codex can keep credentials in an OS keyring instead of `auth.json`, in which case there is no file to move. `authswitch` detects this and refuses rather than half-working; `doctor` reports it as `keyring (not switchable)`. Switch Codex to file storage via `auth_credentials_store_mode` in `config.toml` and log in again to use this tool.
463
+
464
+ **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.
465
+
466
+ **The Codex stash is keyed by email.** Two Codex workspaces with the same email
467
+ cannot both be saved under that key. Their account IDs are distinguished during
468
+ listing and identity checks, and a collision refuses to overwrite either login.
469
+ Account IDs in the shared harness interface do not have this email restriction.
470
+
471
+ **A stash is a credential.** `~/.authswitch` holds live refresh tokens. It is written owner-only, but it is not encrypted, and it belongs in the same threat model as `~/.codex/auth.json` itself.
472
+
473
+ ## Development
474
+
475
+ ```bash
476
+ pnpm install
477
+ pnpm build
478
+ pnpm test
479
+ ```
480
+
481
+ The test suite builds a fixture `CODEX_HOME` with a real SQLite state database and exercises the full stash/login/restore cycle, including the case where a fresh login has removed an account's enrollment row. It never touches the developer's own Codex installation.
482
+
483
+ ## License and Legal Information
484
+
485
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
486
+
487
+ **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
488
+
489
+ ### Trademarks
490
+
491
+ This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
492
+
493
+ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
494
+
495
+ ### Company Information
496
+
497
+ Task Venture Capital GmbH<br>
498
+ Registered at District Court Bremen HRB 35230 HB, Germany
499
+
500
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
501
+
502
+ By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
+ */
4
+ export const commitinfo = {
5
+ name: '@modelprofile.com/authswitch',
6
+ version: '2.1.2',
7
+ description: 'Manage Codex, OpenCode and Claude Code accounts with guided switching and live usage status'
8
+ }
package/ts/accounts.ts ADDED
@@ -0,0 +1,78 @@
1
+ import type { IAuthHarness, IHarnessAccount, IHarnessAccountStatus, IHarnessState, IHarnessStatusSummary } from './interfaces.harness.js';
2
+ import { plainText } from './formatting.js';
3
+
4
+ export interface IAccountRow {
5
+ harness: IAuthHarness;
6
+ account: IHarnessAccount;
7
+ status?: IHarnessAccountStatus;
8
+ }
9
+ export const duration = (secondsArg: number): string => secondsArg % 86400 === 0 ? `${secondsArg / 86400}d` : secondsArg % 3600 === 0 ? `${secondsArg / 3600}h` : `${secondsArg / 60}m`;
10
+ /** A countdown is a snapshot of the provider's deadline, not proof a quota has reset. */
11
+ export const until = (timestampArg: string | null, nowArg: number): string => {
12
+ if (timestampArg === null) return 'Not scheduled';
13
+ const remaining = Date.parse(timestampArg) - nowArg;
14
+ if (!Number.isFinite(remaining)) return 'Unavailable';
15
+ if (remaining <= 0) return 'due';
16
+ const minutes = Math.floor(remaining / 60000);
17
+ if (!minutes) return '<1min';
18
+ const days = Math.floor(minutes / 1440);
19
+ const hours = Math.floor(minutes % 1440 / 60);
20
+ return [days ? `${days}d` : '', hours ? `${hours}h` : '', minutes % 60 ? `${minutes % 60}min` : ''].filter(Boolean).join(' ');
21
+ };
22
+ export const accountPlan = (rowArg: IAccountRow): string => {
23
+ const subscription = rowArg.status?.summary?.subscription;
24
+ return subscription ? `${plainText(subscription.plan)}${subscription.source === 'stored' ? ' (stored; unverified)' : ' (live)'}` : rowArg.status ? 'Unavailable' : 'Loading';
25
+ };
26
+ type TUsageWindow = NonNullable<IHarnessStatusSummary['usageWindows']>[number];
27
+ type TAccountStatusRow = Pick<IAccountRow, 'status'>;
28
+ export const usagePeriod = (windowArg: TUsageWindow): string => windowArg.durationSeconds === 604800 ? 'Weekly' : duration(windowArg.durationSeconds);
29
+ /** Order by scope and actual duration: a provider's primary slot need not be five hours. */
30
+ export const orderedUsageWindows = (windowsArg: readonly TUsageWindow[] = []): TUsageWindow[] => [...windowsArg].sort((left, right) =>
31
+ Number(left.scope === 'feature') - Number(right.scope === 'feature') ||
32
+ Number(left.durationSeconds !== 604800) - Number(right.durationSeconds !== 604800) || right.durationSeconds - left.durationSeconds);
33
+ export const accountUsageWindows = (rowArg: TAccountStatusRow): TUsageWindow[] | undefined => {
34
+ const windows = rowArg.status?.summary?.usageWindows;
35
+ return windows === undefined ? undefined : orderedUsageWindows(windows.filter(window => window.scope !== 'feature'));
36
+ };
37
+ export const accountUsage = (rowArg: TAccountStatusRow): string => {
38
+ const windows = accountUsageWindows(rowArg);
39
+ return windows?.length ? `${usagePeriod(windows[0])}: ${windows[0].usedPercent}% used` : windows ? 'None reported' : rowArg.status ? 'Unavailable' : 'Loading';
40
+ };
41
+ export const accountResets = (rowArg: IAccountRow): string => rowArg.status?.summary?.resets === undefined ? rowArg.status ? 'Unavailable' : 'Loading' : String(rowArg.status.summary.resets.available);
42
+ export const accountNextReset = (rowArg: TAccountStatusRow, nowArg = Date.now()): string => {
43
+ const window = accountUsageWindows(rowArg)?.[0];
44
+ return window ? until(window.resetAt, nowArg) : 'Unavailable';
45
+ };
46
+ export const accountQuotaSummary = (rowArg: TAccountStatusRow, nowArg: number): string => {
47
+ const windows = accountUsageWindows(rowArg);
48
+ return [accountUsage(rowArg), ...(windows?.length ? [
49
+ `Reset in: ${accountNextReset(rowArg, nowArg)}`,
50
+ ...windows.slice(1).filter(window => window.usedPercent >= 100).map(window => `${usagePeriod(window)} exhausted\nResets in: ${until(window.resetAt, nowArg)}`),
51
+ ] : [])].join('\n');
52
+ };
53
+ export const accountState = (rowArg: IAccountRow): string => `${rowArg.account.isActive ? '* active, ' : ''}${rowArg.account.isStashed ? 'saved' : 'not saved'}`;
54
+ export const accountDetails = (rowArg: IAccountRow): string => [
55
+ plainText(rowArg.account.label), accountState(rowArg),
56
+ ...(rowArg.account.savedAt ? ['Saved: ' + plainText(rowArg.account.savedAt)] : []),
57
+ ...rowArg.account.details.map(plainText),
58
+ ...(rowArg.status?.facts.map(factArg => `${plainText(factArg.label)}: ${plainText(factArg.value)}`) ?? ['Loading account status…']),
59
+ ...(rowArg.status?.problems.map(problemArg => `Unavailable: ${plainText(problemArg)}`) ?? []),
60
+ ].join('\n');
61
+
62
+ /** Read-only, bounded parallel status lookup. No account is switched to inspect it. */
63
+ export const readAccountRows = async (
64
+ harnessArg: IAuthHarness,
65
+ optionsArg: { signal?: AbortSignal; onUpdate?: (rowsArg: IAccountRow[], stateArg: IHarnessState) => void | Promise<void> } = {},
66
+ ): Promise<{ rows: IAccountRow[]; state: IHarnessState }> => {
67
+ const state = await harnessArg.readState();
68
+ const rows = state.accounts.map(account => ({ harness: harnessArg, account } as IAccountRow));
69
+ await optionsArg.onUpdate?.(rows, state);
70
+ for (let index = 0; index < rows.length && !optionsArg.signal?.aborted; index += 4) {
71
+ await Promise.all(rows.slice(index, index + 4).map(async row => {
72
+ try { row.status = await harnessArg.readAccountStatus(row.account.id); }
73
+ catch { row.status = { facts: [], problems: ['Could not read account status.'] }; }
74
+ if (!optionsArg.signal?.aborted) await optionsArg.onUpdate?.(rows, state);
75
+ }));
76
+ }
77
+ return { rows, state };
78
+ };