codex-multi-auth 2.17.3 → 2.18.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.17.3",
3
+ "version": "2.18.0",
4
4
  "description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
5
5
  "interface": {
6
6
  "composerIcon": "./assets/codex-multi-auth-icon.svg"
package/README.md CHANGED
@@ -5,10 +5,14 @@
5
5
  [![CI](https://github.com/ndycode/codex-multi-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/ndycode/codex-multi-auth/actions/workflows/ci.yml)
6
6
  [![MIT license](https://img.shields.io/npm/l/codex-multi-auth.svg)](LICENSE)
7
7
 
8
+ <img width="1270" height="729" alt="codex-multi-auth terminal dashboard for Codex CLI multi-account OAuth account status" src="https://github.com/user-attachments/assets/0cecb77e-a6d3-432a-ba48-3577db0c7093" />
9
+
8
10
  `codex-multi-auth` is a multi-account OAuth manager for the official `@openai/codex` CLI. It keeps a named pool of ChatGPT sign-ins on your machine, lets you switch the active account, and — when you run Codex through its optional wrapper — rotates accounts across live requests through a loopback-only proxy. It never installs or shadows a `codex` binary; the official OpenAI install keeps owning that command.
9
11
 
10
12
  Use it when one Codex account is not enough: you hit rate limits or quota windows, you split work across organizations and workspaces, you want per-project account pools, or you run unattended agents that need JSON diagnostics and safe repair commands instead of one opaque auth file.
11
13
 
14
+ Using OpenCode rather than the Codex CLI? The sibling project [`oc-codex-multi-auth`](https://github.com/ndycode/oc-codex-multi-auth) is the plugin that manages the same kind of account pool there.
15
+
12
16
  ## What it does
13
17
 
14
18
  - **Account pool** — OAuth login for multiple ChatGPT accounts, stored locally under `~/.codex/multi-auth` (files `0600`, directories `0700`), with per-project pools under `projects/<project-key>/`.
@@ -71,7 +75,7 @@ mcodex # same wrapper, shorter name
71
75
  | Accounts | `login`, `list`, `status`, `switch <i>`, `unpin`, `workspace` | sign in, inspect the pool, pin or switch the active account, pick workspaces |
72
76
  | Selection | `forecast`, `best`, `why-selected`, `check`, `limits`, `models` | preview, explain, and probe which account serves the next request |
73
77
  | Runtime | `rotation status` `enable` `disable` `bind-app` `unbind-app` `reset-runtime` `reset-rate-limits` | inspect and control live rotation and the app bind |
74
- | Repair | `fix`, `doctor`, `verify`, `verify-flagged`, `uninstall` | diagnose and repair storage, flagged accounts, and install residue |
78
+ | Repair | `fix`, `doctor`, `report`, `verify`, `verify-flagged`, `uninstall` | diagnose and repair storage, flagged accounts, and install residue; machine-readable health report |
75
79
  | Governance | `usage`, `budget`, `account`, `monitor`, `history`, `resets` | usage ledger, budget limits, tags/weights/pause/drain, snapshots |
76
80
  | Local clients | `bridge`, `integrations` | bearer tokens and client snippets for the loopback bridge |
77
81
  | Config | `config explain` `template`, `init-config`, `debug bundle`, `features` | effective-config report, config templates, sanitized debug bundle |
@@ -81,7 +85,7 @@ Everyday examples:
81
85
  ```bash
82
86
  codex-multi-auth switch 2 # pin account #2
83
87
  codex-multi-auth report --live --json # full machine-readable health report
84
- codex-multi-auth fix --live --model gpt-5.5 # live repair probes with a chosen model
88
+ codex-multi-auth fix --live --model gpt-6.1-sol # live repair probes with a chosen model
85
89
  codex-multi-auth doctor --fix # diagnose and apply the safest fixes
86
90
  codex-multi-auth usage --since 24h --by model # local usage ledger summary
87
91
  codex-multi-auth rotation status # is live rotation enabled?
@@ -89,7 +93,7 @@ codex-multi-auth rotation status # is live rotation enabled?
89
93
 
90
94
  ## Runtime rotation, in one paragraph
91
95
 
92
- With rotation on (the default), request-bearing wrapper commands — `exec`, `review`, `resume`, `fork`, `app`, and the bare TUI — route through a local proxy that picks the healthiest account per request, refreshes tokens as needed, and fails over across the pool on rate limits within a bounded retry budget. The proxy listens on loopback only, authenticates its own clients with a per-process token, forwards only Responses API and model-discovery calls, and never writes account emails or tokens to client headers or logs. Plain `codex` is never touched: rotation exists only inside `codex-multi-auth-codex`/`mcodex` sessions, or inside the packaged desktop app after an opt-in, reversible bind. Turn it off with `codex-multi-auth rotation disable` or `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`.
96
+ With rotation on (the default), request-bearing wrapper commands — `exec`, `review`, `resume`, `fork`, `app`, and the bare TUI — route through a local proxy that picks the healthiest account per request, refreshes tokens as needed, and fails over across the pool on rate limits within a bounded retry budget. The proxy listens on loopback only, authenticates its own clients with a per-process token, forwards only Responses API, model-discovery, image, and thread-goal calls, and never writes account emails or tokens to client headers or logs. Plain `codex` is never touched: rotation exists only inside `codex-multi-auth-codex`/`mcodex` sessions, or inside the packaged desktop app after an opt-in, reversible bind. Turn it off with `codex-multi-auth rotation disable` or `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`.
93
97
 
94
98
  ## Where state lives
95
99
 
@@ -128,10 +132,12 @@ Common symptoms — wrong active account, OAuth callback port `1455` already bou
128
132
  | [docs/faq.md](docs/faq.md) | Common questions |
129
133
  | [docs/upgrade.md](docs/upgrade.md) | Version and legacy-package migration |
130
134
  | [docs/privacy.md](docs/privacy.md) | Data handling and local storage behavior |
135
+ | [config/README.md](config/README.md) | Modern vs. legacy catalog templates |
136
+ | [CHANGELOG.md](CHANGELOG.md) | Release history (short version) |
131
137
 
132
138
  ## Release notes
133
139
 
134
- - Current stable: [v2.17.3](docs/releases/v2.17.3.md) — `npm i -g codex-multi-auth`
140
+ - Current stable: [v2.18.0](docs/releases/v2.18.0.md) — `npm i -g codex-multi-auth`
135
141
  - Earlier stable lines: [v2.6.0](docs/releases/v2.6.0.md), [v2.5.0](docs/releases/v2.5.0.md)
136
142
  - Full release archive: [docs/README.md#release-history](docs/README.md#release-history)
137
143
 
package/config/README.md CHANGED
@@ -17,7 +17,7 @@ These files are optional model/provider templates for host/plugin configurations
17
17
 
18
18
  ## Defaults Included
19
19
 
20
- - Current documented OpenAI/Codex model families: GPT-6 Astra (`gpt-6-astra`, `gpt-6-astra-aeon`), GPT-6 Sol (`gpt-6-sol`) and GPT-6 Luna (`gpt-6-luna`), then `gpt-5.6-sol`/`terra`/`luna`, `gpt-5.5` and `gpt-5.5-pro`. Retired models (`gpt-5.4*`, `gpt-5.3-codex` and older) were removed; their ids still resolve to a replacement, see `docs/configuration.md`
20
+ - Current documented OpenAI/Codex model families: GPT-6.1 Sol (`gpt-6.1-sol`, the default), GPT-6 Astra (`gpt-6-astra`), GPT-6 Sol (`gpt-6-sol`) and GPT-6 Luna (`gpt-6-luna`), then `gpt-5.6-sol`/`terra`/`luna`. Retired models (`gpt-5.5`/`gpt-5.5-pro`, `gpt-5.4*`, `gpt-5.3-codex` and older, plus the leaked `gpt-6-astra-aeon` slug) were removed from the picker; their ids still resolve to a replacement, see `docs/configuration.md`
21
21
  - The GPT-6 entries carry `limit.context: 272000`, the `context_window` in the upstream Codex catalog. The GPT-5.6 entries still carry the pre-cut `372000` and were left alone here; change them only alongside a deliberate review, since the value feeds a picker rather than the context budget guard (which refuses to estimate either family)
22
22
  - The Daybreak cyber models resolve at the code level but are deliberately absent from these templates, matching their hidden visibility in the upstream Codex picker
23
23
  - `store: false`
@@ -165,8 +165,8 @@
165
165
  "store": false
166
166
  }
167
167
  },
168
- "gpt-6-astra-aeon-low": {
169
- "name": "GPT 6 Astra Aeon Low (OAuth)",
168
+ "gpt-6.1-sol-low": {
169
+ "name": "GPT 6.1 Sol Low (OAuth)",
170
170
  "limit": {
171
171
  "context": 272000,
172
172
  "output": 128000
@@ -190,8 +190,8 @@
190
190
  "store": false
191
191
  }
192
192
  },
193
- "gpt-6-astra-aeon-medium": {
194
- "name": "GPT 6 Astra Aeon Medium (OAuth)",
193
+ "gpt-6.1-sol-medium": {
194
+ "name": "GPT 6.1 Sol Medium (OAuth)",
195
195
  "limit": {
196
196
  "context": 272000,
197
197
  "output": 128000
@@ -215,8 +215,8 @@
215
215
  "store": false
216
216
  }
217
217
  },
218
- "gpt-6-astra-aeon-high": {
219
- "name": "GPT 6 Astra Aeon High (OAuth)",
218
+ "gpt-6.1-sol-high": {
219
+ "name": "GPT 6.1 Sol High (OAuth)",
220
220
  "limit": {
221
221
  "context": 272000,
222
222
  "output": 128000
@@ -240,8 +240,8 @@
240
240
  "store": false
241
241
  }
242
242
  },
243
- "gpt-6-astra-aeon-xhigh": {
244
- "name": "GPT 6 Astra Aeon Extra High (OAuth)",
243
+ "gpt-6.1-sol-xhigh": {
244
+ "name": "GPT 6.1 Sol Xhigh (OAuth)",
245
245
  "limit": {
246
246
  "context": 272000,
247
247
  "output": 128000
@@ -265,8 +265,8 @@
265
265
  "store": false
266
266
  }
267
267
  },
268
- "gpt-6-astra-aeon-max": {
269
- "name": "GPT 6 Astra Aeon Max (OAuth)",
268
+ "gpt-6.1-sol-max": {
269
+ "name": "GPT 6.1 Sol Max (OAuth)",
270
270
  "limit": {
271
271
  "context": 272000,
272
272
  "output": 128000
@@ -290,8 +290,8 @@
290
290
  "store": false
291
291
  }
292
292
  },
293
- "gpt-6-astra-aeon-ultra": {
294
- "name": "GPT 6 Astra Aeon Ultra (OAuth)",
293
+ "gpt-6.1-sol-ultra": {
294
+ "name": "GPT 6.1 Sol Ultra (OAuth)",
295
295
  "limit": {
296
296
  "context": 272000,
297
297
  "output": 128000
@@ -1014,206 +1014,6 @@
1014
1014
  ],
1015
1015
  "store": false
1016
1016
  }
1017
- },
1018
- "gpt-5.5-none": {
1019
- "name": "GPT 5.5 None (OAuth)",
1020
- "limit": {
1021
- "context": 272000,
1022
- "output": 128000
1023
- },
1024
- "modalities": {
1025
- "input": [
1026
- "text",
1027
- "image"
1028
- ],
1029
- "output": [
1030
- "text"
1031
- ]
1032
- },
1033
- "options": {
1034
- "reasoningEffort": "none",
1035
- "reasoningSummary": "auto",
1036
- "textVerbosity": "medium",
1037
- "include": [
1038
- "reasoning.encrypted_content"
1039
- ],
1040
- "store": false
1041
- }
1042
- },
1043
- "gpt-5.5-low": {
1044
- "name": "GPT 5.5 Low (OAuth)",
1045
- "limit": {
1046
- "context": 272000,
1047
- "output": 128000
1048
- },
1049
- "modalities": {
1050
- "input": [
1051
- "text",
1052
- "image"
1053
- ],
1054
- "output": [
1055
- "text"
1056
- ]
1057
- },
1058
- "options": {
1059
- "reasoningEffort": "low",
1060
- "reasoningSummary": "auto",
1061
- "textVerbosity": "medium",
1062
- "include": [
1063
- "reasoning.encrypted_content"
1064
- ],
1065
- "store": false
1066
- }
1067
- },
1068
- "gpt-5.5-medium": {
1069
- "name": "GPT 5.5 Medium (OAuth)",
1070
- "limit": {
1071
- "context": 272000,
1072
- "output": 128000
1073
- },
1074
- "modalities": {
1075
- "input": [
1076
- "text",
1077
- "image"
1078
- ],
1079
- "output": [
1080
- "text"
1081
- ]
1082
- },
1083
- "options": {
1084
- "reasoningEffort": "medium",
1085
- "reasoningSummary": "auto",
1086
- "textVerbosity": "medium",
1087
- "include": [
1088
- "reasoning.encrypted_content"
1089
- ],
1090
- "store": false
1091
- }
1092
- },
1093
- "gpt-5.5-high": {
1094
- "name": "GPT 5.5 High (OAuth)",
1095
- "limit": {
1096
- "context": 272000,
1097
- "output": 128000
1098
- },
1099
- "modalities": {
1100
- "input": [
1101
- "text",
1102
- "image"
1103
- ],
1104
- "output": [
1105
- "text"
1106
- ]
1107
- },
1108
- "options": {
1109
- "reasoningEffort": "high",
1110
- "reasoningSummary": "detailed",
1111
- "textVerbosity": "medium",
1112
- "include": [
1113
- "reasoning.encrypted_content"
1114
- ],
1115
- "store": false
1116
- }
1117
- },
1118
- "gpt-5.5-xhigh": {
1119
- "name": "GPT 5.5 Extra High (OAuth)",
1120
- "limit": {
1121
- "context": 272000,
1122
- "output": 128000
1123
- },
1124
- "modalities": {
1125
- "input": [
1126
- "text",
1127
- "image"
1128
- ],
1129
- "output": [
1130
- "text"
1131
- ]
1132
- },
1133
- "options": {
1134
- "reasoningEffort": "xhigh",
1135
- "reasoningSummary": "detailed",
1136
- "textVerbosity": "medium",
1137
- "include": [
1138
- "reasoning.encrypted_content"
1139
- ],
1140
- "store": false
1141
- }
1142
- },
1143
- "gpt-5.5-pro-medium": {
1144
- "name": "GPT 5.5 Pro Medium (OAuth)",
1145
- "limit": {
1146
- "context": 272000,
1147
- "output": 128000
1148
- },
1149
- "modalities": {
1150
- "input": [
1151
- "text",
1152
- "image"
1153
- ],
1154
- "output": [
1155
- "text"
1156
- ]
1157
- },
1158
- "options": {
1159
- "reasoningEffort": "medium",
1160
- "reasoningSummary": "detailed",
1161
- "textVerbosity": "medium",
1162
- "include": [
1163
- "reasoning.encrypted_content"
1164
- ],
1165
- "store": false
1166
- }
1167
- },
1168
- "gpt-5.5-pro-high": {
1169
- "name": "GPT 5.5 Pro High (OAuth)",
1170
- "limit": {
1171
- "context": 272000,
1172
- "output": 128000
1173
- },
1174
- "modalities": {
1175
- "input": [
1176
- "text",
1177
- "image"
1178
- ],
1179
- "output": [
1180
- "text"
1181
- ]
1182
- },
1183
- "options": {
1184
- "reasoningEffort": "high",
1185
- "reasoningSummary": "detailed",
1186
- "textVerbosity": "medium",
1187
- "include": [
1188
- "reasoning.encrypted_content"
1189
- ],
1190
- "store": false
1191
- }
1192
- },
1193
- "gpt-5.5-pro-xhigh": {
1194
- "name": "GPT 5.5 Pro Extra High (OAuth)",
1195
- "limit": {
1196
- "context": 272000,
1197
- "output": 128000
1198
- },
1199
- "modalities": {
1200
- "input": [
1201
- "text",
1202
- "image"
1203
- ],
1204
- "output": [
1205
- "text"
1206
- ]
1207
- },
1208
- "options": {
1209
- "reasoningEffort": "xhigh",
1210
- "reasoningSummary": "detailed",
1211
- "textVerbosity": "medium",
1212
- "include": [
1213
- "reasoning.encrypted_content"
1214
- ],
1215
- "store": false
1216
- }
1217
1017
  }
1218
1018
  }
1219
1019
  }
@@ -15,8 +15,8 @@
15
15
  "store": false
16
16
  },
17
17
  "models": {
18
- "gpt-6-astra": {
19
- "name": "GPT 6 Astra (OAuth)",
18
+ "gpt-6.1-sol": {
19
+ "name": "GPT 6.1 Sol (OAuth)",
20
20
  "limit": {
21
21
  "context": 272000,
22
22
  "output": 128000
@@ -63,8 +63,8 @@
63
63
  }
64
64
  }
65
65
  },
66
- "gpt-6-astra-aeon": {
67
- "name": "GPT 6 Astra Aeon (OAuth)",
66
+ "gpt-6-astra": {
67
+ "name": "GPT 6 Astra (OAuth)",
68
68
  "limit": {
69
69
  "context": 272000,
70
70
  "output": 128000
@@ -340,82 +340,6 @@
340
340
  "textVerbosity": "medium"
341
341
  }
342
342
  }
343
- },
344
- "gpt-5.5": {
345
- "name": "GPT 5.5 (OAuth)",
346
- "limit": {
347
- "context": 272000,
348
- "output": 128000
349
- },
350
- "modalities": {
351
- "input": [
352
- "text",
353
- "image"
354
- ],
355
- "output": [
356
- "text"
357
- ]
358
- },
359
- "variants": {
360
- "none": {
361
- "reasoningEffort": "none",
362
- "reasoningSummary": "auto",
363
- "textVerbosity": "medium"
364
- },
365
- "low": {
366
- "reasoningEffort": "low",
367
- "reasoningSummary": "auto",
368
- "textVerbosity": "medium"
369
- },
370
- "medium": {
371
- "reasoningEffort": "medium",
372
- "reasoningSummary": "auto",
373
- "textVerbosity": "medium"
374
- },
375
- "high": {
376
- "reasoningEffort": "high",
377
- "reasoningSummary": "detailed",
378
- "textVerbosity": "medium"
379
- },
380
- "xhigh": {
381
- "reasoningEffort": "xhigh",
382
- "reasoningSummary": "detailed",
383
- "textVerbosity": "medium"
384
- }
385
- }
386
- },
387
- "gpt-5.5-pro": {
388
- "name": "GPT 5.5 Pro (OAuth)",
389
- "limit": {
390
- "context": 272000,
391
- "output": 128000
392
- },
393
- "modalities": {
394
- "input": [
395
- "text",
396
- "image"
397
- ],
398
- "output": [
399
- "text"
400
- ]
401
- },
402
- "variants": {
403
- "medium": {
404
- "reasoningEffort": "medium",
405
- "reasoningSummary": "detailed",
406
- "textVerbosity": "medium"
407
- },
408
- "high": {
409
- "reasoningEffort": "high",
410
- "reasoningSummary": "detailed",
411
- "textVerbosity": "medium"
412
- },
413
- "xhigh": {
414
- "reasoningEffort": "xhigh",
415
- "reasoningSummary": "detailed",
416
- "textVerbosity": "medium"
417
- }
418
- }
419
343
  }
420
344
  }
421
345
  }
@@ -9,7 +9,7 @@
9
9
  }
10
10
  }
11
11
  },
12
- "model": "openai/gpt-5.3-codex"
12
+ "model": "openai/gpt-5.6-sol"
13
13
  }
14
14
 
15
15
 
@@ -6,7 +6,7 @@
6
6
  * behavior (see the v2.5.0 release notes) closely enough to launch that no
7
7
  * estimate here has been reviewed for it yet.
8
8
  */
9
- export declare const UNESTIMATED_ROUTABLE_MODELS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-astra-aeon", "gpt-6-sol", "gpt-6-luna", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
9
+ export declare const UNESTIMATED_ROUTABLE_MODELS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
10
10
  export interface EffectiveContextWindow {
11
11
  tokens: number;
12
12
  source: "override" | "estimate";
@@ -23,14 +23,11 @@ import { getNormalizedModel } from "../request/helpers/model-map.js";
23
23
  * it to UNESTIMATED_ROUTABLE_MODELS instead.
24
24
  */
25
25
  //
26
- // Retired models (`gpt-5.1` through `gpt-5.4-*`, `gpt-5-mini`/`nano`, the codex
27
- // models) are deliberately absent. Their ids now route to a replacement, and
28
- // estimating the window from the retired name would evaluate a request against
29
- // a model it no longer runs on.
30
- const ESTIMATED_MODEL_CONTEXT_WINDOWS = {
31
- "gpt-5.5": 260_000,
32
- "gpt-5.5-pro": 260_000,
33
- };
26
+ // Retired models (`gpt-5.1` through `gpt-5.5-pro`, `gpt-5-mini`/`nano`, the
27
+ // codex models) are deliberately absent. Their ids now route to a
28
+ // replacement, and estimating the window from the retired name would
29
+ // evaluate a request against a model it no longer runs on.
30
+ const ESTIMATED_MODEL_CONTEXT_WINDOWS = {};
34
31
  /**
35
32
  * Routable models this guard consciously does not estimate a window for.
36
33
  * `getEffectiveContextWindow` returns `null` for these absent an override,
@@ -52,11 +49,11 @@ export const UNESTIMATED_ROUTABLE_MODELS = [
52
49
  // measured ceiling for this transport, so it stays unestimated too. Set
53
50
  // `contextBudgetGuardModelWindowOverrides` once you know your real ceiling.
54
51
  "gpt-6-astra",
55
- "gpt-6-astra-aeon",
56
- // Sol and Luna carry the same 272K Codex / 872K max split in the upstream
57
- // catalog as Astra, so the same reasoning keeps them unestimated.
52
+ // Sol, Luna and 6.1 Sol carry the same 272K Codex / 872K max split in the
53
+ // upstream catalog as Astra, so the same reasoning keeps them unestimated.
58
54
  "gpt-6-sol",
59
55
  "gpt-6-luna",
56
+ "gpt-6.1-sol",
60
57
  "gpt-daybreak-blue-latest",
61
58
  "gpt-daybreak-red-latest",
62
59
  ];
@@ -79,8 +76,8 @@ function normalizeModelName(model) {
79
76
  * the way they type the model still wins, then the catalog's canonical id for
80
77
  * it. `getNormalizedModel` is deliberately the exact/alias-only resolver —
81
78
  * `resolveNormalizedModel` falls back to `DEFAULT_MODEL` for anything it does
82
- * not recognize, which would hand an unknown model gpt-5.5's window and
83
- * evaluate a real session against a fabricated number.
79
+ * not recognize, which would hand an unknown model the default model's
80
+ * window and evaluate a real session against a fabricated number.
84
81
  */
85
82
  function windowLookupCandidates(model) {
86
83
  const raw = normalizeModelName(model);
@@ -35,7 +35,8 @@ export type OcChatgptTargetDetectionResult = OcChatgptTargetFound | OcChatgptTar
35
35
  /**
36
36
  * Detects the oc-chatgpt multi-auth storage target by evaluating explicit, canonical, and per-project candidate roots.
37
37
  *
38
- * Examines an explicit override (OC_CHATGPT_MULTI_AUTH_DIR or options.explicitRoot), the canonical user store (~/.opencode),
38
+ * Examines an explicit override (OC_CODEX_MULTI_AUTH_DIR, the legacy OC_CHATGPT_MULTI_AUTH_DIR alias, or
39
+ * options.explicitRoot), the canonical user store (~/.opencode),
39
40
  * and a per-project storage location (derived from options.projectRoot or the current working directory). For each candidate
40
41
  * it checks for account artifacts and storage signals and returns a single resolved target, an ambiguity listing multiple
41
42
  * matching candidates, or a "none" result with the attempted candidates. This function performs synchronous filesystem checks
@@ -45,7 +46,7 @@ export type OcChatgptTargetDetectionResult = OcChatgptTargetFound | OcChatgptTar
45
46
  *
46
47
  * @param options - Optional overrides:
47
48
  * - explicitRoot: absolute path to force as the candidate root (use `null` to explicitly disable); if omitted the
48
- * OC_CHATGPT_MULTI_AUTH_DIR environment variable is considered.
49
+ * OC_CODEX_MULTI_AUTH_DIR environment variable is considered, then the legacy OC_CHATGPT_MULTI_AUTH_DIR alias.
49
50
  * - projectRoot: explicit project root to derive per-project storage; if omitted the current working directory is used to
50
51
  * discover the project root.
51
52
  * @returns An OcChatgptTargetDetectionResult describing either a resolved `target` (with `descriptor` and a `resolution`
@@ -2,7 +2,14 @@ import { existsSync, readdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, normalize, relative, resolve, sep, win32 } from "node:path";
4
4
  import { findProjectRoot, getProjectStorageKey, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
5
- const ACCOUNT_FILE_NAME = "openai-codex-accounts.json";
5
+ // The sibling plugin renamed its account file from `openai-codex-accounts.json`
6
+ // (oc-chatgpt-multi-auth era) to `oc-codex-multi-auth-accounts.json`; both are
7
+ // detected, preferring the current name when a root holds both.
8
+ const ACCOUNT_FILE_NAMES = [
9
+ "oc-codex-multi-auth-accounts.json",
10
+ "openai-codex-accounts.json",
11
+ ];
12
+ const ACCOUNT_FILE_NAME = ACCOUNT_FILE_NAMES[0];
6
13
  const BACKUPS_DIR_NAME = "backups";
7
14
  const PROJECTS_DIR_NAME = "projects";
8
15
  const CANONICAL_HOME_BASENAME = ".opencode";
@@ -107,17 +114,32 @@ function deduplicatePaths(paths) {
107
114
  }
108
115
  return result;
109
116
  }
117
+ /**
118
+ * Returns the path of the primary account file under `root`, preferring the
119
+ * current `oc-codex-multi-auth-accounts.json` name over the legacy
120
+ * `openai-codex-accounts.json` name when both exist, or `null` when neither
121
+ * file (nor its `.wal` sidecar) is present.
122
+ */
123
+ function findPrimaryAccountPath(root) {
124
+ for (const name of ACCOUNT_FILE_NAMES) {
125
+ const accountPath = join(root, name);
126
+ if (existsSync(accountPath) || existsSync(`${accountPath}.wal`)) {
127
+ return accountPath;
128
+ }
129
+ }
130
+ return null;
131
+ }
110
132
  /**
111
133
  * Detects whether an account file or rotated account artifacts exist under the specified storage root.
112
134
  *
113
- * Scans for the canonical account file and its WAL variant, rotated/archived files matching the account filename pattern (excluding `.tmp`, `.wal`, and names containing `.rotate.`), and the backups directory. The probe is tolerant of unreadable directories (errors are ignored) and does not read file contents. Callers should be aware of concurrent filesystem changes — a `false` result does not guarantee no artifact will appear shortly after. On Windows, filesystem matching should be considered case-insensitive by callers. This check does not expose or parse any token contents.
135
+ * Scans for an account file under either known name (`oc-codex-multi-auth-accounts.json` or the legacy `openai-codex-accounts.json`) and its WAL variant, rotated/archived files matching either account filename pattern (excluding `.tmp`, `.wal`, and names containing `.rotate.`), and the backups directory. The probe is tolerant of unreadable directories (errors are ignored) and does not read file contents. Callers should be aware of concurrent filesystem changes — a `false` result does not guarantee no artifact will appear shortly after. On Windows, filesystem matching should be considered case-insensitive by callers. This check does not expose or parse any token contents.
114
136
  *
115
137
  * @param root - Filesystem path of the storage root to probe
138
+ * @param primaryAccountPath - Result of `findPrimaryAccountPath(root)`, so callers resolve the descriptor path once
116
139
  * @returns `true` if any account file or rotated account artifact exists under `root` or its backups directory, `false` otherwise.
117
140
  */
118
- function hasAccountArtifacts(root) {
119
- const accountPath = join(root, ACCOUNT_FILE_NAME);
120
- if (existsSync(accountPath) || existsSync(`${accountPath}.wal`)) {
141
+ function hasAccountArtifacts(root, primaryAccountPath) {
142
+ if (primaryAccountPath !== null) {
121
143
  return true;
122
144
  }
123
145
  const hasRotated = (dir) => {
@@ -126,7 +148,7 @@ function hasAccountArtifacts(root) {
126
148
  for (const entry of entries) {
127
149
  if (!entry.isFile())
128
150
  continue;
129
- if (!entry.name.startsWith(`${ACCOUNT_FILE_NAME}.`))
151
+ if (!ACCOUNT_FILE_NAMES.some((name) => entry.name.startsWith(`${name}.`)))
130
152
  continue;
131
153
  if (entry.name.endsWith(".tmp"))
132
154
  continue;
@@ -146,7 +168,7 @@ function hasAccountArtifacts(root) {
146
168
  return true;
147
169
  }
148
170
  const backupsDir = join(root, BACKUPS_DIR_NAME);
149
- if (existsSync(join(backupsDir, ACCOUNT_FILE_NAME))) {
171
+ if (ACCOUNT_FILE_NAMES.some((name) => existsSync(join(backupsDir, name)))) {
150
172
  return true;
151
173
  }
152
174
  if (hasRotated(backupsDir)) {
@@ -191,7 +213,8 @@ function inferScopeFromRoot(root, canonicalRoot) {
191
213
  /**
192
214
  * Detects the oc-chatgpt multi-auth storage target by evaluating explicit, canonical, and per-project candidate roots.
193
215
  *
194
- * Examines an explicit override (OC_CHATGPT_MULTI_AUTH_DIR or options.explicitRoot), the canonical user store (~/.opencode),
216
+ * Examines an explicit override (OC_CODEX_MULTI_AUTH_DIR, the legacy OC_CHATGPT_MULTI_AUTH_DIR alias, or
217
+ * options.explicitRoot), the canonical user store (~/.opencode),
195
218
  * and a per-project storage location (derived from options.projectRoot or the current working directory). For each candidate
196
219
  * it checks for account artifacts and storage signals and returns a single resolved target, an ambiguity listing multiple
197
220
  * matching candidates, or a "none" result with the attempted candidates. This function performs synchronous filesystem checks
@@ -201,14 +224,17 @@ function inferScopeFromRoot(root, canonicalRoot) {
201
224
  *
202
225
  * @param options - Optional overrides:
203
226
  * - explicitRoot: absolute path to force as the candidate root (use `null` to explicitly disable); if omitted the
204
- * OC_CHATGPT_MULTI_AUTH_DIR environment variable is considered.
227
+ * OC_CODEX_MULTI_AUTH_DIR environment variable is considered, then the legacy OC_CHATGPT_MULTI_AUTH_DIR alias.
205
228
  * - projectRoot: explicit project root to derive per-project storage; if omitted the current working directory is used to
206
229
  * discover the project root.
207
230
  * @returns An OcChatgptTargetDetectionResult describing either a resolved `target` (with `descriptor` and a `resolution`
208
231
  * of `"accounts"` or `"signals"`), an `ambiguous` outcome listing conflicting candidates, or `none` with the tried candidates.
209
232
  */
210
233
  export function detectOcChatgptMultiAuthTarget(options) {
211
- const explicitFromEnv = (process.env.OC_CHATGPT_MULTI_AUTH_DIR ?? "").trim();
234
+ const explicitFromEnv = (firstNonEmpty([
235
+ process.env.OC_CODEX_MULTI_AUTH_DIR,
236
+ process.env.OC_CHATGPT_MULTI_AUTH_DIR,
237
+ ]) ?? "").trim();
212
238
  const hasExplicitRootOption = options !== undefined && "explicitRoot" in options;
213
239
  const explicitRoot = (hasExplicitRootOption ? (options.explicitRoot ?? "") : explicitFromEnv).trim();
214
240
  const userHome = getResolvedUserHomeDir();
@@ -233,9 +259,10 @@ export function detectOcChatgptMultiAuthTarget(options) {
233
259
  : inferredScope === "project"
234
260
  ? "project"
235
261
  : "default-global";
236
- const accountPath = join(root, ACCOUNT_FILE_NAME);
262
+ const primaryAccountPath = findPrimaryAccountPath(root);
263
+ const accountPath = primaryAccountPath ?? join(root, ACCOUNT_FILE_NAME);
237
264
  const backupRoot = join(root, BACKUPS_DIR_NAME);
238
- const hasAccountArtifactsFlag = hasAccountArtifacts(root);
265
+ const hasAccountArtifactsFlag = hasAccountArtifacts(root, primaryAccountPath);
239
266
  return {
240
267
  scope: inferredScope,
241
268
  source,
@@ -314,7 +341,7 @@ export function detectOcChatgptMultiAuthTarget(options) {
314
341
  }
315
342
  return {
316
343
  kind: "none",
317
- reason: "No oc-chatgpt-multi-auth target root found; create ~/.opencode or supply OC_CHATGPT_MULTI_AUTH_DIR.",
344
+ reason: "No oc-chatgpt-multi-auth target root found; create ~/.opencode or supply OC_CODEX_MULTI_AUTH_DIR (legacy OC_CHATGPT_MULTI_AUTH_DIR is still accepted).",
318
345
  tried: candidates.map(({ scope, source, root, accountPath, backupRoot }) => ({
319
346
  scope,
320
347
  source,