codex-multi-auth 2.8.2 → 2.8.4
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.8.
|
|
3
|
+
"version": "2.8.4",
|
|
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
|
@@ -392,7 +392,9 @@ codex-multi-auth doctor --json
|
|
|
392
392
|
|
|
393
393
|
## Release Notes
|
|
394
394
|
|
|
395
|
-
- Current stable: [docs/releases/v2.8.
|
|
395
|
+
- Current stable: [docs/releases/v2.8.4.md](docs/releases/v2.8.4.md) — install via `npm i -g codex-multi-auth`
|
|
396
|
+
- Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
|
|
397
|
+
- Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
|
|
396
398
|
- Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
|
|
397
399
|
- Previous stable: [docs/releases/v2.7.0.md](docs/releases/v2.7.0.md)
|
|
398
400
|
- Previous stable: [docs/releases/v2.6.0.md](docs/releases/v2.6.0.md)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quota-formatters.d.ts","sourceRoot":"","sources":["../../../../lib/codex-manager/formatters/quota-formatters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EACN,KAAK,kBAAkB,EACvB,uBAAuB,EAGvB,MAAM,sBAAsB,CAAC;AAY9B,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAmCzD;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC9C,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC,EAC7D,QAAQ,EAAE,wBAAwB,EAClC,GAAG,SAAa,GACd,MAAM,
|
|
1
|
+
{"version":3,"file":"quota-formatters.d.ts","sourceRoot":"","sources":["../../../../lib/codex-manager/formatters/quota-formatters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EACN,KAAK,kBAAkB,EACvB,uBAAuB,EAGvB,MAAM,sBAAsB,CAAC;AAY9B,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAmCzD;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC9C,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC,EAC7D,QAAQ,EAAE,wBAAwB,EAClC,GAAG,SAAa,GACd,MAAM,CAIR;AAED,wBAAgB,yBAAyB,CACxC,KAAK,EAAE,eAAe,GACpB,kBAAkB,CAgBpB;AA8BD;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAgCD,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,kBAAkB,EAC5B,GAAG,SAAa,EAChB,OAAO,GAAE,yBAA8B,GACrC,MAAM,CA6CR;AAED,wBAAgB,yBAAyB,CACxC,KAAK,EAAE,eAAe,EACtB,GAAG,SAAa,EAChB,OAAO,GAAE,yBAA8B,GACrC,MAAM,CA6CR"}
|
|
@@ -43,7 +43,8 @@ export function styleQuotaSummary(summary) {
|
|
|
43
43
|
export function formatQuotaSnapshotForDashboard(snapshot, settings, now = Date.now()) {
|
|
44
44
|
if (!settings.showQuotaDetails)
|
|
45
45
|
return "live session OK";
|
|
46
|
-
|
|
46
|
+
const summary = formatCompactQuotaSnapshot(snapshot, now, { showReset: true });
|
|
47
|
+
return summary ? `live session OK (${summary})` : "live session OK";
|
|
47
48
|
}
|
|
48
49
|
export function quotaCacheEntryToSnapshot(entry) {
|
|
49
50
|
return {
|
|
@@ -72,17 +73,38 @@ function formatCompactQuotaWindowLabel(windowMinutes) {
|
|
|
72
73
|
return `${windowMinutes / 60}h`;
|
|
73
74
|
return `${windowMinutes}m`;
|
|
74
75
|
}
|
|
76
|
+
function isUninformativeFullQuotaWindow(windowMinutes, usedPercent, resetAtMs, now) {
|
|
77
|
+
if (formatCompactQuotaWindowLabel(windowMinutes) !== "quota")
|
|
78
|
+
return false;
|
|
79
|
+
if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const left = quotaLeftPercentFromUsed(usedPercent);
|
|
83
|
+
return (!formatQuotaResetAt(resetAtMs, now) &&
|
|
84
|
+
(left === undefined || left >= 100));
|
|
85
|
+
}
|
|
75
86
|
function formatCompactQuotaPart(windowMinutes, usedPercent, resetAtMs, options, now) {
|
|
76
87
|
const label = formatCompactQuotaWindowLabel(windowMinutes);
|
|
77
88
|
if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) {
|
|
78
89
|
return null;
|
|
79
90
|
}
|
|
80
91
|
const left = quotaLeftPercentFromUsed(usedPercent);
|
|
92
|
+
// The reset validity is computed regardless of showReset: it also feeds the
|
|
93
|
+
// uninformative-window check below, so hiding stays consistent across the
|
|
94
|
+
// compact (no-reset) and check (with-reset) surfaces.
|
|
95
|
+
const reset = formatQuotaResetAt(resetAtMs, now);
|
|
96
|
+
// An unlabeled window ("quota" — upstream reported no duration) that sits at
|
|
97
|
+
// 100% left with no reset timestamp carries no actionable signal; since the
|
|
98
|
+
// 2026-07/08 upstream limit changes it renders as a permanent "quota 100%"
|
|
99
|
+
// on every account. Hide exactly that shape: the segment reappears as soon
|
|
100
|
+
// as the window reports a duration, any depletion, or a reset time.
|
|
101
|
+
if (!reset && label === "quota" && (left === undefined || left >= 100)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
81
104
|
const part = `${label} ${left}%`;
|
|
82
105
|
if (!options.showReset)
|
|
83
106
|
return part;
|
|
84
107
|
// A missing or malformed reset timestamp must never drop the percentage.
|
|
85
|
-
const reset = formatQuotaResetAt(resetAtMs, now);
|
|
86
108
|
return reset ? `${part}, resets ${reset}` : part;
|
|
87
109
|
}
|
|
88
110
|
export function formatCompactQuotaSnapshot(snapshot, now = Date.now(), options = {}) {
|
|
@@ -99,6 +121,10 @@ export function formatCompactQuotaSnapshot(snapshot, now = Date.now(), options =
|
|
|
99
121
|
if (parts.length > 0) {
|
|
100
122
|
return parts.join(" | ");
|
|
101
123
|
}
|
|
124
|
+
if (isUninformativeFullQuotaWindow(snapshot.primary.windowMinutes, snapshot.primary.usedPercent, snapshot.primary.resetAtMs, now) ||
|
|
125
|
+
isUninformativeFullQuotaWindow(snapshot.secondary.windowMinutes, snapshot.secondary.usedPercent, snapshot.secondary.resetAtMs, now)) {
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
102
128
|
return formatQuotaSnapshotLine(snapshot);
|
|
103
129
|
}
|
|
104
130
|
export function formatAccountQuotaSummary(entry, now = Date.now(), options = {}) {
|
|
@@ -115,5 +141,9 @@ export function formatAccountQuotaSummary(entry, now = Date.now(), options = {})
|
|
|
115
141
|
if (parts.length > 0) {
|
|
116
142
|
return parts.join(" | ");
|
|
117
143
|
}
|
|
144
|
+
if (isUninformativeFullQuotaWindow(entry.primary.windowMinutes, entry.primary.usedPercent, entry.primary.resetAtMs, now) ||
|
|
145
|
+
isUninformativeFullQuotaWindow(entry.secondary.windowMinutes, entry.secondary.usedPercent, entry.secondary.resetAtMs, now)) {
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
118
148
|
return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
|
|
119
149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.4",
|
|
4
4
|
"description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
package/scripts/codex.js
CHANGED
|
@@ -76,6 +76,8 @@ const APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV =
|
|
|
76
76
|
"CODEX_MULTI_AUTH_REAL_CODEX_HOME";
|
|
77
77
|
const APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV =
|
|
78
78
|
"CODEX_MULTI_AUTH_APP_ROTATION_USE_CANONICAL_HOME";
|
|
79
|
+
const APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV =
|
|
80
|
+
"CODEX_MULTI_AUTH_APP_ROTATION_INSTALL_APP_SERVER_SHIM";
|
|
79
81
|
const APP_SERVER_CONFIG_ARGS_ENV =
|
|
80
82
|
"CODEX_MULTI_AUTH_APP_SERVER_CONFIG_ARGS_JSON";
|
|
81
83
|
const APP_RUNTIME_HELPER_STATUS_FILE =
|
|
@@ -1769,6 +1771,10 @@ async function forwardToRealCodex(codexBin, rawArgs, baseEnv = process.env) {
|
|
|
1769
1771
|
compatibility,
|
|
1770
1772
|
rawArgs,
|
|
1771
1773
|
);
|
|
1774
|
+
if (runtimeProxyContext.startupError) {
|
|
1775
|
+
console.error(runtimeProxyContext.startupError);
|
|
1776
|
+
return 1;
|
|
1777
|
+
}
|
|
1772
1778
|
const result = await forwardToRealCodexOnce(
|
|
1773
1779
|
codexBin,
|
|
1774
1780
|
runtimeProxyContext.args,
|
|
@@ -4058,17 +4064,30 @@ async function runRuntimeRotationAppHelper(identityToken = "") {
|
|
|
4058
4064
|
clientApiKey,
|
|
4059
4065
|
configTomlModule,
|
|
4060
4066
|
);
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4067
|
+
// The shim only exists so a Codex process that spawns its own
|
|
4068
|
+
// `codex app-server` through `CODEX_CLI_PATH` — the desktop app — gets that
|
|
4069
|
+
// spawn routed back through the wrapper. Installing it is not free and not
|
|
4070
|
+
// inert: it copies the node image into a per-pid directory and stamps
|
|
4071
|
+
// `CODEX_CLI_PATH`, `NODE_OPTIONS` and `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`
|
|
4072
|
+
// onto the environment every forwarded child inherits. A wrapper-invoked
|
|
4073
|
+
// `app-server` is already launched with the rotation overrides on its own
|
|
4074
|
+
// command line, so it must not pay for or inherit any of that (#659).
|
|
4075
|
+
const installAppServerShim =
|
|
4076
|
+
(process.env[APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV] ?? "1").trim() !==
|
|
4077
|
+
"0";
|
|
4078
|
+
if (installAppServerShim) {
|
|
4079
|
+
const appServerConfigArgs = useCanonicalHome
|
|
4080
|
+
? [
|
|
4081
|
+
...(runtimeContext.args ?? []),
|
|
4082
|
+
"-c",
|
|
4083
|
+
`model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
|
|
4084
|
+
]
|
|
4085
|
+
: [];
|
|
4086
|
+
appServerShimDir = installRuntimeRotationAppServerCliShim(
|
|
4087
|
+
runtimeContext.env,
|
|
4088
|
+
appServerConfigArgs,
|
|
4089
|
+
);
|
|
4090
|
+
}
|
|
4072
4091
|
lastRequestCount = proxyServer.getStatus?.().totalRequests ?? 0;
|
|
4073
4092
|
publishStatus("running");
|
|
4074
4093
|
process.stdout.write(
|
|
@@ -4194,6 +4213,8 @@ function startRuntimeRotationAppHelper(baseContext, options = {}) {
|
|
|
4194
4213
|
[APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV]: realCodexHome,
|
|
4195
4214
|
[APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV]:
|
|
4196
4215
|
options.useCanonicalHome === true ? "1" : "0",
|
|
4216
|
+
[APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV]:
|
|
4217
|
+
options.installAppServerShim === false ? "0" : "1",
|
|
4197
4218
|
};
|
|
4198
4219
|
const helper = spawn(
|
|
4199
4220
|
process.execPath,
|
|
@@ -4227,6 +4248,12 @@ function startRuntimeRotationAppHelper(baseContext, options = {}) {
|
|
|
4227
4248
|
}, APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS);
|
|
4228
4249
|
helper.stdout?.setEncoding("utf8");
|
|
4229
4250
|
helper.stdout?.on("data", (chunk) => {
|
|
4251
|
+
// Keep consuming after startup settles so the helper never blocks on a
|
|
4252
|
+
// full pipe, but stop accumulating: `app-server` holds this parent open
|
|
4253
|
+
// for the resident server's whole life, so a buffer that only ever grows
|
|
4254
|
+
// is a leak, and re-parsing the retained ready line on every later chunk
|
|
4255
|
+
// is pure waste.
|
|
4256
|
+
if (settled) return;
|
|
4230
4257
|
stdoutBuffer += chunk;
|
|
4231
4258
|
const newlineIndex = stdoutBuffer.indexOf("\n");
|
|
4232
4259
|
if (newlineIndex < 0) return;
|
|
@@ -4249,6 +4276,7 @@ function startRuntimeRotationAppHelper(baseContext, options = {}) {
|
|
|
4249
4276
|
});
|
|
4250
4277
|
helper.stderr?.setEncoding("utf8");
|
|
4251
4278
|
helper.stderr?.on("data", (chunk) => {
|
|
4279
|
+
if (settled) return;
|
|
4252
4280
|
stderrBuffer += chunk;
|
|
4253
4281
|
});
|
|
4254
4282
|
helper.once("error", fail);
|
|
@@ -4268,11 +4296,17 @@ async function createRuntimeRotationAppHelperContext(
|
|
|
4268
4296
|
configTomlModule,
|
|
4269
4297
|
options = {},
|
|
4270
4298
|
) {
|
|
4271
|
-
const startedAt = Date.now();
|
|
4272
4299
|
const { helper, message } = await startRuntimeRotationAppHelper(
|
|
4273
4300
|
baseContext,
|
|
4274
4301
|
options,
|
|
4275
4302
|
);
|
|
4303
|
+
// Start the grace clock once the helper is ready, not before it is launched.
|
|
4304
|
+
// Launching is bounded by APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS (15s), so a
|
|
4305
|
+
// cold or loaded machine could otherwise burn the whole 5s window before the
|
|
4306
|
+
// forwarded command had run at all, and `codex app` — which relies on the
|
|
4307
|
+
// window rather than an explicit `detachOnExit` — would kill the helper it
|
|
4308
|
+
// had just handed the desktop app off to.
|
|
4309
|
+
const startedAt = Date.now();
|
|
4276
4310
|
const helperEnv = message.env ?? {};
|
|
4277
4311
|
const helperShimDir =
|
|
4278
4312
|
typeof helperEnv.CODEX_CLI_PATH === "string" &&
|
|
@@ -4284,9 +4318,16 @@ async function createRuntimeRotationAppHelperContext(
|
|
|
4284
4318
|
: [];
|
|
4285
4319
|
const detachGraceMs = resolveRuntimeRotationAppHelperDetachGraceMs(baseContext.env);
|
|
4286
4320
|
|
|
4321
|
+
// An explicit `detachOnExit: false` is a decision, not a default: a resident
|
|
4322
|
+
// server owns its proxy for its whole lifetime, and leaving the helper behind
|
|
4323
|
+
// on a short-lived run — a client attaches, runs one query, disconnects —
|
|
4324
|
+
// strands it for the full idle timeout with nobody left to refresh its
|
|
4325
|
+
// activity clock. Only an unset `detachOnExit` falls back to the grace window.
|
|
4287
4326
|
const cleanup = async ({ exitCode } = {}) => {
|
|
4288
4327
|
const livedMs = Date.now() - startedAt;
|
|
4289
|
-
|
|
4328
|
+
const detachWithinGrace =
|
|
4329
|
+
options.detachOnExit !== false && livedMs < detachGraceMs;
|
|
4330
|
+
if (exitCode === 0 && (options.detachOnExit === true || detachWithinGrace)) {
|
|
4290
4331
|
helper.stdout?.destroy();
|
|
4291
4332
|
helper.stderr?.destroy();
|
|
4292
4333
|
helper.unref();
|
|
@@ -4321,6 +4362,11 @@ async function createRuntimeRotationAppHelperContext(
|
|
|
4321
4362
|
...baseContext.env,
|
|
4322
4363
|
...helperEnv,
|
|
4323
4364
|
},
|
|
4365
|
+
// Without the app-server shim there is no `CODEX_MULTI_AUTH_APP_SERVER_ACCOUNT_LABEL`
|
|
4366
|
+
// in the forwarded environment, so the caller has to ask for the
|
|
4367
|
+
// `account/read` / `getAuthStatus` / `account/rateLimits/read` rewriting
|
|
4368
|
+
// explicitly. Only the shadow branch used to set it.
|
|
4369
|
+
proxyAppServerAccountRead: options.proxyAppServerAccountRead === true,
|
|
4324
4370
|
cleanup: async (details) => {
|
|
4325
4371
|
try {
|
|
4326
4372
|
await cleanup(details);
|
|
@@ -4348,14 +4394,56 @@ async function createRuntimeRotationProxyContextIfEnabled(
|
|
|
4348
4394
|
return baseContext;
|
|
4349
4395
|
}
|
|
4350
4396
|
|
|
4397
|
+
// A helper that cannot start is a hard failure for these branches — unlike the
|
|
4398
|
+
// shadow path below, there is no rotation-off shape left to degrade into. Turn
|
|
4399
|
+
// it into a diagnostic and a nonzero exit rather than an unhandled rejection,
|
|
4400
|
+
// and release the compatibility shadow home the caller already built.
|
|
4401
|
+
const startAppHelperContext = async (options) => {
|
|
4402
|
+
try {
|
|
4403
|
+
return await createRuntimeRotationAppHelperContext(
|
|
4404
|
+
baseContext,
|
|
4405
|
+
configTomlModule,
|
|
4406
|
+
options,
|
|
4407
|
+
);
|
|
4408
|
+
} catch (error) {
|
|
4409
|
+
try {
|
|
4410
|
+
baseContext.cleanup?.();
|
|
4411
|
+
} catch {
|
|
4412
|
+
// Best-effort cleanup only; report the original failure.
|
|
4413
|
+
}
|
|
4414
|
+
return {
|
|
4415
|
+
startupError: `codex-multi-auth runtime rotation helper failed to start: ${
|
|
4416
|
+
error instanceof Error ? error.message : String(error)
|
|
4417
|
+
}`,
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
};
|
|
4421
|
+
|
|
4422
|
+
// `app-server` is a resident process, and the shadow home cannot host one: the
|
|
4423
|
+
// mirror links directories, so Codex's lstat-strict check on
|
|
4424
|
+
// `<CODEX_HOME>/app-server-control` refuses to start, and a server that does
|
|
4425
|
+
// start serves every attached client a frozen snapshot of the thread index for
|
|
4426
|
+
// its whole life. Same reasoning that moved `resume`/`fork` in #648 (#659).
|
|
4427
|
+
if (isCodexAppServerCommand(rawArgs)) {
|
|
4428
|
+
return startAppHelperContext({
|
|
4429
|
+
// A resident server owns its proxy for its whole lifetime.
|
|
4430
|
+
detachOnExit: false,
|
|
4431
|
+
useCanonicalHome: true,
|
|
4432
|
+
// Nothing here spawns `codex app-server` through `CODEX_CLI_PATH`; the
|
|
4433
|
+
// server is launched with the rotation overrides already on its command
|
|
4434
|
+
// line. See the shim gate in `runRuntimeRotationAppHelper`.
|
|
4435
|
+
installAppServerShim: false,
|
|
4436
|
+
proxyAppServerAccountRead: true,
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4351
4439
|
if (isCodexAppCommand(rawArgs)) {
|
|
4352
|
-
return
|
|
4440
|
+
return startAppHelperContext();
|
|
4353
4441
|
}
|
|
4354
4442
|
if (
|
|
4355
4443
|
isCodexInteractiveTuiCommand(rawArgs) ||
|
|
4356
4444
|
isCodexInteractiveResumeCommand(rawArgs)
|
|
4357
4445
|
) {
|
|
4358
|
-
return
|
|
4446
|
+
return startAppHelperContext({
|
|
4359
4447
|
detachOnExit: true,
|
|
4360
4448
|
useCanonicalHome: true,
|
|
4361
4449
|
});
|