oc-codex-multi-auth 6.23.0 → 6.24.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 (84) hide show
  1. package/README.md +67 -3
  2. package/dist/index.d.ts +7 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +35 -15
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/codex-usage.d.ts +35 -0
  7. package/dist/lib/codex-usage.d.ts.map +1 -1
  8. package/dist/lib/codex-usage.js +51 -0
  9. package/dist/lib/codex-usage.js.map +1 -1
  10. package/dist/lib/config.d.ts +5 -3
  11. package/dist/lib/config.d.ts.map +1 -1
  12. package/dist/lib/config.js +2 -0
  13. package/dist/lib/config.js.map +1 -1
  14. package/dist/lib/opencode-v2-provider.d.ts +2 -0
  15. package/dist/lib/opencode-v2-provider.d.ts.map +1 -0
  16. package/dist/lib/opencode-v2-provider.js +4 -0
  17. package/dist/lib/opencode-v2-provider.js.map +1 -0
  18. package/dist/lib/opencode-v2-rpc.d.ts +27 -0
  19. package/dist/lib/opencode-v2-rpc.d.ts.map +1 -0
  20. package/dist/lib/opencode-v2-rpc.js +16 -0
  21. package/dist/lib/opencode-v2-rpc.js.map +1 -0
  22. package/dist/lib/opencode-v2-status.d.ts +17 -0
  23. package/dist/lib/opencode-v2-status.d.ts.map +1 -0
  24. package/dist/lib/opencode-v2-status.js +91 -0
  25. package/dist/lib/opencode-v2-status.js.map +1 -0
  26. package/dist/lib/opencode-v2-tui.d.ts +4 -0
  27. package/dist/lib/opencode-v2-tui.d.ts.map +1 -0
  28. package/dist/lib/opencode-v2-tui.js +105 -0
  29. package/dist/lib/opencode-v2-tui.js.map +1 -0
  30. package/dist/lib/opencode-v2.d.ts +11 -0
  31. package/dist/lib/opencode-v2.d.ts.map +1 -0
  32. package/dist/lib/opencode-v2.js +235 -0
  33. package/dist/lib/opencode-v2.js.map +1 -0
  34. package/dist/lib/prompts/codex.d.ts.map +1 -1
  35. package/dist/lib/prompts/codex.js +4 -0
  36. package/dist/lib/prompts/codex.js.map +1 -1
  37. package/dist/lib/quota-capacity.d.ts +4 -0
  38. package/dist/lib/quota-capacity.d.ts.map +1 -0
  39. package/dist/lib/quota-capacity.js +45 -0
  40. package/dist/lib/quota-capacity.js.map +1 -0
  41. package/dist/lib/quota-overview.d.ts +13 -27
  42. package/dist/lib/quota-overview.d.ts.map +1 -1
  43. package/dist/lib/quota-overview.js +59 -99
  44. package/dist/lib/quota-overview.js.map +1 -1
  45. package/dist/lib/quota-recovery.d.ts +5 -0
  46. package/dist/lib/quota-recovery.d.ts.map +1 -0
  47. package/dist/lib/quota-recovery.js +66 -0
  48. package/dist/lib/quota-recovery.js.map +1 -0
  49. package/dist/lib/request/fetch-helpers.d.ts +1 -1
  50. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  51. package/dist/lib/request/fetch-helpers.js +1 -1
  52. package/dist/lib/request/fetch-helpers.js.map +1 -1
  53. package/dist/lib/schemas.d.ts +3 -1
  54. package/dist/lib/schemas.d.ts.map +1 -1
  55. package/dist/lib/schemas.js +3 -2
  56. package/dist/lib/schemas.js.map +1 -1
  57. package/dist/lib/storage/state.d.ts +8 -0
  58. package/dist/lib/storage/state.d.ts.map +1 -1
  59. package/dist/lib/storage/state.js +47 -20
  60. package/dist/lib/storage/state.js.map +1 -1
  61. package/dist/lib/tools/codex-limits.d.ts.map +1 -1
  62. package/dist/lib/tools/codex-limits.js +49 -3
  63. package/dist/lib/tools/codex-limits.js.map +1 -1
  64. package/dist/lib/tui-quota-cache.d.ts +1 -0
  65. package/dist/lib/tui-quota-cache.d.ts.map +1 -1
  66. package/dist/lib/tui-quota-cache.js +2 -0
  67. package/dist/lib/tui-quota-cache.js.map +1 -1
  68. package/dist/lib/tui-quota-overview.d.ts.map +1 -1
  69. package/dist/lib/tui-quota-overview.js +4 -0
  70. package/dist/lib/tui-quota-overview.js.map +1 -1
  71. package/dist/lib/tui-status-slot.d.ts +3 -0
  72. package/dist/lib/tui-status-slot.d.ts.map +1 -0
  73. package/dist/lib/tui-status-slot.js +27 -0
  74. package/dist/lib/tui-status-slot.js.map +1 -0
  75. package/dist/lib/tui-status.d.ts +1 -0
  76. package/dist/lib/tui-status.d.ts.map +1 -1
  77. package/dist/lib/tui-status.js +1 -0
  78. package/dist/lib/tui-status.js.map +1 -1
  79. package/dist/tui.d.ts +10 -3
  80. package/dist/tui.d.ts.map +1 -1
  81. package/dist/tui.js +22 -1
  82. package/dist/tui.js.map +1 -1
  83. package/package.json +5 -3
  84. package/scripts/install-oc-codex-multi-auth-core.js +81 -5
package/README.md CHANGED
@@ -9,6 +9,8 @@
9
9
 
10
10
  Use it when you want OpenCode to run Codex-style coding workflows from your own ChatGPT subscription while keeping accounts visible, switchable, health-checked, and recoverable from the terminal.
11
11
 
12
+ **OpenCode V2 is supported (2.0.16+).** The V2 adapter uses the existing OAuth account pool and Codex routing pipeline; the V1 entrypoint remains available for OpenCode 1.18.29+. See [OpenCode V2 installation](#opencode-v2) for setup and login instructions.
13
+
12
14
  <img width="1227" height="702" alt="oc-codex-multi-auth OpenCode plugin dashboard for ChatGPT OAuth, Codex routing, and multi-account health" src="https://github.com/user-attachments/assets/b796eb2f-282e-468a-ba6a-acadf09d731b" />
13
15
 
14
16
 
@@ -74,6 +76,57 @@ The plugin does not replace OpenCode. OpenCode remains the host; this package in
74
76
 
75
77
  ## Installation
76
78
 
79
+ ### OpenCode V2
80
+
81
+ The V2 compatibility adapter targets OpenCode **2.0.16 or newer** and reuses the
82
+ existing OAuth account pool, refresh, rotation, retry, and Codex request pipeline.
83
+ Register the package in `opencode.json(c)`:
84
+
85
+ ```json
86
+ {
87
+ "$schema": "https://opencode.ai/config.json",
88
+ "plugins": ["oc-codex-multi-auth"]
89
+ }
90
+ ```
91
+
92
+ For a working checkout, use its absolute directory path in `plugins`, then run
93
+ `npm install` and `npm run build` in that checkout. The V2 terminal automatically
94
+ loads the package's quota UI through its `./tui` export.
95
+
96
+ The installer also accepts `--v2` to register the plugin without rewriting the
97
+ model catalog. It refuses to modify an existing `opencode.jsonc` or convert a
98
+ config with V1 `plugin` entries: edit the JSONC `plugins` list directly, or keep
99
+ separate V1 and V2 configurations so the V1 registration is not lost. Restart
100
+ the background service after installing or rebuilding:
101
+
102
+ ```bash
103
+ opencode service restart
104
+ ```
105
+
106
+ Run `opencode auth login` from your project directory and select **OpenAI** →
107
+ **Codex OAuth (Add account — ChatGPT Plus/Pro)**. Repeat for each account, using
108
+ a private browser window or switching browser accounts to select a different login.
109
+ The built-in **ChatGPT Pro/Plus (browser)** method does not run the plugin's add-account flow.
110
+ The plugin's **Device Code**, **Open URL Manually**, and **Manual URL Paste**
111
+ methods are also available through login or `/connect`. Each adds to the pool;
112
+ logging into the same account updates its existing entry. Pools are per-project
113
+ by default, so log in from the directory where you use OpenCode. Existing
114
+ plugin accounts remain usable; V2's own credentials are managed through its
115
+ integration API. Use the plugin's `codex-list` and `codex-switch` tools to manage
116
+ its pool. V2 normalizes tool names, so these appear as `codex_list`, `codex_switch`,
117
+ and so on. The **Codex accounts** sidebar section lists the pool and marks its
118
+ active account. Use `/codex-accounts` or **Codex accounts** in the command palette
119
+ to view the list even when the sidebar is hidden. The quota details command is
120
+ also available in the command palette.
121
+
122
+ Existing supported V1 provider/model config can remain in place. The adapter
123
+ uses HTTP Responses through the existing plugin transport. V1's interactive
124
+ multi-account login menu and session-repair client calls are replaced by the
125
+ V2 connection UI and host session handling. The V1 entrypoint remains available
126
+ for OpenCode **1.18.29+**.
127
+
128
+ ### OpenCode V1
129
+
77
130
  <details open>
78
131
  <summary><b>For Humans</b></summary>
79
132
 
@@ -244,7 +297,7 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
244
297
  | `codex-switch` | How do I move to a different saved account? |
245
298
  | `codex-warm` | How do I start every account's usage window now (stagger quota cooldowns)? |
246
299
  | `codex-status` | Which account, model family, and routing state are active? |
247
- | `codex-limits` | What quota or rate-limit state is visible now? |
300
+ | `codex-limits` | What quota is visible now, per account and across the pool? |
248
301
  | `codex-reset` | Do I have a banked rate-limit reset credit, and how do I redeem it? |
249
302
  | `codex-dashboard` | What does a read-only snapshot of account eligibility, retry budgets, and refresh queue health show? |
250
303
  | `codex-pool` | Which accounts are preferred for each model, and how do I change them? |
@@ -288,6 +341,7 @@ Most of these also run as a **direct CLI** with no agent or model involvement, s
288
341
  - Business workspace memberships and Personal accounts keep separate usage and quota windows. Business members sharing one workspace are distinguished by their member/seat identity, so their usage is not collapsed into one row.
289
342
  - An account identifies itself by its own ChatGPT email and the last 6 characters of its account id, with the email masked when `maskEmail` is on. An account id names a ChatGPT workspace and every member of a Business workspace shares it, so a record that also carries a member/seat id prints a short excerpt of that as `seat:`. The excerpt is a 6-character tail where that is enough to tell the listed accounts apart. Where it is not, it widens, moves to where those ids first differ, or joins two short excerpts with `..` - real member ids are long, share a leading prefix, and differ in more than one place, so a tail alone often cannot separate them. Where no excerpt that short can separate them, `seat:` is instead an **opaque hash prefix** such as `719f78b5`: it identifies the seat and stays stable, but it is not part of the member id and cannot be matched against anything ChatGPT shows you. Whichever form it takes, two distinct seats never render the same `seat:` and a `seat:` is never longer than 32 characters. A record with no member id renders exactly as before. The OAuth id_token also lists the API-platform organizations the login belongs to; those are not ChatGPT workspaces and are never used to name an account, so logging in clears a label left behind by one. A label you set with `codex-label` is always kept.
290
343
  - The ChatGPT plan (`Free`, `Plus`, `Pro`, `Business`, `Business Premium`, `Enterprise`) is read from the access token, refreshed on every token refresh, and shown by `codex-list` and `codex-status`. `codex-limits` and the TUI read the plan live from the usage endpoint and name it the same way. An unrecognized plan is reported verbatim rather than renamed.
344
+ - `codex-limits` and the standalone `limits` CLI name what one of that plan's seats is worth beside the others (`Plan: Pro (20x)`) and close with what the pool holds between them (`Pool: 93% used of 81x across 11 accounts`). The percentage is a **weighted** mean over exactly that `81x`, since a spent Pro seat costs the pool twenty times what a spent Plus seat does, and an account whose usage could not be read is left out of both figures. A plan that publishes no ratio carries no badge but still weighs one baseline seat. See [docs/tools-and-cli.md](docs/tools-and-cli.md#what-limits-reports) and [docs/plan-allotments.md](docs/plan-allotments.md).
291
345
 
292
346
  ---
293
347
 
@@ -413,9 +467,17 @@ does at 50%, so an unweighted average would describe a pool nobody has. The
413
467
  per-plan ratios are listed in [docs/plan-allotments.md](docs/plan-allotments.md),
414
468
  and `"allotment": true` shows what they add up to.
415
469
 
470
+ For just that total and its recovery forecast, use `"layout": "total"`,
471
+ `"recovery": "all"`, and `"allotment": false`. This reads, for example,
472
+ `25% +1% in 3h, +12% in 3d, +5% in 4d`: each positive figure is incremental
473
+ capacity returned in percentage points, even with `quotaDisplay: "used"`.
474
+ See the [forecast semantics](docs/configuration.md#what-the-line-says).
475
+
416
476
  `mode` also accepts a list, and the line then alternates between those screens
417
- every `rotateMs` (default 5000). The third screen, `resets`, appears only once
418
- every account is spent and lists the banked reset credits worth redeeming,
477
+ every `rotateMs` (default 5000). The third screen, `resets`, appears by default
478
+ once every readable account is spent. Set `resetsMinUsedPercent` (0-100) to
479
+ show it earlier, for example at 90% total weighted usage. It lists known
480
+ applicable banked reset credits,
419
481
  latest reset first - redeeming one on an account that renews by itself tomorrow
420
482
  throws it away:
421
483
 
@@ -444,6 +506,8 @@ Add the object to `~/.opencode/openai-codex-auth-config.json`. It is read from
444
506
  that file only - a display preference belongs to a person, not to a shell - and
445
507
  the status line re-reads it while sessions are open, so an edit takes effect
446
508
  within a couple of seconds without a restart.
509
+ New plugin code still needs a one-time process restart after an upgrade;
510
+ subsequent changes to these settings reload live.
447
511
 
448
512
  ### Desktop quota notifications
449
513
 
package/dist/index.d.ts CHANGED
@@ -41,5 +41,11 @@ import type { Plugin } from "@opencode-ai/plugin";
41
41
  */
42
42
  export declare const OpenAIOAuthPlugin: Plugin;
43
43
  export declare const OpenAIAuthPlugin: Plugin;
44
- export default OpenAIOAuthPlugin;
44
+ declare const _default: {
45
+ id: string;
46
+ server: Plugin;
47
+ /** V2 loads the same package through setup instead of the V1 server hook. */
48
+ setup(context: import("@opencode/plugin").Plugin.Context): Promise<() => Promise<void>>;
49
+ };
50
+ export default _default;
45
51
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAsV/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA+pJ/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,OAAO,KAAK,EAAE,MAAM,EAAsB,MAAM,qBAAqB,CAAC;AAsVtE;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAA2E,CAAC;AA+qJ5G,eAAO,MAAM,gBAAgB,QAAoB,CAAC;;;;IAKjD,6EAA6E;mBACxD,OAAO,kBAAkB,EAAE,MAAM,CAAC,OAAO;;AAJ/D,wBAQE"}
package/dist/index.js CHANGED
@@ -179,8 +179,16 @@ function resolveOpenAIBaseURL() {
179
179
  * }
180
180
  * ```
181
181
  */
182
- export const OpenAIOAuthPlugin = async ({ client }) => {
183
- initLogger(client);
182
+ export const OpenAIOAuthPlugin = async ({ client }) => createPluginRuntime({ client });
183
+ /**
184
+ * Shared request/account runtime; V2 has no V1 client or host auth.json.
185
+ *
186
+ * Not exported: V1 hosts call every function this module exports as a plugin,
187
+ * so an exported factory would boot a second runtime beside the real one. The
188
+ * V2 entry receives it through `setup` instead.
189
+ */
190
+ async function createPluginRuntime({ client, directory = process.cwd() }) {
191
+ initLogger(client ?? {});
184
192
  let customBaseURLWarningShown = false;
185
193
  let customBaseURLErrorShown = false;
186
194
  let cachedAccountManager = null;
@@ -456,7 +464,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
456
464
  };
457
465
  const showToast = async (message, variant = "success", options) => {
458
466
  try {
459
- await client.tui.showToast({
467
+ await client?.tui.showToast({
460
468
  body: {
461
469
  message,
462
470
  variant,
@@ -1478,6 +1486,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1478
1486
  if (event.type === "server.instance.disposed") {
1479
1487
  quotaMonitor.dispose();
1480
1488
  disposeAccountsWatcher();
1489
+ await cachedAccountManager?.flushPendingSave();
1490
+ cachedAccountManager?.disposeShutdownHandler();
1481
1491
  return;
1482
1492
  }
1483
1493
  // Handle TUI account selection events
@@ -1585,8 +1595,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1585
1595
  };
1586
1596
  const startupPluginConfig = loadPluginConfig();
1587
1597
  const startupPerProjectAccounts = getPerProjectAccounts(startupPluginConfig);
1588
- setStoragePath(startupPerProjectAccounts ? process.cwd() : null);
1589
- await backfillHostOpenAIAuthFromPool();
1598
+ setStoragePath(startupPerProjectAccounts ? directory : null);
1599
+ if (client)
1600
+ await backfillHostOpenAIAuthFromPool();
1590
1601
  quotaMonitor.start();
1591
1602
  return {
1592
1603
  event: eventHandler,
@@ -1635,7 +1646,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1635
1646
  let perProjectAccounts = getPerProjectAccounts(pluginConfig);
1636
1647
  let storageTransition;
1637
1648
  const activeFetches = new Set();
1638
- setStoragePath(perProjectAccounts ? process.cwd() : null);
1649
+ setStoragePath(perProjectAccounts ? directory : null);
1639
1650
  const authFallback = auth.type === "oauth" ? auth : undefined;
1640
1651
  // Prefer multi-account auth metadata when available, but still handle
1641
1652
  // plain OAuth credentials (for OpenCode versions that inject internal
@@ -1744,8 +1755,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1744
1755
  prewarmOpenCodeCodexPrompt();
1745
1756
  }
1746
1757
  }
1747
- const recoveryHook = sessionRecoveryEnabled
1748
- ? createSessionRecoveryHook({ client, directory: process.cwd() }, { sessionRecovery: true, autoResume: autoResumeEnabled })
1758
+ const recoveryHook = sessionRecoveryEnabled && client
1759
+ ? createSessionRecoveryHook({ client, directory }, { sessionRecovery: true, autoResume: autoResumeEnabled })
1749
1760
  : null;
1750
1761
  const pluginOrigin = getPluginOrigin();
1751
1762
  if (pluginOrigin && !startupOriginRecorded) {
@@ -1800,7 +1811,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1800
1811
  await Promise.all(activeFetches);
1801
1812
  // Drain the old pool's pending write before changing the global path.
1802
1813
  await cachedAccountManager?.flushPendingSave();
1803
- setStoragePath(currentPerProjectAccounts ? process.cwd() : null);
1814
+ setStoragePath(currentPerProjectAccounts ? directory : null);
1804
1815
  invalidateAccountManagerCache();
1805
1816
  perProjectAccounts = currentPerProjectAccounts;
1806
1817
  })().finally(() => { storageTransition = undefined; });
@@ -3127,7 +3138,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3127
3138
  const authPluginConfig = loadPluginConfig();
3128
3139
  applyUiRuntimeFromConfig(authPluginConfig);
3129
3140
  const authPerProjectAccounts = getPerProjectAccounts(authPluginConfig);
3130
- setStoragePath(authPerProjectAccounts ? process.cwd() : null);
3141
+ setStoragePath(authPerProjectAccounts ? directory : null);
3131
3142
  const accounts = [];
3132
3143
  // Programmatic input, not a menu choice: a headless caller or
3133
3144
  // script passes it to say "never launch a browser". Ignoring it
@@ -3886,7 +3897,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3886
3897
  const manualBrowserPluginConfig = loadPluginConfig();
3887
3898
  applyUiRuntimeFromConfig(manualBrowserPluginConfig);
3888
3899
  const manualBrowserPerProjectAccounts = getPerProjectAccounts(manualBrowserPluginConfig);
3889
- setStoragePath(manualBrowserPerProjectAccounts ? process.cwd() : null);
3900
+ setStoragePath(manualBrowserPerProjectAccounts ? directory : null);
3890
3901
  const session = await startLoopbackFlow({ openBrowser: false });
3891
3902
  if (session.type === "unavailable") {
3892
3903
  const message = unavailableMessage(session.lifecycle);
@@ -3934,7 +3945,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3934
3945
  const devicePluginConfig = loadPluginConfig();
3935
3946
  applyUiRuntimeFromConfig(devicePluginConfig);
3936
3947
  const devicePerProjectAccounts = getPerProjectAccounts(devicePluginConfig);
3937
- setStoragePath(devicePerProjectAccounts ? process.cwd() : null);
3948
+ setStoragePath(devicePerProjectAccounts ? directory : null);
3938
3949
  const started = await createDeviceCodeSession();
3939
3950
  if (started.type === "failed") {
3940
3951
  return {
@@ -3972,7 +3983,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3972
3983
  const manualPluginConfig = loadPluginConfig();
3973
3984
  applyUiRuntimeFromConfig(manualPluginConfig);
3974
3985
  const manualPerProjectAccounts = getPerProjectAccounts(manualPluginConfig);
3975
- setStoragePath(manualPerProjectAccounts ? process.cwd() : null);
3986
+ setStoragePath(manualPerProjectAccounts ? directory : null);
3976
3987
  const { pkce, state, url } = await createAuthorizationFlow();
3977
3988
  return buildManualOAuthFlow(pkce, url, state, false);
3978
3989
  },
@@ -3981,7 +3992,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3981
3992
  },
3982
3993
  tool: createToolRegistry(ctx),
3983
3994
  };
3984
- };
3995
+ }
3996
+ ;
3985
3997
  export const OpenAIAuthPlugin = OpenAIOAuthPlugin;
3986
- export default OpenAIOAuthPlugin;
3998
+ export default {
3999
+ id: "oc-codex-multi-auth",
4000
+ server: OpenAIOAuthPlugin,
4001
+ /** V2 loads the same package through setup instead of the V1 server hook. */
4002
+ async setup(context) {
4003
+ const { setupV2 } = await import("./lib/opencode-v2.js");
4004
+ return setupV2(context, createPluginRuntime);
4005
+ },
4006
+ };
3987
4007
  //# sourceMappingURL=index.js.map