codex-multi-auth 2.1.13-beta.2 → 2.2.1
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.
- package/.codex-plugin/plugin.json +9 -0
- package/README.md +5 -4
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +30 -1
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/device-auth.d.ts.map +1 -1
- package/dist/lib/auth/device-auth.js +15 -0
- package/dist/lib/auth/device-auth.js.map +1 -1
- package/dist/lib/auth/org-override.d.ts +19 -0
- package/dist/lib/auth/org-override.d.ts.map +1 -0
- package/dist/lib/auth/org-override.js +23 -0
- package/dist/lib/auth/org-override.js.map +1 -0
- package/dist/lib/auth/server.d.ts.map +1 -1
- package/dist/lib/auth/server.js +12 -7
- package/dist/lib/auth/server.js.map +1 -1
- package/dist/lib/capability-policy.d.ts.map +1 -1
- package/dist/lib/capability-policy.js +5 -0
- package/dist/lib/capability-policy.js.map +1 -1
- package/dist/lib/circuit-breaker.d.ts +6 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +8 -0
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/codex-cli/state.d.ts.map +1 -1
- package/dist/lib/codex-cli/state.js +28 -7
- package/dist/lib/codex-cli/state.js.map +1 -1
- package/dist/lib/codex-manager/account-manager-commands.d.ts +14 -0
- package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -0
- package/dist/lib/codex-manager/account-manager-commands.js +43 -0
- package/dist/lib/codex-manager/account-manager-commands.js.map +1 -0
- package/dist/lib/codex-manager/commands/best.d.ts +1 -1
- package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/best.js +4 -3
- package/dist/lib/codex-manager/commands/best.js.map +1 -1
- package/dist/lib/codex-manager/commands/budget.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/budget.js +6 -0
- package/dist/lib/codex-manager/commands/budget.js.map +1 -1
- package/dist/lib/codex-manager/commands/debug-bundle.d.ts +16 -0
- package/dist/lib/codex-manager/commands/debug-bundle.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/debug-bundle.js +80 -5
- package/dist/lib/codex-manager/commands/debug-bundle.js.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts +1 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +11 -8
- package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
- package/dist/lib/codex-manager/commands/integrations.js +2 -2
- package/dist/lib/codex-manager/commands/integrations.js.map +1 -1
- package/dist/lib/codex-manager/commands/models.js +3 -3
- package/dist/lib/codex-manager/commands/models.js.map +1 -1
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +11 -9
- package/dist/lib/codex-manager/commands/report.js.map +1 -1
- package/dist/lib/codex-manager/commands/status.d.ts +2 -0
- package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/status.js +84 -25
- package/dist/lib/codex-manager/commands/status.js.map +1 -1
- package/dist/lib/codex-manager/commands/switch.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/switch.js +7 -0
- package/dist/lib/codex-manager/commands/switch.js.map +1 -1
- package/dist/lib/codex-manager/commands/workspace.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/workspace.js +12 -1
- package/dist/lib/codex-manager/commands/workspace.js.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.d.ts.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.js +13 -3
- package/dist/lib/codex-manager/experimental-settings-prompt.js.map +1 -1
- package/dist/lib/codex-manager/forecast-report-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/forecast-report-commands.js +14 -13
- package/dist/lib/codex-manager/forecast-report-commands.js.map +1 -1
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +7 -5
- package/dist/lib/codex-manager/help.js.map +1 -1
- package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/repair-commands.js +18 -8
- package/dist/lib/codex-manager/repair-commands.js.map +1 -1
- package/dist/lib/codex-manager/settings-hub/experimental.d.ts.map +1 -1
- package/dist/lib/codex-manager/settings-hub/experimental.js +8 -0
- package/dist/lib/codex-manager/settings-hub/experimental.js.map +1 -1
- package/dist/lib/codex-manager.d.ts +3 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +121 -93
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +327 -18
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-overflow.d.ts +7 -2
- package/dist/lib/context-overflow.d.ts.map +1 -1
- package/dist/lib/context-overflow.js +55 -37
- package/dist/lib/context-overflow.js.map +1 -1
- package/dist/lib/errors.d.ts +16 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +23 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/forecast.d.ts +1 -0
- package/dist/lib/forecast.d.ts.map +1 -1
- package/dist/lib/forecast.js +25 -2
- package/dist/lib/forecast.js.map +1 -1
- package/dist/lib/integration-generators.d.ts.map +1 -1
- package/dist/lib/integration-generators.js +1 -1
- package/dist/lib/integration-generators.js.map +1 -1
- package/dist/lib/local-bridge.d.ts +8 -0
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/local-bridge.js +81 -7
- package/dist/lib/local-bridge.js.map +1 -1
- package/dist/lib/local-client-tokens.d.ts.map +1 -1
- package/dist/lib/local-client-tokens.js +117 -64
- package/dist/lib/local-client-tokens.js.map +1 -1
- package/dist/lib/logger.d.ts +9 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +104 -19
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/model-capability-matrix.d.ts.map +1 -1
- package/dist/lib/model-capability-matrix.js +13 -10
- package/dist/lib/model-capability-matrix.js.map +1 -1
- package/dist/lib/oc-chatgpt-orchestrator.d.ts +7 -1
- package/dist/lib/oc-chatgpt-orchestrator.d.ts.map +1 -1
- package/dist/lib/oc-chatgpt-orchestrator.js +68 -9
- package/dist/lib/oc-chatgpt-orchestrator.js.map +1 -1
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +23 -1
- package/dist/lib/policy/runtime-policy.js.map +1 -1
- package/dist/lib/prompts/codex.d.ts +1 -1
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +128 -33
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/prompts/fetch-utils.d.ts +72 -0
- package/dist/lib/prompts/fetch-utils.d.ts.map +1 -0
- package/dist/lib/prompts/fetch-utils.js +165 -0
- package/dist/lib/prompts/fetch-utils.js.map +1 -0
- package/dist/lib/prompts/host-codex-prompt.d.ts.map +1 -1
- package/dist/lib/prompts/host-codex-prompt.js +24 -10
- package/dist/lib/prompts/host-codex-prompt.js.map +1 -1
- package/dist/lib/quota-cache.d.ts.map +1 -1
- package/dist/lib/quota-cache.js +26 -2
- package/dist/lib/quota-cache.js.map +1 -1
- package/dist/lib/quota-probe.d.ts +21 -0
- package/dist/lib/quota-probe.d.ts.map +1 -1
- package/dist/lib/quota-probe.js +41 -1
- package/dist/lib/quota-probe.js.map +1 -1
- package/dist/lib/quota-readiness.d.ts +3 -1
- package/dist/lib/quota-readiness.d.ts.map +1 -1
- package/dist/lib/quota-readiness.js +16 -3
- package/dist/lib/quota-readiness.js.map +1 -1
- package/dist/lib/recovery/storage.d.ts +7 -0
- package/dist/lib/recovery/storage.d.ts.map +1 -1
- package/dist/lib/recovery/storage.js +178 -10
- package/dist/lib/recovery/storage.js.map +1 -1
- package/dist/lib/refresh-lease.d.ts +1 -1
- package/dist/lib/refresh-lease.d.ts.map +1 -1
- package/dist/lib/refresh-lease.js +25 -3
- package/dist/lib/refresh-lease.js.map +1 -1
- package/dist/lib/refresh-queue.d.ts.map +1 -1
- package/dist/lib/refresh-queue.js +31 -17
- package/dist/lib/refresh-queue.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +0 -5
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +39 -7
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts +1 -0
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
- package/dist/lib/request/helpers/model-map.js +10 -0
- package/dist/lib/request/helpers/model-map.js.map +1 -1
- package/dist/lib/rotation.d.ts +12 -0
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +36 -0
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/routing-mutex.d.ts +12 -0
- package/dist/lib/routing-mutex.d.ts.map +1 -1
- package/dist/lib/routing-mutex.js +38 -1
- package/dist/lib/routing-mutex.js.map +1 -1
- package/dist/lib/runtime/account-check-types.d.ts +1 -0
- package/dist/lib/runtime/account-check-types.d.ts.map +1 -1
- package/dist/lib/runtime/account-check-types.js +1 -0
- package/dist/lib/runtime/account-check-types.js.map +1 -1
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +15 -4
- package/dist/lib/runtime/account-check.js.map +1 -1
- package/dist/lib/runtime/quota-probe.d.ts.map +1 -1
- package/dist/lib/runtime/quota-probe.js +2 -5
- package/dist/lib/runtime/quota-probe.js.map +1 -1
- package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-observability.js +24 -2
- package/dist/lib/runtime/runtime-observability.js.map +1 -1
- package/dist/lib/runtime-rotation-proxy.d.ts +24 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +289 -26
- package/dist/lib/runtime-rotation-proxy.js.map +1 -1
- package/dist/lib/storage/account-clear.d.ts.map +1 -1
- package/dist/lib/storage/account-clear.js +4 -2
- package/dist/lib/storage/account-clear.js.map +1 -1
- package/dist/lib/storage/account-persistence.d.ts.map +1 -1
- package/dist/lib/storage/account-persistence.js +11 -1
- package/dist/lib/storage/account-persistence.js.map +1 -1
- package/dist/lib/storage/flagged-storage-io.d.ts.map +1 -1
- package/dist/lib/storage/flagged-storage-io.js +4 -1
- package/dist/lib/storage/flagged-storage-io.js.map +1 -1
- package/dist/lib/storage/import-export.d.ts.map +1 -1
- package/dist/lib/storage/import-export.js +29 -9
- package/dist/lib/storage/import-export.js.map +1 -1
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +93 -0
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage/record-utils.d.ts.map +1 -1
- package/dist/lib/storage/record-utils.js +6 -1
- package/dist/lib/storage/record-utils.js.map +1 -1
- package/dist/lib/storage/storage-parser.d.ts.map +1 -1
- package/dist/lib/storage/storage-parser.js +7 -1
- package/dist/lib/storage/storage-parser.js.map +1 -1
- package/dist/lib/storage/transactions.d.ts +8 -0
- package/dist/lib/storage/transactions.d.ts.map +1 -1
- package/dist/lib/storage/transactions.js +14 -1
- package/dist/lib/storage/transactions.js.map +1 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +59 -18
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/table-formatter.d.ts.map +1 -1
- package/dist/lib/table-formatter.js +20 -2
- package/dist/lib/table-formatter.js.map +1 -1
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +8 -2
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/lib/ui/display-width.d.ts +27 -0
- package/dist/lib/ui/display-width.d.ts.map +1 -0
- package/dist/lib/ui/display-width.js +194 -0
- package/dist/lib/ui/display-width.js.map +1 -0
- package/dist/lib/ui/select.d.ts +23 -0
- package/dist/lib/ui/select.d.ts.map +1 -1
- package/dist/lib/ui/select.js +64 -16
- package/dist/lib/ui/select.js.map +1 -1
- package/dist/lib/ui/theme.d.ts +13 -0
- package/dist/lib/ui/theme.d.ts.map +1 -1
- package/dist/lib/ui/theme.js +34 -1
- package/dist/lib/ui/theme.js.map +1 -1
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -0
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -0
- package/node_modules/@codex-ai/sdk/package.json +8 -0
- package/package.json +10 -6
- package/scripts/codex-bin-resolver.js +28 -1
- package/scripts/codex-routing.js +3 -0
- package/scripts/codex.js +508 -1
- package/scripts/mcodex.js +367 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
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",
|
|
@@ -114,11 +114,13 @@
|
|
|
114
114
|
"prepare": "husky"
|
|
115
115
|
},
|
|
116
116
|
"bin": {
|
|
117
|
+
"mcodex": "scripts/mcodex.js",
|
|
117
118
|
"codex-multi-auth-codex": "scripts/codex.js",
|
|
118
119
|
"codex-multi-auth-app-launcher": "scripts/codex-app-launcher.js",
|
|
119
120
|
"codex-multi-auth": "scripts/codex-multi-auth.js"
|
|
120
121
|
},
|
|
121
122
|
"files": [
|
|
123
|
+
".codex-plugin/plugin.json",
|
|
122
124
|
"dist/",
|
|
123
125
|
"assets/",
|
|
124
126
|
"config/",
|
|
@@ -128,6 +130,7 @@
|
|
|
128
130
|
"scripts/codex-bin-resolver.js",
|
|
129
131
|
"scripts/codex-multi-auth.js",
|
|
130
132
|
"scripts/codex-routing.js",
|
|
133
|
+
"scripts/mcodex.js",
|
|
131
134
|
"scripts/install-codex-auth-utils.js",
|
|
132
135
|
"scripts/postinstall.js",
|
|
133
136
|
"scripts/preuninstall.js",
|
|
@@ -137,7 +140,8 @@
|
|
|
137
140
|
"LICENSE"
|
|
138
141
|
],
|
|
139
142
|
"bundleDependencies": [
|
|
140
|
-
"@codex-ai/plugin"
|
|
143
|
+
"@codex-ai/plugin",
|
|
144
|
+
"@codex-ai/sdk"
|
|
141
145
|
],
|
|
142
146
|
"lint-staged": {
|
|
143
147
|
"*.ts": [
|
|
@@ -154,23 +158,23 @@
|
|
|
154
158
|
"typescript": "^5"
|
|
155
159
|
},
|
|
156
160
|
"devDependencies": {
|
|
157
|
-
"@codex-ai/sdk": "file:vendor/codex-ai-sdk",
|
|
158
161
|
"@fast-check/vitest": "^0.2.4",
|
|
159
162
|
"@types/node": "^25.3.0",
|
|
160
163
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
161
164
|
"@typescript-eslint/parser": "^8.56.0",
|
|
162
|
-
"@vitest/coverage-v8": "^4.
|
|
163
|
-
"@vitest/ui": "^4.
|
|
165
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
166
|
+
"@vitest/ui": "^4.1.8",
|
|
164
167
|
"eslint": "^10.0.0",
|
|
165
168
|
"fast-check": "^4.5.3",
|
|
166
169
|
"husky": "^9.1.7",
|
|
167
170
|
"lint-staged": "^16.2.7",
|
|
168
171
|
"typescript": "^5.9.3",
|
|
169
172
|
"typescript-language-server": "^5.1.3",
|
|
170
|
-
"vitest": "^4.
|
|
173
|
+
"vitest": "^4.1.8"
|
|
171
174
|
},
|
|
172
175
|
"dependencies": {
|
|
173
176
|
"@codex-ai/plugin": "file:vendor/codex-ai-plugin",
|
|
177
|
+
"@codex-ai/sdk": "file:vendor/codex-ai-sdk",
|
|
174
178
|
"@openauthjs/openauth": "^0.4.3",
|
|
175
179
|
"hono": "4.12.18",
|
|
176
180
|
"undici": "6.25.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { basename, delimiter, dirname, extname, join } from "node:path";
|
|
4
|
+
import { basename, delimiter, dirname, extname, isAbsolute, join, relative } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
@@ -25,6 +25,21 @@ function normalizeResolvedPath(candidatePath, realpathSyncImpl) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Defense-in-depth self-recursion guard. The exact-realpath check only blocks
|
|
29
|
+
// the wrapper's own codex.js (the POSIX symlink case). If the wrapper were ever
|
|
30
|
+
// exposed as a native codex/codex.exe sitting alongside it, that exact-path
|
|
31
|
+
// check would miss it. Skipping any candidate that resolves inside the
|
|
32
|
+
// wrapper's own directory closes that latent self-loop.
|
|
33
|
+
function isWithinDirectory(candidatePath, directoryPath, realpathSyncImpl) {
|
|
34
|
+
if (!directoryPath) return false;
|
|
35
|
+
const resolvedCandidate = normalizeResolvedPath(candidatePath, realpathSyncImpl);
|
|
36
|
+
const relativePath = relative(directoryPath, resolvedCandidate);
|
|
37
|
+
return (
|
|
38
|
+
relativePath === "" ||
|
|
39
|
+
(!relativePath.startsWith("..") && !isAbsolute(relativePath))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
function resolveWrapperScriptPath(moduleUrl, realpathSyncImpl) {
|
|
29
44
|
return normalizeResolvedPath(fileURLToPath(moduleUrl), realpathSyncImpl);
|
|
30
45
|
}
|
|
@@ -92,6 +107,12 @@ function resolveCodexExecutableFromPath(
|
|
|
92
107
|
) {
|
|
93
108
|
continue;
|
|
94
109
|
}
|
|
110
|
+
if (
|
|
111
|
+
selfScriptPath &&
|
|
112
|
+
isWithinDirectory(candidate, dirname(selfScriptPath), realpathSyncImpl)
|
|
113
|
+
) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
95
116
|
return candidate;
|
|
96
117
|
}
|
|
97
118
|
}
|
|
@@ -154,6 +175,12 @@ function resolveCodexExecutableFromSystemPath(
|
|
|
154
175
|
) {
|
|
155
176
|
continue;
|
|
156
177
|
}
|
|
178
|
+
if (
|
|
179
|
+
selfScriptPath &&
|
|
180
|
+
isWithinDirectory(candidate, dirname(selfScriptPath), realpathSyncImpl)
|
|
181
|
+
) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
157
184
|
const fileName = basename(candidate).toLowerCase();
|
|
158
185
|
if (fileName === "codex" || fileName === "codex.exe") {
|
|
159
186
|
return candidate;
|
package/scripts/codex-routing.js
CHANGED
|
@@ -3,6 +3,8 @@ const AUTH_SUBCOMMANDS = new Set([
|
|
|
3
3
|
"list",
|
|
4
4
|
"status",
|
|
5
5
|
"switch",
|
|
6
|
+
"unpin",
|
|
7
|
+
"workspace",
|
|
6
8
|
"best",
|
|
7
9
|
"check",
|
|
8
10
|
"features",
|
|
@@ -13,6 +15,7 @@ const AUTH_SUBCOMMANDS = new Set([
|
|
|
13
15
|
"report",
|
|
14
16
|
"fix",
|
|
15
17
|
"doctor",
|
|
18
|
+
"uninstall",
|
|
16
19
|
"account",
|
|
17
20
|
"budget",
|
|
18
21
|
"bridge",
|
package/scripts/codex.js
CHANGED
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
symlinkSync,
|
|
19
19
|
writeFileSync,
|
|
20
20
|
} from "node:fs";
|
|
21
|
+
import { rm as rmAsync } from "node:fs/promises";
|
|
21
22
|
import { createRequire } from "node:module";
|
|
22
23
|
import { homedir, tmpdir } from "node:os";
|
|
23
|
-
import { basename, delimiter, dirname, join, resolve as resolvePath } from "node:path";
|
|
24
|
+
import { basename, delimiter, dirname, join, resolve as resolvePath, sep } from "node:path";
|
|
24
25
|
import process from "node:process";
|
|
25
26
|
import { StringDecoder } from "node:string_decoder";
|
|
26
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -81,6 +82,9 @@ const APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS = 15_000;
|
|
|
81
82
|
const APP_SERVER_SHIM_DIR_NAME = "app-server-shims";
|
|
82
83
|
const APP_SERVER_SHIM_HELPER_PREFIX = "helper-";
|
|
83
84
|
const DEFAULT_STARTUP_UPDATE_NOTICE_BUDGET_MS = 3_000;
|
|
85
|
+
const DEFAULT_STATUS_QUOTA_REFRESH_INTERVAL_MS = 10 * 60 * 1000;
|
|
86
|
+
const STATUS_QUOTA_REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
87
|
+
const STATUS_QUOTA_REFRESH_LOCK_DIR = "status-quota-refresh.lock";
|
|
84
88
|
const STARTUP_UPDATE_NOTICE_TIMED_OUT = Symbol("startup-update-notice-timed-out");
|
|
85
89
|
let shadowHomeCleanupBusyFailuresRemaining = Number.parseInt(
|
|
86
90
|
process.env.CODEX_MULTI_AUTH_TEST_SHADOW_CLEANUP_BUSY_FAILURES ?? "0",
|
|
@@ -163,6 +167,28 @@ function removeDirectoryWithRetry(targetPath) {
|
|
|
163
167
|
}
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Best-effort async directory removal for hot-path callbacks (e.g. the
|
|
172
|
+
* status-refresh child's close/error handlers, which fire on the PARENT event
|
|
173
|
+
* loop while Codex is running). Unlike removeDirectoryWithRetry this never calls
|
|
174
|
+
* the Atomics.wait-backed sleepSync, so a retryable Windows lock (EBUSY/EPERM/
|
|
175
|
+
* ENOTEMPTY) can't stall the event loop for up to ~200ms. fsPromises.rm has its
|
|
176
|
+
* own internal retry (maxRetries) and yields between attempts. On persistent
|
|
177
|
+
* failure we give up silently — the 10-minute stale-lock recovery reclaims it.
|
|
178
|
+
*/
|
|
179
|
+
async function removeDirectoryBestEffortAsync(targetPath) {
|
|
180
|
+
try {
|
|
181
|
+
await rmAsync(targetPath, {
|
|
182
|
+
recursive: true,
|
|
183
|
+
force: true,
|
|
184
|
+
maxRetries: 3,
|
|
185
|
+
retryDelay: 20,
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
// Best-effort; stale-lock recovery handles any leftover.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
function hydrateCliVersionEnv() {
|
|
167
193
|
try {
|
|
168
194
|
const require = createRequire(import.meta.url);
|
|
@@ -176,6 +202,485 @@ function hydrateCliVersionEnv() {
|
|
|
176
202
|
}
|
|
177
203
|
}
|
|
178
204
|
|
|
205
|
+
function readJsonFileQuiet(path) {
|
|
206
|
+
try {
|
|
207
|
+
if (!existsSync(path)) return null;
|
|
208
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveMultiAuthDirFromEnv(env = process.env) {
|
|
215
|
+
const configured = (env.CODEX_MULTI_AUTH_DIR ?? "").trim();
|
|
216
|
+
if (configured.length > 0) return configured;
|
|
217
|
+
return join(resolveCodexHomeDir(env), "multi-auth");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function resolveAccountsPath(env = process.env, dir = resolveMultiAuthDirFromEnv(env)) {
|
|
221
|
+
return join(dir, "openai-codex-accounts.json");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveQuotaCachePath(env = process.env, dir = resolveMultiAuthDirFromEnv(env)) {
|
|
225
|
+
return join(dir, "quota-cache.json");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolveRuntimeObservabilityPath(env = process.env, dir = resolveMultiAuthDirFromEnv(env)) {
|
|
229
|
+
return join(dir, "runtime-observability.json");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Resolve the multi-auth dir the status line should read ACCOUNTS from, mirroring
|
|
234
|
+
* the runtime's own account scoping (lib/runtime/account-scope.ts):
|
|
235
|
+
* - when perProjectAccounts is enabled, Codex CLI sync is OFF, an explicit
|
|
236
|
+
* CODEX_MULTI_AUTH_DIR is NOT set, and the cwd resolves to a git/project root,
|
|
237
|
+
* accounts live in the per-project pool at
|
|
238
|
+
* <configDir>/projects/<project-key>/openai-codex-accounts.json;
|
|
239
|
+
* - otherwise the global dir is used.
|
|
240
|
+
* Only the ACCOUNTS pool is per-project; quota-cache.json and
|
|
241
|
+
* runtime-observability.json remain global (lib: getCodexMultiAuthDir). Reusing
|
|
242
|
+
* the built dist helpers (never re-deriving project keys from raw paths, per
|
|
243
|
+
* AGENTS.md) keeps the status line consistent with what Codex routes through.
|
|
244
|
+
* Any failure falls back to the global dir so the launcher never breaks.
|
|
245
|
+
*/
|
|
246
|
+
async function resolveStatusAccountsDir(env = process.env) {
|
|
247
|
+
const globalDir = resolveMultiAuthDirFromEnv(env);
|
|
248
|
+
try {
|
|
249
|
+
const [configMod, pathsMod, stateMod] = await Promise.all([
|
|
250
|
+
import("../dist/lib/config.js"),
|
|
251
|
+
import("../dist/lib/storage/paths.js"),
|
|
252
|
+
import("../dist/lib/codex-cli/state.js"),
|
|
253
|
+
]);
|
|
254
|
+
if (
|
|
255
|
+
typeof configMod.loadPluginConfig !== "function" ||
|
|
256
|
+
typeof configMod.getPerProjectAccounts !== "function" ||
|
|
257
|
+
typeof pathsMod.findProjectRoot !== "function" ||
|
|
258
|
+
typeof pathsMod.resolveProjectStorageIdentityRoot !== "function" ||
|
|
259
|
+
typeof pathsMod.getProjectGlobalConfigDir !== "function"
|
|
260
|
+
) {
|
|
261
|
+
return globalDir;
|
|
262
|
+
}
|
|
263
|
+
const pluginConfig = configMod.loadPluginConfig();
|
|
264
|
+
if (configMod.getPerProjectAccounts(pluginConfig) !== true) return globalDir;
|
|
265
|
+
// Codex CLI sync forces the global pool (account-scope.ts), so honor that.
|
|
266
|
+
if (typeof stateMod.isCodexCliSyncEnabled === "function" && stateMod.isCodexCliSyncEnabled()) {
|
|
267
|
+
return globalDir;
|
|
268
|
+
}
|
|
269
|
+
const projectRoot = pathsMod.findProjectRoot(process.cwd());
|
|
270
|
+
if (!projectRoot) return globalDir;
|
|
271
|
+
// getProjectGlobalConfigDir grounds the per-project pool in the dist config
|
|
272
|
+
// dir, which itself honors CODEX_MULTI_AUTH_DIR / CODEX_HOME — so the pool is
|
|
273
|
+
// nested under an explicit dir exactly as the runtime writes it. No special
|
|
274
|
+
// casing of CODEX_MULTI_AUTH_DIR here, or the status line would diverge.
|
|
275
|
+
const identityRoot = pathsMod.resolveProjectStorageIdentityRoot(projectRoot);
|
|
276
|
+
return pathsMod.getProjectGlobalConfigDir(identityRoot);
|
|
277
|
+
} catch {
|
|
278
|
+
return globalDir;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeAccountIdentifier(value) {
|
|
283
|
+
return typeof value === "string" && value.trim().length > 0
|
|
284
|
+
? value.trim().toLowerCase()
|
|
285
|
+
: "";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function findAccountIndexByIdOrEmail(accounts, id, email) {
|
|
289
|
+
const normalizedId = normalizeAccountIdentifier(id);
|
|
290
|
+
const normalizedEmail = normalizeAccountIdentifier(email);
|
|
291
|
+
for (let index = 0; index < accounts.length; index += 1) {
|
|
292
|
+
const account = accounts[index];
|
|
293
|
+
if (!account || typeof account !== "object") continue;
|
|
294
|
+
if (
|
|
295
|
+
normalizedId &&
|
|
296
|
+
normalizeAccountIdentifier(account.accountId) === normalizedId
|
|
297
|
+
) {
|
|
298
|
+
return index;
|
|
299
|
+
}
|
|
300
|
+
if (
|
|
301
|
+
normalizedEmail &&
|
|
302
|
+
normalizeAccountIdentifier(account.email) === normalizedEmail
|
|
303
|
+
) {
|
|
304
|
+
return index;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return -1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function resolveModelFamilyForStatus(model) {
|
|
311
|
+
const normalized = typeof model === "string" ? model.trim().toLowerCase() : "";
|
|
312
|
+
if (normalized.startsWith("gpt-5.2")) return "gpt-5.2";
|
|
313
|
+
if (normalized.startsWith("gpt-5.1")) return "gpt-5.1";
|
|
314
|
+
if (normalized.includes("codex-max")) return "codex-max";
|
|
315
|
+
if (normalized.includes("codex")) return "codex";
|
|
316
|
+
if (normalized.startsWith("gpt-5")) return "gpt-5-codex";
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resolveStatusAccountIndex(storage, runtime, model) {
|
|
321
|
+
const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
|
|
322
|
+
if (accounts.length === 0) return -1;
|
|
323
|
+
|
|
324
|
+
const runtimeUpdatedAt =
|
|
325
|
+
typeof runtime?.lastAccountUpdatedAt === "number"
|
|
326
|
+
? runtime.lastAccountUpdatedAt
|
|
327
|
+
: typeof runtime?.updatedAt === "number"
|
|
328
|
+
? runtime.updatedAt
|
|
329
|
+
: 0;
|
|
330
|
+
if (Date.now() - runtimeUpdatedAt <= 60 * 60 * 1000) {
|
|
331
|
+
const runtimeIndex = findAccountIndexByIdOrEmail(
|
|
332
|
+
accounts,
|
|
333
|
+
runtime?.lastAccountId,
|
|
334
|
+
runtime?.lastAccountEmail,
|
|
335
|
+
);
|
|
336
|
+
if (runtimeIndex >= 0) return runtimeIndex;
|
|
337
|
+
if (
|
|
338
|
+
typeof runtime?.lastAccountIndex === "number" &&
|
|
339
|
+
runtime.lastAccountIndex >= 0 &&
|
|
340
|
+
runtime.lastAccountIndex < accounts.length
|
|
341
|
+
) {
|
|
342
|
+
return runtime.lastAccountIndex;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const family = resolveModelFamilyForStatus(model);
|
|
347
|
+
const familyIndex =
|
|
348
|
+
family && storage?.activeIndexByFamily && typeof storage.activeIndexByFamily[family] === "number"
|
|
349
|
+
? storage.activeIndexByFamily[family]
|
|
350
|
+
: undefined;
|
|
351
|
+
if (
|
|
352
|
+
typeof familyIndex === "number" &&
|
|
353
|
+
familyIndex >= 0 &&
|
|
354
|
+
familyIndex < accounts.length
|
|
355
|
+
) {
|
|
356
|
+
return familyIndex;
|
|
357
|
+
}
|
|
358
|
+
if (
|
|
359
|
+
typeof storage?.activeIndex === "number" &&
|
|
360
|
+
storage.activeIndex >= 0 &&
|
|
361
|
+
storage.activeIndex < accounts.length
|
|
362
|
+
) {
|
|
363
|
+
return storage.activeIndex;
|
|
364
|
+
}
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function extractConfigAssignmentValue(rawConfig, key) {
|
|
369
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=\\s*([^\\n#]+)`, "m");
|
|
370
|
+
const match = rawConfig.match(pattern);
|
|
371
|
+
if (!match) return null;
|
|
372
|
+
const rawValue = (match[1] ?? "").trim();
|
|
373
|
+
const quoted = rawValue.match(/^["'](.*)["']$/);
|
|
374
|
+
return (quoted ? quoted[1] : rawValue).trim() || null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function readCodexConfigValue(env, key) {
|
|
378
|
+
const configPath = join(resolveCodexHomeDir(env), "config.toml");
|
|
379
|
+
try {
|
|
380
|
+
if (!existsSync(configPath)) return null;
|
|
381
|
+
return extractConfigAssignmentValue(readFileSync(configPath, "utf8"), key);
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function extractArgValue(args, longName, shortName) {
|
|
388
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
389
|
+
const arg = args[index];
|
|
390
|
+
if (arg === longName || (shortName && arg === shortName)) {
|
|
391
|
+
const next = args[index + 1];
|
|
392
|
+
return typeof next === "string" ? next : null;
|
|
393
|
+
}
|
|
394
|
+
if (arg.startsWith(`${longName}=`)) {
|
|
395
|
+
return arg.slice(longName.length + 1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function extractConfigOverrideValue(args, key) {
|
|
402
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
403
|
+
const arg = args[index];
|
|
404
|
+
let assignment = null;
|
|
405
|
+
if (arg === "-c" || arg === "--config") {
|
|
406
|
+
assignment = args[index + 1] ?? null;
|
|
407
|
+
} else if (arg.startsWith("-c=")) {
|
|
408
|
+
assignment = arg.slice(3);
|
|
409
|
+
} else if (arg.startsWith("--config=")) {
|
|
410
|
+
assignment = arg.slice("--config=".length);
|
|
411
|
+
}
|
|
412
|
+
if (typeof assignment !== "string") continue;
|
|
413
|
+
const separator = assignment.indexOf("=");
|
|
414
|
+
if (separator <= 0) continue;
|
|
415
|
+
if (assignment.slice(0, separator).trim() !== key) continue;
|
|
416
|
+
return assignment
|
|
417
|
+
.slice(separator + 1)
|
|
418
|
+
.trim()
|
|
419
|
+
.replace(/^["']|["']$/g, "");
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function resolveStatusModel(args, env = process.env) {
|
|
425
|
+
return (
|
|
426
|
+
extractArgValue(args, "--model", "-m") ??
|
|
427
|
+
extractConfigOverrideValue(args, "model") ??
|
|
428
|
+
readCodexConfigValue(env, "model") ??
|
|
429
|
+
"unknown-model"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function resolveStatusReasoningEffort(args, env = process.env) {
|
|
434
|
+
return (
|
|
435
|
+
extractConfigOverrideValue(args, "model_reasoning_effort") ??
|
|
436
|
+
readCodexConfigValue(env, "model_reasoning_effort") ??
|
|
437
|
+
"unknown"
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatStatusPath(cwd = process.cwd(), home = homedir()) {
|
|
442
|
+
const resolvedCwd = resolvePath(cwd);
|
|
443
|
+
const resolvedHome = resolvePath(home);
|
|
444
|
+
if (resolvedCwd === resolvedHome) return "~";
|
|
445
|
+
// Use the platform separator, not a hardcoded "/": on Windows resolvePath
|
|
446
|
+
// returns backslash paths (C:\Users\user\project), so the old "/"-anchored
|
|
447
|
+
// prefix check never matched and the cwd was never abbreviated to ~. `sep`
|
|
448
|
+
// keeps the boundary check correct on both POSIX and Windows.
|
|
449
|
+
const prefix = `${resolvedHome}${sep}`;
|
|
450
|
+
if (resolvedCwd.startsWith(prefix)) {
|
|
451
|
+
// Normalize the remainder to forward slashes for a stable, readable status
|
|
452
|
+
// line regardless of the host separator.
|
|
453
|
+
return `~/${resolvedCwd.slice(prefix.length).split(sep).join("/")}`;
|
|
454
|
+
}
|
|
455
|
+
return resolvedCwd;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function formatStatusResetTime(resetAtMs) {
|
|
459
|
+
if (typeof resetAtMs !== "number" || !Number.isFinite(resetAtMs) || resetAtMs <= 0) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const date = new Date(resetAtMs);
|
|
463
|
+
if (!Number.isFinite(date.getTime())) return null;
|
|
464
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function formatStatusResetDate(resetAtMs) {
|
|
468
|
+
if (typeof resetAtMs !== "number" || !Number.isFinite(resetAtMs) || resetAtMs <= 0) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
const date = new Date(resetAtMs);
|
|
472
|
+
if (!Number.isFinite(date.getTime())) return null;
|
|
473
|
+
return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function formatCacheAge(updatedAt) {
|
|
477
|
+
if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt) || updatedAt <= 0) {
|
|
478
|
+
return "stale";
|
|
479
|
+
}
|
|
480
|
+
const ageMs = Math.max(0, Date.now() - updatedAt);
|
|
481
|
+
if (ageMs < 60_000) return "now";
|
|
482
|
+
if (ageMs < 60 * 60_000) return `${Math.floor(ageMs / 60_000)}m`;
|
|
483
|
+
return `${Math.floor(ageMs / (60 * 60_000))}h`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function getQuotaEntryForAccount(quotaCache, account) {
|
|
487
|
+
const byAccountId =
|
|
488
|
+
quotaCache && typeof quotaCache === "object" && quotaCache.byAccountId
|
|
489
|
+
? quotaCache.byAccountId
|
|
490
|
+
: {};
|
|
491
|
+
const byEmail =
|
|
492
|
+
quotaCache && typeof quotaCache === "object" && quotaCache.byEmail
|
|
493
|
+
? quotaCache.byEmail
|
|
494
|
+
: {};
|
|
495
|
+
const accountId = typeof account?.accountId === "string" ? account.accountId : "";
|
|
496
|
+
const email = typeof account?.email === "string" ? account.email.toLowerCase() : "";
|
|
497
|
+
return byAccountId?.[accountId] ?? byEmail?.[email] ?? null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function formatUsageWindow(label, window, resetFormatter) {
|
|
501
|
+
const used =
|
|
502
|
+
typeof window?.usedPercent === "number" && Number.isFinite(window.usedPercent)
|
|
503
|
+
? Math.max(0, Math.min(100, Math.round(window.usedPercent)))
|
|
504
|
+
: null;
|
|
505
|
+
const reset = resetFormatter(window?.resetAtMs);
|
|
506
|
+
if (used === null && !reset) return null;
|
|
507
|
+
if (used === null) return `${label} resets ${reset}`;
|
|
508
|
+
if (!reset) return `${label} ${used}%`;
|
|
509
|
+
return `${label} ${used}% ${reset}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function formatUsageSegment(entry) {
|
|
513
|
+
const primary = formatUsageWindow("5h", entry?.primary, formatStatusResetTime);
|
|
514
|
+
const secondary = formatUsageWindow("week", entry?.secondary, formatStatusResetDate);
|
|
515
|
+
const parts = [primary, secondary].filter(Boolean);
|
|
516
|
+
return parts.length > 0 ? parts.join(" | ") : "usage cached";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function formatPlan(planType) {
|
|
520
|
+
if (typeof planType !== "string" || planType.trim().length === 0) return "Plan?";
|
|
521
|
+
const normalized = planType.trim();
|
|
522
|
+
if (normalized.length <= 1) return normalized.toUpperCase();
|
|
523
|
+
return `${normalized[0].toUpperCase()}${normalized.slice(1).toLowerCase()}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function shouldShowForwardStatus(args, env = process.env) {
|
|
527
|
+
const override = (env.CODEX_MULTI_AUTH_STATUSLINE ?? "").trim().toLowerCase();
|
|
528
|
+
if (new Set(["0", "false", "no", "off"]).has(override)) return false;
|
|
529
|
+
if (new Set(["1", "true", "yes", "on"]).has(override)) return true;
|
|
530
|
+
if ((env.CODEX_MULTI_AUTH_STATUS_REFRESH_CHILD ?? "").trim() === "1") return false;
|
|
531
|
+
if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V")) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
return process.stderr.isTTY === true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function formatForwardStatusLine(rawArgs, env = process.env, accountsDir = resolveMultiAuthDirFromEnv(env)) {
|
|
538
|
+
// Only the accounts pool is per-project; quota-cache.json and
|
|
539
|
+
// runtime-observability.json are global (lib: getCodexMultiAuthDir), so read
|
|
540
|
+
// accounts from the (possibly project-scoped) dir and the rest from global.
|
|
541
|
+
const storage = readJsonFileQuiet(resolveAccountsPath(env, accountsDir));
|
|
542
|
+
const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
|
|
543
|
+
if (accounts.length === 0) return null;
|
|
544
|
+
|
|
545
|
+
const runtime = readJsonFileQuiet(resolveRuntimeObservabilityPath(env));
|
|
546
|
+
const quotaCache = readJsonFileQuiet(resolveQuotaCachePath(env));
|
|
547
|
+
const model = resolveStatusModel(rawArgs, env);
|
|
548
|
+
const effort = resolveStatusReasoningEffort(rawArgs, env);
|
|
549
|
+
const accountIndex = resolveStatusAccountIndex(storage, runtime, model);
|
|
550
|
+
const account = accounts[accountIndex];
|
|
551
|
+
if (!account || typeof account !== "object") return null;
|
|
552
|
+
|
|
553
|
+
const quotaEntry = getQuotaEntryForAccount(quotaCache, account);
|
|
554
|
+
const email = typeof account.email === "string" && account.email.trim()
|
|
555
|
+
? account.email.trim()
|
|
556
|
+
: `Account ${accountIndex + 1}`;
|
|
557
|
+
const plan = formatPlan(quotaEntry?.planType);
|
|
558
|
+
const usage = formatUsageSegment(quotaEntry);
|
|
559
|
+
const cacheAge = formatCacheAge(quotaEntry?.updatedAt);
|
|
560
|
+
const parts = [
|
|
561
|
+
"codex-multi-auth",
|
|
562
|
+
`${model} ${effort}`,
|
|
563
|
+
formatStatusPath(),
|
|
564
|
+
`Account ${accountIndex + 1}`,
|
|
565
|
+
usage,
|
|
566
|
+
`${email}(${plan})`,
|
|
567
|
+
`cache ${cacheAge}`,
|
|
568
|
+
];
|
|
569
|
+
return parts.join(" | ");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function maybePrintForwardStatusLine(rawArgs, env = process.env) {
|
|
573
|
+
if (!shouldShowForwardStatus(rawArgs, env)) return;
|
|
574
|
+
const accountsDir = await resolveStatusAccountsDir(env);
|
|
575
|
+
const line = formatForwardStatusLine(rawArgs, env, accountsDir);
|
|
576
|
+
if (!line) return;
|
|
577
|
+
process.stderr.write(`${line}\n`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseDurationMs(value, fallback) {
|
|
581
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
582
|
+
if (trimmed.length === 0) return fallback;
|
|
583
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
584
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
585
|
+
return parsed;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function quotaCacheNeedsRefresh(env = process.env) {
|
|
589
|
+
const intervalMs = parseDurationMs(
|
|
590
|
+
env.CODEX_MULTI_AUTH_STATUS_QUOTA_REFRESH_INTERVAL_MS,
|
|
591
|
+
DEFAULT_STATUS_QUOTA_REFRESH_INTERVAL_MS,
|
|
592
|
+
);
|
|
593
|
+
if (intervalMs <= 0) return false;
|
|
594
|
+
const cache = readJsonFileQuiet(resolveQuotaCachePath(env));
|
|
595
|
+
const entries = [
|
|
596
|
+
...Object.values(cache?.byAccountId ?? {}),
|
|
597
|
+
...Object.values(cache?.byEmail ?? {}),
|
|
598
|
+
].filter((entry) => entry && typeof entry === "object");
|
|
599
|
+
if (entries.length === 0) return true;
|
|
600
|
+
const newest = Math.max(
|
|
601
|
+
...entries.map((entry) =>
|
|
602
|
+
typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt)
|
|
603
|
+
? entry.updatedAt
|
|
604
|
+
: 0,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
return newest <= 0 || Date.now() - newest >= intervalMs;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function acquireStatusRefreshLock(env = process.env) {
|
|
611
|
+
const lockPath = join(resolveMultiAuthDirFromEnv(env), STATUS_QUOTA_REFRESH_LOCK_DIR);
|
|
612
|
+
try {
|
|
613
|
+
mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
614
|
+
mkdirSync(lockPath);
|
|
615
|
+
writeFileSync(
|
|
616
|
+
join(lockPath, "owner.json"),
|
|
617
|
+
`${JSON.stringify({ pid: process.pid, createdAt: Date.now() })}\n`,
|
|
618
|
+
{ mode: 0o600 },
|
|
619
|
+
);
|
|
620
|
+
return { lockPath, acquired: true };
|
|
621
|
+
} catch {
|
|
622
|
+
try {
|
|
623
|
+
const stat = statSync(lockPath);
|
|
624
|
+
if (Date.now() - stat.mtimeMs > STATUS_QUOTA_REFRESH_LOCK_STALE_MS) {
|
|
625
|
+
// Known, bounded TOCTOU: two processes that both observe a stale lock
|
|
626
|
+
// will both rmSync({force:true}) (both succeed) then race on mkdirSync;
|
|
627
|
+
// only one wins, so dual lock acquisition is still prevented. The
|
|
628
|
+
// residual risk is evicting a refresh owner that is merely SLOW (alive,
|
|
629
|
+
// holding the lock past the stale threshold) rather than dead, which can
|
|
630
|
+
// briefly yield two `forecast --live --json` children. That is benign
|
|
631
|
+
// here — the refresh is idempotent, read-mostly, and rate-limited by the
|
|
632
|
+
// cache TTL — so we accept it rather than add a heavier liveness probe.
|
|
633
|
+
removeDirectoryWithRetry(lockPath);
|
|
634
|
+
mkdirSync(lockPath);
|
|
635
|
+
writeFileSync(
|
|
636
|
+
join(lockPath, "owner.json"),
|
|
637
|
+
`${JSON.stringify({ pid: process.pid, createdAt: Date.now(), recovered: true })}\n`,
|
|
638
|
+
{ mode: 0o600 },
|
|
639
|
+
);
|
|
640
|
+
return { lockPath, acquired: true };
|
|
641
|
+
}
|
|
642
|
+
} catch {
|
|
643
|
+
// Another process can win the race; skip refresh.
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return { lockPath, acquired: false };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function maybeRefreshQuotaCacheInBackground(env = process.env) {
|
|
650
|
+
if ((env.CODEX_MULTI_AUTH_STATUS_REFRESH_CHILD ?? "").trim() === "1") return;
|
|
651
|
+
if (!quotaCacheNeedsRefresh(env)) return;
|
|
652
|
+
const { lockPath, acquired } = acquireStatusRefreshLock(env);
|
|
653
|
+
if (!acquired) return;
|
|
654
|
+
|
|
655
|
+
const scriptPath = join(dirname(fileURLToPath(import.meta.url)), "codex-multi-auth.js");
|
|
656
|
+
// Note: the child is detached + unref'd so it outlives this short-lived launcher
|
|
657
|
+
// process. The close/error handlers below remove the lock dir, but if the parent
|
|
658
|
+
// exits before the child settles they will NOT fire — in that case the lock is
|
|
659
|
+
// reclaimed by the 10-minute stale-lock recovery in acquireStatusRefreshLock.
|
|
660
|
+
const child = spawn(
|
|
661
|
+
process.execPath,
|
|
662
|
+
[scriptPath, "forecast", "--live", "--json"],
|
|
663
|
+
{
|
|
664
|
+
env: {
|
|
665
|
+
...env,
|
|
666
|
+
CODEX_MULTI_AUTH_STATUS_REFRESH_CHILD: "1",
|
|
667
|
+
CODEX_MULTI_AUTH_STATUSLINE: "0",
|
|
668
|
+
},
|
|
669
|
+
stdio: "ignore",
|
|
670
|
+
detached: true,
|
|
671
|
+
},
|
|
672
|
+
);
|
|
673
|
+
child.once("close", () => {
|
|
674
|
+
// Async, non-blocking: these handlers fire on the parent event loop while
|
|
675
|
+
// Codex is running, so the cleanup must never block it on a Windows lock.
|
|
676
|
+
void removeDirectoryBestEffortAsync(lockPath);
|
|
677
|
+
});
|
|
678
|
+
child.once("error", () => {
|
|
679
|
+
void removeDirectoryBestEffortAsync(lockPath);
|
|
680
|
+
});
|
|
681
|
+
child.unref();
|
|
682
|
+
}
|
|
683
|
+
|
|
179
684
|
function isRotationEnableCommand(args) {
|
|
180
685
|
return args[0] === "auth" && args[1] === "rotation" && args[2] === "enable";
|
|
181
686
|
}
|
|
@@ -4262,6 +4767,8 @@ async function main() {
|
|
|
4262
4767
|
}
|
|
4263
4768
|
|
|
4264
4769
|
await autoSyncManagerActiveSelectionIfEnabled();
|
|
4770
|
+
await maybePrintForwardStatusLine(rawArgs);
|
|
4771
|
+
maybeRefreshQuotaCacheInBackground();
|
|
4265
4772
|
try {
|
|
4266
4773
|
return await withForwardedRuntimeObservability(rawArgs, () =>
|
|
4267
4774
|
forwardToRealCodex(realCodexBin, rawArgs),
|