codex-multi-auth 2.19.0 → 2.20.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.
Files changed (107) hide show
  1. package/.codex-plugin/plugin.json +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +149 -148
  4. package/assets/codex-multi-auth-icon.svg +39 -39
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +31 -31
  7. package/config/codex-legacy.json +1021 -1021
  8. package/config/codex-modern.json +347 -347
  9. package/config/minimal-codex.json +15 -15
  10. package/dist/index.js +1 -1
  11. package/dist/lib/accounts/rate-limits.js +5 -0
  12. package/dist/lib/accounts.js +5 -4
  13. package/dist/lib/auth/account-access.js +2 -2
  14. package/dist/lib/codex-cli/version.d.ts +22 -0
  15. package/dist/lib/codex-cli/version.js +78 -0
  16. package/dist/lib/codex-manager/commands/limits.d.ts +2 -0
  17. package/dist/lib/codex-manager/commands/limits.js +31 -0
  18. package/dist/lib/codex-manager/commands/status.js +1 -1
  19. package/dist/lib/codex-manager/dashboard-settings-data.js +2 -0
  20. package/dist/lib/codex-manager/formatters/subscription-status.d.ts +2 -2
  21. package/dist/lib/codex-manager/formatters/text-style.js +4 -1
  22. package/dist/lib/codex-manager/settings-hub/dashboard.d.ts +1 -1
  23. package/dist/lib/codex-manager/settings-hub/dashboard.js +4 -0
  24. package/dist/lib/codex-manager/settings-panels.d.ts +1 -0
  25. package/dist/lib/codex-manager/settings-panels.js +1 -0
  26. package/dist/lib/codex-manager/theme-settings-panel.d.ts +2 -1
  27. package/dist/lib/codex-manager/theme-settings-panel.js +39 -0
  28. package/dist/lib/codex-manager.js +2 -0
  29. package/dist/lib/constants.d.ts +2 -0
  30. package/dist/lib/constants.js +2 -0
  31. package/dist/lib/context-budget/model-context-windows.d.ts +1 -1
  32. package/dist/lib/context-budget/model-context-windows.js +4 -0
  33. package/dist/lib/context-budget-response.js +8 -8
  34. package/dist/lib/context-overflow.js +10 -10
  35. package/dist/lib/dashboard-settings.d.ts +2 -0
  36. package/dist/lib/dashboard-settings.js +18 -0
  37. package/dist/lib/luna-reserve.d.ts +26 -0
  38. package/dist/lib/luna-reserve.js +68 -0
  39. package/dist/lib/model-route-policy.d.ts +5 -5
  40. package/dist/lib/oauth-success.html +125 -125
  41. package/dist/lib/policy/runtime-policy.js +14 -0
  42. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  43. package/dist/lib/prompts/codex.js +79 -79
  44. package/dist/lib/quota-readiness.d.ts +2 -2
  45. package/dist/lib/request/helpers/model-map.d.ts +1 -1
  46. package/dist/lib/request/helpers/model-map.js +12 -0
  47. package/dist/lib/request/response-outcome.d.ts +1 -1
  48. package/dist/lib/request/response-outcome.js +4 -1
  49. package/dist/lib/request/url-rewriting.js +3 -1
  50. package/dist/lib/runtime/account-model-catalog.d.ts +5 -5
  51. package/dist/lib/runtime/account-reset-credits.d.ts +2 -2
  52. package/dist/lib/runtime/account-status.js +2 -1
  53. package/dist/lib/runtime/luna-reserve-usage.d.ts +9 -0
  54. package/dist/lib/runtime/luna-reserve-usage.js +68 -0
  55. package/dist/lib/runtime/model-discovery-status.d.ts +11 -7
  56. package/dist/lib/runtime/model-discovery-status.js +13 -5
  57. package/dist/lib/runtime/reset-credit-routing.js +4 -1
  58. package/dist/lib/runtime/reset-credits.d.ts +7 -7
  59. package/dist/lib/runtime/resume-picker.js +1 -1
  60. package/dist/lib/runtime/rotation-server-types.d.ts +2 -0
  61. package/dist/lib/runtime/runtime-observability.d.ts +2 -0
  62. package/dist/lib/runtime/runtime-observability.js +1 -0
  63. package/dist/lib/runtime/workspace-model-scopes.d.ts +3 -3
  64. package/dist/lib/runtime-rotation-proxy.js +200 -32
  65. package/dist/lib/schemas.d.ts +65 -65
  66. package/dist/lib/ui/index.d.ts +1 -1
  67. package/dist/lib/ui/index.js +1 -1
  68. package/dist/lib/ui/runtime.d.ts +3 -1
  69. package/dist/lib/ui/runtime.js +11 -1
  70. package/dist/lib/ui/theme.d.ts +20 -1
  71. package/dist/lib/ui/theme.js +154 -30
  72. package/dist/lib/ui/ui-copy.d.ts +3 -2
  73. package/dist/lib/ui/ui-copy.js +3 -2
  74. package/dist/lib/usage/pricing.d.ts +1 -1
  75. package/dist/lib/usage/pricing.js +4 -0
  76. package/dist/lib/usage/usage-extraction.d.ts +2 -0
  77. package/dist/lib/usage/usage-extraction.js +25 -1
  78. package/dist/lib/utils.d.ts +1 -1
  79. package/dist/lib/utils.js +1 -1
  80. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  81. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  82. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  83. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  84. package/node_modules/@codex-ai/plugin/package.json +9 -9
  85. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  86. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  87. package/node_modules/@codex-ai/sdk/package.json +8 -8
  88. package/package.json +207 -206
  89. package/scripts/codex-app-launcher.js +653 -653
  90. package/scripts/codex-app-router.js +374 -373
  91. package/scripts/codex-bin-resolver.js +332 -332
  92. package/scripts/codex-multi-auth.js +65 -65
  93. package/scripts/codex-routing.js +62 -62
  94. package/scripts/codex.js +7006 -7005
  95. package/scripts/install-codex-auth-utils.js +213 -213
  96. package/scripts/mcodex.js +367 -367
  97. package/scripts/postinstall.js +114 -114
  98. package/scripts/preuninstall.js +265 -265
  99. package/skills/codex-auth-setup/SKILL.md +73 -73
  100. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  101. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  102. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  103. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  104. package/vendor/codex-ai-plugin/package.json +9 -9
  105. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  106. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  107. package/vendor/codex-ai-sdk/package.json +8 -8
@@ -1,15 +1,15 @@
1
- {
2
- "$schema": "./schema/config.schema.json",
3
- "plugin": ["codex-multi-auth"],
4
-
5
- "provider": {
6
- "openai": {
7
- "options": {
8
- "store": false
9
- }
10
- }
11
- },
12
- "model": "openai/gpt-5.6-sol"
13
- }
14
-
15
-
1
+ {
2
+ "$schema": "./schema/config.schema.json",
3
+ "plugin": ["codex-multi-auth"],
4
+
5
+ "provider": {
6
+ "openai": {
7
+ "options": {
8
+ "store": false
9
+ }
10
+ }
11
+ },
12
+ "model": "openai/gpt-5.6-sol"
13
+ }
14
+
15
+
package/dist/index.js CHANGED
@@ -1043,7 +1043,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1043
1043
  }
1044
1044
  let response;
1045
1045
  const fetchStart = performance.now();
1046
- // Merge user AbortSignal with timeout (Node 18 compatible - no AbortSignal.any)
1046
+ // Merge user AbortSignal with timeout without AbortSignal.any (keeps listeners bounded)
1047
1047
  const fetchController = new AbortController();
1048
1048
  const requestTimeoutMs = fetchTimeoutMs;
1049
1049
  let requestTimedOut = false;
@@ -2,6 +2,7 @@
2
2
  * Rate limiting utilities for account management.
3
3
  * Extracted from accounts.ts to reduce module size and improve cohesion.
4
4
  */
5
+ import { LUNA_RESERVE_MODEL } from "../constants.js";
5
6
  import { nowMs } from "../utils.js";
6
7
  export function parseRateLimitReason(code) {
7
8
  if (!code)
@@ -61,6 +62,10 @@ export function isRateLimitedForFamily(entity, family, model) {
61
62
  if (isRateLimitedForQuotaKey(entity, modelKey)) {
62
63
  return true;
63
64
  }
65
+ // Luna Reserve is independently metered. Ordinary Luna/Codex family
66
+ // exhaustion must not suppress a separately available reserve allowance.
67
+ if (model === LUNA_RESERVE_MODEL)
68
+ return false;
64
69
  }
65
70
  const baseKey = getQuotaKey(family);
66
71
  return isRateLimitedForQuotaKey(entity, baseKey);
@@ -9,7 +9,7 @@ import { MODEL_FAMILIES } from "./request/helpers/model-map.js";
9
9
  import { getHealthTracker, getTokenTracker, resetTrackers, selectHybridAccount, } from "./rotation.js";
10
10
  import { withRoutingMutex, } from "./routing-mutex.js";
11
11
  import { nowMs } from "./utils.js";
12
- import { ERROR_MESSAGES, HTTP_STATUS, MAX_RATE_LIMIT_DELAY_MS } from "./constants.js";
12
+ import { ERROR_MESSAGES, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS } from "./constants.js";
13
13
  import { CodexAuthError } from "./errors.js";
14
14
  import { loadCodexCliState, } from "./codex-cli/state.js";
15
15
  import { syncAccountStorageFromCodexCli } from "./codex-cli/sync.js";
@@ -1062,13 +1062,14 @@ export class AccountManager {
1062
1062
  : undefined;
1063
1063
  const newLimits = { ...carried?.limits };
1064
1064
  const baseKey = getQuotaKey(family);
1065
- if (!model || reason === "quota" || reason === "unknown") {
1065
+ const independentReserve = model === LUNA_RESERVE_MODEL;
1066
+ if (!independentReserve && (!model || reason === "quota" || reason === "unknown")) {
1066
1067
  const currentResetAt = account.rateLimitResetTimes[baseKey] ?? 0;
1067
1068
  account.rateLimitResetTimes[baseKey] = Math.max(currentResetAt, resetAt);
1068
1069
  newLimits[baseKey] = Math.max(newLimits[baseKey] ?? 0, resetAt);
1069
1070
  }
1070
1071
  if (model &&
1071
- (reason === "tokens" || reason === "concurrent" || reason === "unknown")) {
1072
+ (independentReserve || reason === "tokens" || reason === "concurrent" || reason === "unknown")) {
1072
1073
  const modelKey = getQuotaKey(family, model);
1073
1074
  const currentResetAt = account.rateLimitResetTimes[modelKey] ?? 0;
1074
1075
  account.rateLimitResetTimes[modelKey] = Math.max(currentResetAt, resetAt);
@@ -1550,7 +1551,7 @@ export class AccountManager {
1550
1551
  for (const account of enabledAccounts) {
1551
1552
  const perAccountWaitTimes = [];
1552
1553
  const baseResetAt = account.rateLimitResetTimes[baseKey];
1553
- if (typeof baseResetAt === "number") {
1554
+ if (model !== LUNA_RESERVE_MODEL && typeof baseResetAt === "number") {
1554
1555
  perAccountWaitTimes.push(Math.max(0, baseResetAt - now));
1555
1556
  }
1556
1557
  if (modelKey) {
@@ -51,8 +51,8 @@ export async function fetchAuthorizedAccounts(accessToken, options = {}) {
51
51
  return null;
52
52
  const doFetch = options.fetch ?? globalThis.fetch;
53
53
  const timeout = AbortSignal.timeout(ACCOUNTS_CHECK_TIMEOUT_MS);
54
- // combineSignals wraps AbortSignal.any (available since Node 18.17, the
55
- // engines floor) with null-tolerance; the native composite also detaches
54
+ // combineSignals wraps AbortSignal.any (available on every Node the
55
+ // engines floor, `>=22.19`, permits) with null-tolerance; the native composite also detaches
56
56
  // from a long-lived caller signal once this request completes.
57
57
  const signal = options.signal
58
58
  ? combineSignals(options.signal, timeout)
@@ -0,0 +1,22 @@
1
+ interface ResolvedCodexBin {
2
+ path: string;
3
+ launchWithNode: boolean;
4
+ }
5
+ export interface CodexCliVersionDeps {
6
+ resolveBin?: () => Promise<ResolvedCodexBin | null>;
7
+ execFile?: (file: string, args: readonly string[], options: {
8
+ timeout: number;
9
+ }) => Promise<{
10
+ stdout: string;
11
+ }>;
12
+ }
13
+ export declare const CLIENT_VERSION_PATTERN: RegExp;
14
+ /** Extracts `0.160.1` from `codex --version` output such as `codex-cli 0.160.1`. */
15
+ export declare function parseCodexCliVersion(output: string): string | null;
16
+ /**
17
+ * Returns the installed Codex CLI version, or null when no binary resolves or
18
+ * the probe fails. Detection failure must never break a check: the caller just
19
+ * sends no `client_version`, exactly as before.
20
+ */
21
+ export declare function detectInstalledCodexCliVersion(deps?: CodexCliVersionDeps): Promise<string | null>;
22
+ export {};
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Detects the installed Codex CLI version for the `client_version` parameter
3
+ * the upstream models catalog requires (issue #733). The catalog filters by
4
+ * `minimal_client_version`, so the honest value is whatever `codex --version`
5
+ * reports on this host — never a hardcoded string.
6
+ */
7
+ import { execFile } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ // scripts/codex-bin-resolver.js is plain shipped JS, so it loads by URL:
11
+ // dist/lib/codex-cli/ resolves ../../../scripts, the source tree ../../scripts.
12
+ const RESOLVER_CANDIDATES = [
13
+ fileURLToPath(new URL("../../../scripts/codex-bin-resolver.js", import.meta.url)),
14
+ fileURLToPath(new URL("../../scripts/codex-bin-resolver.js", import.meta.url)),
15
+ ];
16
+ async function loadBinResolver() {
17
+ for (const candidate of RESOLVER_CANDIDATES) {
18
+ if (!existsSync(candidate))
19
+ continue;
20
+ const resolverModule = (await import(pathToFileURL(candidate).href).catch(() => null));
21
+ if (typeof resolverModule?.resolveRealCodexBin === "function") {
22
+ return resolverModule;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ async function defaultResolveBin() {
28
+ const resolverModule = await loadBinResolver();
29
+ const resolve = resolverModule?.resolveRealCodexBin;
30
+ if (!resolve)
31
+ return null;
32
+ const bin = resolve();
33
+ // PATH discovery can hand back npm's extensionless `codex` shim on Windows,
34
+ // which no spawn API can exec. The resolver knows the sibling package entry
35
+ // the shim points at; launch that through node instead.
36
+ if (bin &&
37
+ process.platform === "win32" &&
38
+ !bin.launchWithNode &&
39
+ resolverModule.isWindowsShimPath?.(bin.path)) {
40
+ const entry = resolverModule.resolveWindowsShimPackageEntry?.(bin.path);
41
+ if (entry)
42
+ return { path: entry, launchWithNode: true };
43
+ }
44
+ return bin;
45
+ }
46
+ const defaultExecFile = (file, args, options) => new Promise((resolve, reject) => {
47
+ execFile(file, [...args], { ...options, encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024 }, (error, stdout) => {
48
+ if (error)
49
+ reject(error);
50
+ else
51
+ resolve({ stdout: String(stdout) });
52
+ });
53
+ });
54
+ export const CLIENT_VERSION_PATTERN = /^[0-9A-Za-z.+_-]{1,80}$/;
55
+ /** Extracts `0.160.1` from `codex --version` output such as `codex-cli 0.160.1`. */
56
+ export function parseCodexCliVersion(output) {
57
+ const match = /(?:^|\s)v?(\d+\.\d+\.\d+[-0-9A-Za-z.+_]*)/.exec(output);
58
+ const version = match?.[1];
59
+ return version && CLIENT_VERSION_PATTERN.test(version) ? version : null;
60
+ }
61
+ /**
62
+ * Returns the installed Codex CLI version, or null when no binary resolves or
63
+ * the probe fails. Detection failure must never break a check: the caller just
64
+ * sends no `client_version`, exactly as before.
65
+ */
66
+ export async function detectInstalledCodexCliVersion(deps = {}) {
67
+ try {
68
+ const bin = await (deps.resolveBin ?? defaultResolveBin)();
69
+ if (!bin || typeof bin.path !== "string" || bin.path.length === 0)
70
+ return null;
71
+ const exec = deps.execFile ?? defaultExecFile;
72
+ const { stdout } = await exec(bin.launchWithNode ? process.execPath : bin.path, bin.launchWithNode ? [bin.path, "--version"] : ["--version"], { timeout: 10_000 });
73
+ return parseCodexCliVersion(stdout);
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
@@ -1,11 +1,13 @@
1
1
  import { type ModelFamily } from "../../prompts/codex.js";
2
2
  import type { QuotaCacheData } from "../../quota-cache.js";
3
3
  import type { AccountStorageV3 } from "../../storage.js";
4
+ import type { LunaReserveSnapshot } from "../../luna-reserve.js";
4
5
  export interface LimitsCommandDeps {
5
6
  setStoragePath: (path: string | null) => void;
6
7
  loadAccounts: () => Promise<AccountStorageV3 | null>;
7
8
  loadQuotaCache: () => Promise<QuotaCacheData>;
8
9
  refreshQuotaCache: (storage: AccountStorageV3, cache: QuotaCacheData, maxAgeMs: number) => Promise<QuotaCacheData>;
10
+ refreshLunaReserveUsage?: (storage: AccountStorageV3) => Promise<Record<number, LunaReserveSnapshot>>;
9
11
  resolveActiveIndex: (storage: AccountStorageV3, family?: ModelFamily) => number;
10
12
  getNow?: () => number;
11
13
  logInfo?: (message: string) => void;
@@ -46,6 +46,32 @@ function publicQuotaEntry(entry) {
46
46
  secondary: publicWindow(entry.secondary),
47
47
  };
48
48
  }
49
+ /** Convert one Reserve window to the stable public JSON shape. */
50
+ function publicReserveWindow(window) {
51
+ return window
52
+ ? {
53
+ usedPercent: window.usedPercent,
54
+ remainingPercent: window.remainingPercent,
55
+ windowMinutes: window.windowMinutes,
56
+ resetAtMs: window.resetAtMs,
57
+ }
58
+ : null;
59
+ }
60
+ /** Convert a Reserve snapshot to public JSON while preserving explicit unknowns. */
61
+ function publicLunaReserve(snapshot) {
62
+ if (!snapshot)
63
+ return null;
64
+ return {
65
+ observedAt: snapshot.observedAt,
66
+ offered: snapshot.offered,
67
+ available: snapshot.available,
68
+ limitId: snapshot.limitId,
69
+ limitName: snapshot.limitName,
70
+ normalModelSlug: snapshot.normalModelSlug,
71
+ primary: publicReserveWindow(snapshot.primary),
72
+ secondary: publicReserveWindow(snapshot.secondary),
73
+ };
74
+ }
49
75
  /**
50
76
  * Emit configured accounts joined to safe cached quota records.
51
77
  *
@@ -86,8 +112,12 @@ export async function runLimitsCommand(args, deps) {
86
112
  return 0;
87
113
  }
88
114
  let cache = await deps.loadQuotaCache();
115
+ let lunaReserveByAccount = {};
89
116
  if (parsed.options.refresh) {
90
117
  cache = await deps.refreshQuotaCache(storage, cache, LIMITS_REFRESH_MAX_AGE_MS);
118
+ if (deps.refreshLunaReserveUsage) {
119
+ lunaReserveByAccount = await deps.refreshLunaReserveUsage(storage);
120
+ }
91
121
  }
92
122
  const generatedAt = deps.getNow?.() ?? Date.now();
93
123
  const selection = resolveSelection(storage, deps.resolveActiveIndex);
@@ -102,6 +132,7 @@ export async function runLimitsCommand(args, deps) {
102
132
  enabled: account.enabled !== false,
103
133
  current: index === selection.routedIndex,
104
134
  quota: quota ? publicQuotaEntry(quota) : null,
135
+ lunaReserve: publicLunaReserve(lunaReserveByAccount[index]),
105
136
  };
106
137
  });
107
138
  logInfo(JSON.stringify({
@@ -279,7 +279,7 @@ export async function runStatusCommand(deps) {
279
279
  runtimeSnapshot.serverBurstCooldownUntil > now
280
280
  ? formatWaitTime(runtimeSnapshot.serverBurstCooldownUntil - now)
281
281
  : null;
282
- logInfo(`Runtime: responses=${runtimeSnapshot.responsesRequests}, refresh=${runtimeSnapshot.authRefreshRequests}, probes=${runtimeSnapshot.diagnosticProbeRequests}, budgetExhaustions=${runtimeMetrics.requestAttemptBudgetExhaustions}`);
282
+ logInfo(`Runtime: responses=${runtimeSnapshot.responsesRequests}, refresh=${runtimeSnapshot.authRefreshRequests}, probes=${runtimeSnapshot.diagnosticProbeRequests}, reserveFallbacks=${runtimeSnapshot.lunaReserveFallbacks ?? 0}, budgetExhaustions=${runtimeMetrics.requestAttemptBudgetExhaustions}`);
283
283
  const lastRuntimeAccount = formatRuntimeLastAccount(runtimeSnapshot);
284
284
  if (lastRuntimeAccount) {
285
285
  logInfo(paint(`Last runtime account: ${lastRuntimeAccount}`, "accent"));
@@ -22,6 +22,7 @@ export function cloneDashboardSettingsData(settings, deps) {
22
22
  menuSortQuickSwitchVisibleRow: settings.menuSortQuickSwitchVisibleRow ?? true,
23
23
  uiThemePreset: settings.uiThemePreset ?? "green",
24
24
  uiAccentColor: settings.uiAccentColor ?? "green",
25
+ uiColorMode: settings.uiColorMode ?? "auto",
25
26
  menuShowStatusBadge: settings.menuShowStatusBadge ?? true,
26
27
  menuShowCurrentBadge: settings.menuShowCurrentBadge ?? true,
27
28
  menuShowLastUsed: settings.menuShowLastUsed ?? true,
@@ -71,6 +72,7 @@ export function dashboardSettingsDataEqual(left, right, deps) {
71
72
  (right.menuSortQuickSwitchVisibleRow ?? true) &&
72
73
  (left.uiThemePreset ?? "green") === (right.uiThemePreset ?? "green") &&
73
74
  (left.uiAccentColor ?? "green") === (right.uiAccentColor ?? "green") &&
75
+ (left.uiColorMode ?? "auto") === (right.uiColorMode ?? "auto") &&
74
76
  (left.menuShowStatusBadge ?? true) ===
75
77
  (right.menuShowStatusBadge ?? true) &&
76
78
  (left.menuShowCurrentBadge ?? true) ===
@@ -7,8 +7,8 @@ export declare function subscriptionStatus(account: AccountMetadataV3, entry: Qu
7
7
  planLabel: string;
8
8
  selectedWorkspace: string;
9
9
  autoPrime: boolean | null;
10
- primingState: "failed" | "unknown" | "awaiting-first-use" | "timer-running" | "used" | "completed" | "not-applicable";
11
- primingFailure: "timed out" | "stream ended early" | "upstream failed" | "response too large" | "network error" | null;
10
+ primingState: "awaiting-first-use" | "completed" | "failed" | "not-applicable" | "timer-running" | "unknown" | "used";
11
+ primingFailure: "network error" | "response too large" | "stream ended early" | "timed out" | "upstream failed" | null;
12
12
  automaticBlock: string | null;
13
13
  freshness: string;
14
14
  observedAt: number | null;
@@ -8,7 +8,10 @@ export function stylePromptText(text, tone) {
8
8
  const ui = getUiRuntimeOptions();
9
9
  if (ui.v2Enabled) {
10
10
  if (tone === "muted") {
11
- return `${ui.theme.colors.dim}${paintUiText(ui, text, "muted")}${ui.theme.colors.reset}`;
11
+ // In light mode the muted foreground is already dark; stacking the
12
+ // dim attribute on top makes the text faint again on light themes.
13
+ const dim = ui.theme.colorMode === "light" ? "" : ui.theme.colors.dim;
14
+ return `${dim}${paintUiText(ui, text, "muted")}${ui.theme.colors.reset}`;
12
15
  }
13
16
  const mapped = tone === "accent" ? "primary" : tone;
14
17
  return paintUiText(ui, text, mapped);
@@ -1,6 +1,6 @@
1
1
  import { type DashboardDisplaySettings } from "../../dashboard-settings.js";
2
2
  export declare const BEHAVIOR_PANEL_KEYS: readonly ["actionAutoReturnMs", "actionPauseOnKey", "menuAutoFetchLimits", "menuShowFetchStatus", "menuQuotaTtlMs"];
3
- export declare const THEME_PANEL_KEYS: readonly ["uiThemePreset", "uiAccentColor"];
3
+ export declare const THEME_PANEL_KEYS: readonly ["uiThemePreset", "uiAccentColor", "uiColorMode"];
4
4
  export declare function applyUiThemeFromDashboardSettings(settings: DashboardDisplaySettings): void;
5
5
  export declare function promptDashboardDisplaySettings(initial: DashboardDisplaySettings): Promise<DashboardDisplaySettings | null>;
6
6
  export declare function configureDashboardDisplaySettings(currentSettings?: DashboardDisplaySettings): Promise<DashboardDisplaySettings>;
@@ -93,6 +93,7 @@ const ACCENT_COLOR_OPTIONS = [
93
93
  "blue",
94
94
  "yellow",
95
95
  ];
96
+ const COLOR_MODE_OPTIONS = ["auto", "dark", "light"];
96
97
  const ACCOUNT_LIST_PANEL_KEYS = [
97
98
  "menuShowStatusBadge",
98
99
  "menuShowCurrentBadge",
@@ -121,6 +122,7 @@ export const BEHAVIOR_PANEL_KEYS = [
121
122
  export const THEME_PANEL_KEYS = [
122
123
  "uiThemePreset",
123
124
  "uiAccentColor",
125
+ "uiColorMode",
124
126
  ];
125
127
  export function applyUiThemeFromDashboardSettings(settings) {
126
128
  const current = getUiRuntimeOptions();
@@ -130,6 +132,7 @@ export function applyUiThemeFromDashboardSettings(settings) {
130
132
  glyphMode: current.glyphMode,
131
133
  palette: settings.uiThemePreset ?? "green",
132
134
  accent: settings.uiAccentColor ?? "green",
135
+ colorMode: settings.uiColorMode ?? "auto",
133
136
  });
134
137
  }
135
138
  /* c8 ignore start - interactive prompt flows are covered by integration tests */
@@ -215,6 +218,7 @@ export async function promptThemeSettings(initial) {
215
218
  applyUiThemeFromDashboardSettings,
216
219
  THEME_PRESET_OPTIONS,
217
220
  ACCENT_COLOR_OPTIONS,
221
+ COLOR_MODE_OPTIONS,
218
222
  THEME_PANEL_KEYS,
219
223
  UI_COPY,
220
224
  });
@@ -50,6 +50,7 @@ export declare function promptThemeSettingsPanelEntry(params: {
50
50
  applyUiThemeFromDashboardSettings: ThemeSettingsPanelDeps["applyUiThemeFromDashboardSettings"];
51
51
  THEME_PRESET_OPTIONS: ThemeSettingsPanelDeps["THEME_PRESET_OPTIONS"];
52
52
  ACCENT_COLOR_OPTIONS: ThemeSettingsPanelDeps["ACCENT_COLOR_OPTIONS"];
53
+ COLOR_MODE_OPTIONS: ThemeSettingsPanelDeps["COLOR_MODE_OPTIONS"];
53
54
  THEME_PANEL_KEYS: ThemeSettingsPanelDeps["THEME_PANEL_KEYS"];
54
55
  UI_COPY: ThemeSettingsPanelDeps["UI_COPY"];
55
56
  }): Promise<DashboardDisplaySettings | null>;
@@ -66,6 +66,7 @@ export async function promptThemeSettingsPanelEntry(params) {
66
66
  applyUiThemeFromDashboardSettings: params.applyUiThemeFromDashboardSettings,
67
67
  THEME_PRESET_OPTIONS: params.THEME_PRESET_OPTIONS,
68
68
  ACCENT_COLOR_OPTIONS: params.ACCENT_COLOR_OPTIONS,
69
+ COLOR_MODE_OPTIONS: params.COLOR_MODE_OPTIONS,
69
70
  THEME_PANEL_KEYS: params.THEME_PANEL_KEYS,
70
71
  UI_COPY: params.UI_COPY,
71
72
  });
@@ -1,4 +1,4 @@
1
- import type { DashboardAccentColor, DashboardDisplaySettings, DashboardThemePreset } from "../dashboard-settings.js";
1
+ import type { DashboardAccentColor, DashboardColorMode, DashboardDisplaySettings, DashboardThemePreset } from "../dashboard-settings.js";
2
2
  import type { UI_COPY } from "../ui/ui-copy.js";
3
3
  export interface ThemeSettingsPanelDeps {
4
4
  cloneDashboardSettings: (settings: DashboardDisplaySettings) => DashboardDisplaySettings;
@@ -6,6 +6,7 @@ export interface ThemeSettingsPanelDeps {
6
6
  applyUiThemeFromDashboardSettings: (settings: DashboardDisplaySettings) => void;
7
7
  THEME_PRESET_OPTIONS: readonly DashboardThemePreset[];
8
8
  ACCENT_COLOR_OPTIONS: readonly DashboardAccentColor[];
9
+ COLOR_MODE_OPTIONS: readonly DashboardColorMode[];
9
10
  THEME_PANEL_KEYS: readonly (keyof DashboardDisplaySettings)[];
10
11
  UI_COPY: typeof UI_COPY;
11
12
  }
@@ -14,6 +14,7 @@ export async function promptThemeSettingsPanel(initial, deps) {
14
14
  const ui = getUiRuntimeOptions();
15
15
  const palette = draft.uiThemePreset ?? "green";
16
16
  const accent = draft.uiAccentColor ?? "green";
17
+ const colorMode = draft.uiColorMode ?? "auto";
17
18
  const paletteItems = deps.THEME_PRESET_OPTIONS.map((candidate, index) => {
18
19
  const color = palette === candidate ? "green" : "yellow";
19
20
  return {
@@ -33,6 +34,21 @@ export async function promptThemeSettingsPanel(initial, deps) {
33
34
  color,
34
35
  };
35
36
  });
37
+ const colorModeItems = deps.COLOR_MODE_OPTIONS.map((candidate) => {
38
+ const color = colorMode === candidate ? "green" : "yellow";
39
+ const key = candidate === "auto" ? "A" : candidate === "dark" ? "D" : "L";
40
+ const hint = candidate === "auto"
41
+ ? "Detect from COLORFGBG; dark when unknown."
42
+ : candidate === "dark"
43
+ ? "Pale text on a dark terminal background."
44
+ : "Dark text on a light terminal background.";
45
+ return {
46
+ label: `${colorMode === candidate ? "[x]" : "[ ]"} ${key}. ${candidate}`,
47
+ hint,
48
+ value: { type: "set-color-mode", colorMode: candidate },
49
+ color,
50
+ };
51
+ });
36
52
  const items = [
37
53
  {
38
54
  label: deps.UI_COPY.settings.baseTheme,
@@ -48,6 +64,13 @@ export async function promptThemeSettingsPanel(initial, deps) {
48
64
  },
49
65
  ...accentItems,
50
66
  { label: "", value: { type: "cancel" }, separator: true },
67
+ {
68
+ label: deps.UI_COPY.settings.terminalBackground,
69
+ value: { type: "cancel" },
70
+ kind: "heading",
71
+ },
72
+ ...colorModeItems,
73
+ { label: "", value: { type: "cancel" }, separator: true },
51
74
  {
52
75
  label: deps.UI_COPY.settings.resetDefault,
53
76
  value: { type: "reset" },
@@ -74,6 +97,10 @@ export async function promptThemeSettingsPanel(initial, deps) {
74
97
  if (value.type === "set-accent" && focus.type === "set-accent") {
75
98
  return value.accent === focus.accent;
76
99
  }
100
+ if (value.type === "set-color-mode" &&
101
+ focus.type === "set-color-mode") {
102
+ return value.colorMode === focus.colorMode;
103
+ }
77
104
  return true;
78
105
  });
79
106
  const result = await select(items, {
@@ -98,6 +125,12 @@ export async function promptThemeSettingsPanel(initial, deps) {
98
125
  return { type: "save" };
99
126
  if (lower === "r")
100
127
  return { type: "reset" };
128
+ if (lower === "a")
129
+ return { type: "set-color-mode", colorMode: "auto" };
130
+ if (lower === "d")
131
+ return { type: "set-color-mode", colorMode: "dark" };
132
+ if (lower === "l")
133
+ return { type: "set-color-mode", colorMode: "light" };
101
134
  if (raw === "1")
102
135
  return { type: "set-palette", palette: "green" };
103
136
  if (raw === "2")
@@ -123,6 +156,12 @@ export async function promptThemeSettingsPanel(initial, deps) {
123
156
  deps.applyUiThemeFromDashboardSettings(draft);
124
157
  continue;
125
158
  }
159
+ if (result.type === "set-color-mode") {
160
+ draft = { ...draft, uiColorMode: result.colorMode };
161
+ focus = result;
162
+ deps.applyUiThemeFromDashboardSettings(draft);
163
+ continue;
164
+ }
126
165
  draft = { ...draft, uiAccentColor: result.accent };
127
166
  focus = result;
128
167
  deps.applyUiThemeFromDashboardSettings(draft);
@@ -46,6 +46,7 @@ import { runUnpinCommand } from "./codex-manager/commands/unpin.js";
46
46
  import { runWorkspaceCommand } from "./codex-manager/commands/workspace.js";
47
47
  import { runUsageCommand } from "./codex-manager/commands/usage.js";
48
48
  import { refreshQuotaCacheForMenu } from "./codex-manager/login-menu-data.js";
49
+ import { refreshLunaReserveUsage } from "./runtime/luna-reserve-usage.js";
49
50
  import { printUsage } from "./codex-manager/help.js";
50
51
  import { availabilityTone, formatBackupSavedAt, formatCompactQuotaSnapshot, formatRateLimitEntry, formatResultSummary, normalizeFailureDetail, riskTone, styleAccountDetailText, stylePromptText, styleQuotaSummary, } from "./codex-manager/formatters/index.js";
51
52
  import { applyUiThemeFromDashboardSettings } from "./codex-manager/settings-hub.js";
@@ -307,6 +308,7 @@ const CLI_COMMAND_HANDLERS = new Map([
307
308
  loadAccounts,
308
309
  loadQuotaCache,
309
310
  refreshQuotaCache: refreshQuotaCacheForMenu,
311
+ refreshLunaReserveUsage,
310
312
  resolveActiveIndex,
311
313
  }),
312
314
  ],
@@ -6,6 +6,8 @@
6
6
  export declare const PLUGIN_NAME = "codex-multi-auth";
7
7
  /** Base URL for ChatGPT backend API */
8
8
  export declare const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
9
+ /** Backend model used for the separately metered Luna Reserve allowance. */
10
+ export declare const LUNA_RESERVE_MODEL: "gpt-reserve";
9
11
  /** Dummy API key used for OpenAI SDK (actual auth via OAuth) */
10
12
  export declare const DUMMY_API_KEY = "chatgpt-oauth";
11
13
  /** Provider ID for UI display - shows under "OpenAI" in auth dropdown */
@@ -6,6 +6,8 @@
6
6
  export const PLUGIN_NAME = "codex-multi-auth";
7
7
  /** Base URL for ChatGPT backend API */
8
8
  export const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
9
+ /** Backend model used for the separately metered Luna Reserve allowance. */
10
+ export const LUNA_RESERVE_MODEL = "gpt-reserve";
9
11
  /** Dummy API key used for OpenAI SDK (actual auth via OAuth) */
10
12
  export const DUMMY_API_KEY = "chatgpt-oauth";
11
13
  /** Provider ID for UI display - shows under "OpenAI" in auth dropdown */
@@ -6,7 +6,7 @@
6
6
  * behavior (see the v2.5.0 release notes) closely enough to launch that no
7
7
  * estimate here has been reviewed for it yet.
8
8
  */
9
- export declare const UNESTIMATED_ROUTABLE_MODELS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
9
+ export declare const UNESTIMATED_ROUTABLE_MODELS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-reserve", "gpt-6.1-sol", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
10
10
  export interface EffectiveContextWindow {
11
11
  tokens: number;
12
12
  source: "override" | "estimate";
@@ -53,6 +53,10 @@ export const UNESTIMATED_ROUTABLE_MODELS = [
53
53
  // upstream catalog as Astra, so the same reasoning keeps them unestimated.
54
54
  "gpt-6-sol",
55
55
  "gpt-6-luna",
56
+ // Luna Reserve is a quota alias of Luna, but the backend does not publish
57
+ // a distinct transport context ceiling for the alias. Keep it fail-closed
58
+ // alongside Luna rather than copying an unverified API/catalog number.
59
+ "gpt-reserve",
56
60
  "gpt-6.1-sol",
57
61
  "gpt-daybreak-blue-latest",
58
62
  "gpt-daybreak-red-latest",
@@ -17,14 +17,14 @@ function buildPauseMessage(advisory) {
17
17
  const windowNote = advisory.windowSource === "override"
18
18
  ? "your configured window size"
19
19
  : "an estimated window size (unverified — set `contextBudgetGuardModelWindowOverrides` if you know the real limit)";
20
- return `[Plugin Notice] Context budget guard paused this session at ${formatPercent(advisory.percent)}% of ${windowNote} for \`${advisory.model}\`, before hitting the model's actual limit.
21
-
22
- Please use one of these commands to reduce context size:
23
-
24
- • **/compact** - Compress conversation history (recommended)
25
- • **/clear** - Start fresh with empty context
26
- • **/undo** - Remove recent messages
27
-
20
+ return `[Plugin Notice] Context budget guard paused this session at ${formatPercent(advisory.percent)}% of ${windowNote} for \`${advisory.model}\`, before hitting the model's actual limit.
21
+
22
+ Please use one of these commands to reduce context size:
23
+
24
+ • **/compact** - Compress conversation history (recommended)
25
+ • **/clear** - Start fresh with empty context
26
+ • **/undo** - Remove recent messages
27
+
28
28
  Then retry your request. This pause clears itself automatically once your context usage drops back below the threshold.`;
29
29
  }
30
30
  /**
@@ -33,16 +33,16 @@ export function isContextOverflowError(status, bodyText) {
33
33
  /**
34
34
  * The message shown to users when context overflow occurs
35
35
  */
36
- const CONTEXT_OVERFLOW_MESSAGE = `[Plugin Notice] Context is too long for this model.
37
-
38
- Please use one of these commands to reduce context size:
39
-
40
- • **/compact** - Compress conversation history (recommended)
41
- • **/clear** - Start fresh with empty context
42
- • **/undo** - Remove recent messages
43
-
44
- Then retry your request.
45
-
36
+ const CONTEXT_OVERFLOW_MESSAGE = `[Plugin Notice] Context is too long for this model.
37
+
38
+ Please use one of these commands to reduce context size:
39
+
40
+ • **/compact** - Compress conversation history (recommended)
41
+ • **/clear** - Start fresh with empty context
42
+ • **/undo** - Remove recent messages
43
+
44
+ Then retry your request.
45
+
46
46
  Alternatively, you can switch to a model with a larger context window.`;
47
47
  /**
48
48
  * Creates a synthetic SSE response for context overflow errors.
@@ -1,5 +1,6 @@
1
1
  export type DashboardThemePreset = "green" | "blue";
2
2
  export type DashboardAccentColor = "green" | "cyan" | "blue" | "yellow";
3
+ export type DashboardColorMode = "auto" | "dark" | "light";
3
4
  export type DashboardAccountSortMode = "manual" | "ready-first";
4
5
  type DashboardLayoutMode = "compact-details" | "expanded-rows";
5
6
  type DashboardFocusStyle = "row-invert";
@@ -18,6 +19,7 @@ export interface DashboardDisplaySettings {
18
19
  menuSortQuickSwitchVisibleRow?: boolean;
19
20
  uiThemePreset?: DashboardThemePreset;
20
21
  uiAccentColor?: DashboardAccentColor;
22
+ uiColorMode?: DashboardColorMode;
21
23
  menuShowStatusBadge?: boolean;
22
24
  menuShowCurrentBadge?: boolean;
23
25
  menuShowLastUsed?: boolean;