codex-multi-auth 2.1.13-beta.2 → 2.2.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.
- package/README.md +3 -3
- 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/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/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-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 +2 -1
- package/dist/lib/codex-manager/commands/best.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 +2 -1
- package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +2 -2
- 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/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 +3 -3
- package/dist/lib/codex-manager/forecast-report-commands.js.map +1 -1
- package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/repair-commands.js +11 -1
- 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 +53 -65
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +110 -5
- 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/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/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/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 +15 -1
- package/dist/lib/policy/runtime-policy.js.map +1 -1
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +124 -29
- 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 +10 -1
- 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 +39 -0
- 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 +36 -7
- package/dist/lib/request/fetch-helpers.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/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/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 +1 -0
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +163 -16
- 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/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/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.d.ts.map +1 -1
- package/dist/lib/storage.js +13 -9
- 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 +9 -6
- package/scripts/codex-routing.js +3 -0
- package/scripts/codex.js +508 -1
- package/scripts/mcodex +117 -0
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),
|
package/scripts/mcodex
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
monitor_interval="${MCODEX_MONITOR_INTERVAL:-5}"
|
|
5
|
+
live_accounts=0
|
|
6
|
+
tmux_history_limit="${MCODEX_TMUX_HISTORY_LIMIT:-50000}"
|
|
7
|
+
|
|
8
|
+
# Security: monitor_interval is embedded into `watch -n <n> ...` command strings
|
|
9
|
+
# that tmux hands to a shell. An attacker-controlled MCODEX_MONITOR_INTERVAL like
|
|
10
|
+
# "5; rm -rf ~" would otherwise execute. Require a positive integer (optionally
|
|
11
|
+
# with a fractional part for sub-second intervals) and fall back to the default
|
|
12
|
+
# otherwise, so no shell metacharacters can ever reach the command string.
|
|
13
|
+
if ! [[ "$monitor_interval" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
|
14
|
+
echo "mcodex: invalid MCODEX_MONITOR_INTERVAL '$monitor_interval'; using 5" >&2
|
|
15
|
+
monitor_interval=5
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Same hardening for the tmux scrollback history limit (used as a tmux option
|
|
19
|
+
# argument): must be a plain integer.
|
|
20
|
+
if ! [[ "$tmux_history_limit" =~ ^[0-9]+$ ]]; then
|
|
21
|
+
echo "mcodex: invalid MCODEX_TMUX_HISTORY_LIMIT '$tmux_history_limit'; using 50000" >&2
|
|
22
|
+
tmux_history_limit=50000
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
quote_args() {
|
|
26
|
+
local quoted=()
|
|
27
|
+
local arg
|
|
28
|
+
for arg in "$@"; do
|
|
29
|
+
printf -v arg '%q' "$arg"
|
|
30
|
+
quoted+=("$arg")
|
|
31
|
+
done
|
|
32
|
+
# Join with single spaces and NO trailing space (a trailing space would become
|
|
33
|
+
# part of the interpolated tmux command string).
|
|
34
|
+
local IFS=' '
|
|
35
|
+
printf '%s' "${quoted[*]}"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
require_watch() {
|
|
39
|
+
if ! command -v watch >/dev/null 2>&1; then
|
|
40
|
+
echo "mcodex: 'watch' is not installed; the live account monitor requires it (install procps / procps-ng)." >&2
|
|
41
|
+
return 1
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
run_monitor() {
|
|
46
|
+
require_watch || return 1
|
|
47
|
+
watch -n "$monitor_interval" 'codex-multi-auth list'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
configure_tmux_scrollback() {
|
|
51
|
+
local target="${1:-}"
|
|
52
|
+
local target_args=()
|
|
53
|
+
if [[ -n "$target" ]]; then
|
|
54
|
+
target_args=(-t "$target")
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
tmux set-option "${target_args[@]}" mouse on >/dev/null 2>&1
|
|
58
|
+
tmux set-option "${target_args[@]}" history-limit "$tmux_history_limit" >/dev/null 2>&1
|
|
59
|
+
tmux bind-key -T root WheelUpPane copy-mode -e >/dev/null 2>&1
|
|
60
|
+
tmux bind-key -T copy-mode WheelUpPane send-keys -X scroll-up >/dev/null 2>&1
|
|
61
|
+
tmux bind-key -T copy-mode WheelDownPane send-keys -X scroll-down >/dev/null 2>&1
|
|
62
|
+
tmux bind-key -T copy-mode-vi WheelUpPane send-keys -X scroll-up >/dev/null 2>&1
|
|
63
|
+
tmux bind-key -T copy-mode-vi WheelDownPane send-keys -X scroll-down >/dev/null 2>&1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if [[ "${1:-}" == "--monitor" ]]; then
|
|
67
|
+
# --monitor takes no extra args; the live account list is fixed. Propagate the
|
|
68
|
+
# watch-availability check's exit code instead of masking it with `exit 0`.
|
|
69
|
+
run_monitor
|
|
70
|
+
exit $?
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if [[ "${1:-}" == "--tmux" || "${1:-}" == "-t" ]]; then
|
|
74
|
+
shift
|
|
75
|
+
if [[ "${1:-}" == "--live-accounts" ]]; then
|
|
76
|
+
live_accounts=1
|
|
77
|
+
shift
|
|
78
|
+
fi
|
|
79
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
80
|
+
echo "mcodex: tmux is not installed; launching without tmux" >&2
|
|
81
|
+
exec codex-multi-auth-codex "$@"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [[ -n "${TMUX:-}" ]]; then
|
|
85
|
+
configure_tmux_scrollback
|
|
86
|
+
if [[ "$live_accounts" == "1" ]] && require_watch; then
|
|
87
|
+
tmux split-window -h "watch -n $monitor_interval 'codex-multi-auth list'"
|
|
88
|
+
fi
|
|
89
|
+
exec codex-multi-auth-codex "$@"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
session="${MCODEX_TMUX_SESSION:-mcodex}"
|
|
93
|
+
suffix="$(date +%H%M%S)"
|
|
94
|
+
if tmux has-session -t "$session" 2>/dev/null; then
|
|
95
|
+
session="${session}-${suffix}"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Build the inner shell-command as a single string. quote_args runs each arg
|
|
99
|
+
# through printf %q, so every token is already shell-safe (embedded quotes,
|
|
100
|
+
# spaces, $, backticks are escaped); the surrounding expansion is not
|
|
101
|
+
# re-evaluated, so the command tmux receives is exactly this string.
|
|
102
|
+
cmd="codex-multi-auth-codex"
|
|
103
|
+
args="$(quote_args "$@")"
|
|
104
|
+
if [[ -n "$args" ]]; then
|
|
105
|
+
cmd="$cmd $args"
|
|
106
|
+
fi
|
|
107
|
+
tmux new-session -d -s "$session" -n codex "$cmd"
|
|
108
|
+
configure_tmux_scrollback "$session"
|
|
109
|
+
if [[ "$live_accounts" == "1" ]] && require_watch; then
|
|
110
|
+
tmux split-window -h -t "$session:0" "watch -n $monitor_interval 'codex-multi-auth list'"
|
|
111
|
+
fi
|
|
112
|
+
tmux select-pane -t "$session:0.0"
|
|
113
|
+
tmux attach-session -t "$session"
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
exec codex-multi-auth-codex "$@"
|