oc-codex-multi-auth 6.4.0 → 6.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-codex-multi-auth",
3
- "version": "6.4.0",
3
+ "version": "6.5.0",
4
4
  "description": "OpenCode plugin for ChatGPT Plus/Pro OAuth with Codex/GPT-5 routing, multi-account rotation, account switching, health checks, diagnostics, and recovery tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -2,7 +2,7 @@ import { existsSync, realpathSync } from "node:fs";
2
2
  import { copyFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
 
7
7
  const PACKAGE_NAME = "oc-codex-multi-auth";
8
8
  const LEGACY_PACKAGE_NAMES = ["oc-chatgpt-multi-auth"];
@@ -13,7 +13,7 @@ const STALE_MANAGED_MODEL_KEYS = new Set([
13
13
  "gpt-5.3-codex",
14
14
  "gpt-5.4",
15
15
  ]);
16
- const STANDALONE_COMMANDS = new Set(["doctor", "status", "list", "limits", "dashboard", "health", "diag"]);
16
+ const STANDALONE_COMMANDS = new Set(["doctor", "status", "list", "limits", "dashboard", "health", "diag", "warm"]);
17
17
  const INSTALLER_COMMANDS = new Set(["install"]);
18
18
 
19
19
  function splitCommandArgv(argv) {
@@ -83,7 +83,8 @@ function printHelp() {
83
83
  " limits Show stored rate-limit state\n" +
84
84
  " dashboard Print dashboard guidance\n" +
85
85
  " health Check local token/account health\n" +
86
- " diag Alias for doctor --deep\n\n" +
86
+ " diag Alias for doctor --deep\n" +
87
+ " warm Open every enabled account's usage window now (one request each)\n\n" +
87
88
  `Installer usage: ${PACKAGE_NAME} [--modern|--full|--legacy] [--dry-run] [--no-cache-clear]\n\n` +
88
89
  "Default behavior:\n" +
89
90
  " - Installs/updates global config at ~/.config/opencode/opencode.json\n" +
@@ -352,6 +353,125 @@ function printStandaloneResult(command, payload, json) {
352
353
  if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
353
354
  }
354
355
 
356
+ async function loadWarmRuntime(env) {
357
+ // Reuse the COMPILED warm logic from dist/ so the standalone CLI behaves
358
+ // identically to the in-conversation codex-warm tool. dist/ ships in the
359
+ // npm package (files allowlist) and none of these modules import the
360
+ // OpenCode plugin runtime, so they load cleanly in plain Node.
361
+ const distRoot = join(repoRoot, "dist", "lib");
362
+ const toUrl = (rel) => pathToFileURL(join(distRoot, rel)).href;
363
+ try {
364
+ const [storageMod, usageMod, warmReqMod, warmMod] = await Promise.all([
365
+ import(toUrl("storage.js")),
366
+ import(toUrl("codex-usage.js")),
367
+ import(toUrl("accounts/warm-request.js")),
368
+ import(toUrl("accounts/warm.js")),
369
+ ]);
370
+ return { storageMod, usageMod, warmReqMod, warmMod };
371
+ } catch (error) {
372
+ throw new Error(
373
+ `Could not load warm runtime from dist/. Build the package first (npm run build). Cause: ${formatErrorForLog(error)}`,
374
+ );
375
+ }
376
+ }
377
+
378
+ export async function runWarmCommand(parsed, options = {}) {
379
+ const { env = process.env } = options;
380
+ const storagePath = getStandaloneStoragePath(parsed, env);
381
+
382
+ let runtime;
383
+ try {
384
+ runtime = await loadWarmRuntime(env);
385
+ } catch (error) {
386
+ const payload = { command: "warm", storagePath, error: formatErrorForLog(error) };
387
+ printWarmResult(payload, parsed.json);
388
+ return { exitCode: 1, action: "warm", storagePath };
389
+ }
390
+
391
+ const { storageMod, usageMod, warmReqMod, warmMod } = runtime;
392
+ // Point dist storage at the resolved accounts file so a refreshed token is
393
+ // persisted to the SAME file the rest of the toolchain reads.
394
+ storageMod.setStoragePathDirect(storagePath);
395
+
396
+ const storage = await storageMod.loadAccounts();
397
+ const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
398
+ if (accounts.length === 0) {
399
+ const payload = {
400
+ command: "warm",
401
+ storagePath,
402
+ totalAccounts: 0,
403
+ warmed: 0,
404
+ failed: 0,
405
+ skipped: 0,
406
+ results: [],
407
+ message: "No accounts configured.",
408
+ nextAction: "Run opencode auth login.",
409
+ };
410
+ printWarmResult(payload, parsed.json);
411
+ return { exitCode: 0, action: "warm", storagePath };
412
+ }
413
+
414
+ // Same adapter as lib/tools/codex-warm.ts createWarmOne: refresh → resolve
415
+ // account id → open the usage window; map an exhausted (quota-429) account
416
+ // to a failure so it is not reported as warmed.
417
+ const warmOne = async (account) => {
418
+ const { accessToken } = await usageMod.ensureCodexUsageAccessToken({ storage, account });
419
+ const accountId = usageMod.resolveCodexUsageAccountId({ account, accessToken });
420
+ if (!accountId) {
421
+ return { status: "failed", detail: "could not resolve account id (re-login may be required)" };
422
+ }
423
+ const result = await warmReqMod.warmAccountWindow({
424
+ accountId,
425
+ accessToken,
426
+ organizationId: account.organizationId,
427
+ });
428
+ if (result.status === "exhausted") {
429
+ return { status: "failed", detail: result.detail ?? "quota/usage limit reached" };
430
+ }
431
+ return { status: "warmed" };
432
+ };
433
+
434
+ const summary = await warmMod.warmAccounts(accounts, warmOne);
435
+ const payload = {
436
+ command: "warm",
437
+ storagePath,
438
+ totalAccounts: summary.total,
439
+ warmed: summary.warmedCount,
440
+ failed: summary.failedCount,
441
+ skipped: summary.skippedCount,
442
+ results: summary.results.map((r) => ({
443
+ index: r.index,
444
+ email: maskValue(accounts[r.index]?.email, parsed.includeSensitive),
445
+ status: r.status,
446
+ detail: r.detail,
447
+ })),
448
+ };
449
+ printWarmResult(payload, parsed.json);
450
+ return { exitCode: summary.failedCount > 0 ? 1 : 0, action: "warm", storagePath };
451
+ }
452
+
453
+ function printWarmResult(payload, json) {
454
+ if (json) {
455
+ console.log(JSON.stringify(payload, null, 2));
456
+ return;
457
+ }
458
+ console.log(`oc-codex-multi-auth warm`);
459
+ if (payload.message) console.log(payload.message);
460
+ console.log(`Storage: ${payload.storagePath}`);
461
+ if (payload.error) {
462
+ console.log(`Error: ${payload.error}`);
463
+ return;
464
+ }
465
+ console.log(`Accounts: ${payload.totalAccounts}`);
466
+ for (const r of payload.results ?? []) {
467
+ const label = r.email ? `[${r.index}] ${r.email}` : `[${r.index}]`;
468
+ const detail = r.detail ? ` — ${r.detail}` : "";
469
+ console.log(`- ${label}: ${r.status}${detail}`);
470
+ }
471
+ console.log(`Summary: ${payload.warmed} warmed, ${payload.failed} failed, ${payload.skipped} skipped`);
472
+ if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
473
+ }
474
+
355
475
  export async function runStandaloneCommand(command, argv = [], options = {}) {
356
476
  const parsed = parseStandaloneArgs(argv);
357
477
  if (command === "diag") {
@@ -362,6 +482,9 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
362
482
  printHelp();
363
483
  return { exitCode: 0, action: "help" };
364
484
  }
485
+ if (command === "warm") {
486
+ return runWarmCommand(parsed, options);
487
+ }
365
488
  const { env = process.env } = options;
366
489
  const storagePath = getStandaloneStoragePath(parsed, env);
367
490
  const { storage, error } = await readStandaloneStorage(storagePath);
@@ -19,9 +19,19 @@ export function isDirectRunPath(
19
19
  }
20
20
 
21
21
  if (isDirectRunPath()) {
22
- runInstaller().catch((error) => {
23
- const message = error instanceof Error ? error.message : String(error);
24
- console.error(`Installer failed: ${message}`);
25
- process.exit(1);
26
- });
22
+ runInstaller()
23
+ .then((result) => {
24
+ // Propagate a non-zero exit from standalone commands (e.g. `warm`
25
+ // reporting per-account failures) without throwing. Read-only
26
+ // commands return exitCode 0; only set a failure code so a clean run
27
+ // still exits 0.
28
+ if (result && typeof result.exitCode === "number" && result.exitCode !== 0) {
29
+ process.exitCode = result.exitCode;
30
+ }
31
+ })
32
+ .catch((error) => {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ console.error(`Installer failed: ${message}`);
35
+ process.exit(1);
36
+ });
27
37
  }