oc-chatgpt-multi-auth 5.3.3 → 5.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -85
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1385 -37
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +16 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +60 -0
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +36 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/refresh-queue.d.ts +16 -0
- package/dist/lib/refresh-queue.d.ts.map +1 -1
- package/dist/lib/refresh-queue.js +46 -0
- package/dist/lib/refresh-queue.js.map +1 -1
- package/dist/lib/request/retry-budget.d.ts +19 -0
- package/dist/lib/request/retry-budget.d.ts.map +1 -0
- package/dist/lib/request/retry-budget.js +99 -0
- package/dist/lib/request/retry-budget.js.map +1 -0
- package/dist/lib/schemas.d.ts +26 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +28 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +4 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage/migrations.js +2 -0
- package/dist/lib/storage/migrations.js.map +1 -1
- package/dist/lib/storage.d.ts +31 -5
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +211 -45
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/ui/beginner.d.ts +57 -0
- package/dist/lib/ui/beginner.d.ts.map +1 -0
- package/dist/lib/ui/beginner.js +230 -0
- package/dist/lib/ui/beginner.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,25 +24,27 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
26
26
|
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
|
|
27
|
-
import { queuedRefresh } from "./lib/refresh-queue.js";
|
|
27
|
+
import { queuedRefresh, getRefreshQueueMetrics } from "./lib/refresh-queue.js";
|
|
28
28
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
29
29
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
30
30
|
import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
|
|
31
|
-
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, loadPluginConfig, } from "./lib/config.js";
|
|
31
|
+
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, loadPluginConfig, } from "./lib/config.js";
|
|
32
32
|
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
33
33
|
import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
|
|
34
34
|
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
35
35
|
import { handleContextOverflow } from "./lib/context-overflow.js";
|
|
36
36
|
import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
|
|
37
|
-
import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
37
|
+
import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, previewImportAccounts, createTimestampedBackupPath, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
38
38
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
39
39
|
import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
|
|
40
40
|
import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
|
|
41
41
|
import { isEmptyResponse } from "./lib/request/response-handler.js";
|
|
42
|
+
import { RetryBudgetTracker, resolveRetryBudgetLimits, } from "./lib/request/retry-budget.js";
|
|
42
43
|
import { addJitter } from "./lib/rotation.js";
|
|
43
44
|
import { buildTableHeader, buildTableRow } from "./lib/table-formatter.js";
|
|
44
45
|
import { setUiRuntimeOptions } from "./lib/ui/runtime.js";
|
|
45
46
|
import { paintUiText, formatUiBadge, formatUiHeader, formatUiItem, formatUiKeyValue, formatUiSection } from "./lib/ui/format.js";
|
|
47
|
+
import { buildBeginnerChecklist, buildBeginnerDoctorFindings, recommendBeginnerNextAction, summarizeBeginnerAccounts, } from "./lib/ui/beginner.js";
|
|
46
48
|
import { getModelFamily, getCodexInstructions, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
|
|
47
49
|
import { prewarmOpenCodeCodexPrompt } from "./lib/prompts/opencode-codex.js";
|
|
48
50
|
import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
|
|
@@ -69,7 +71,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
69
71
|
let accountManagerPromise = null;
|
|
70
72
|
let loaderMutex = null;
|
|
71
73
|
let startupPrewarmTriggered = false;
|
|
74
|
+
let startupPreflightShown = false;
|
|
75
|
+
let beginnerSafeModeEnabled = false;
|
|
72
76
|
const MIN_BACKOFF_MS = 100;
|
|
77
|
+
const createRetryBudgetUsage = () => ({
|
|
78
|
+
authRefresh: 0,
|
|
79
|
+
network: 0,
|
|
80
|
+
server: 0,
|
|
81
|
+
rateLimitShort: 0,
|
|
82
|
+
rateLimitGlobal: 0,
|
|
83
|
+
emptyResponse: 0,
|
|
84
|
+
});
|
|
73
85
|
const runtimeMetrics = {
|
|
74
86
|
startedAt: Date.now(),
|
|
75
87
|
totalRequests: 0,
|
|
@@ -82,8 +94,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
82
94
|
emptyResponseRetries: 0,
|
|
83
95
|
accountRotations: 0,
|
|
84
96
|
cumulativeLatencyMs: 0,
|
|
97
|
+
retryBudgetExhaustions: 0,
|
|
98
|
+
retryBudgetUsage: createRetryBudgetUsage(),
|
|
99
|
+
retryBudgetLimits: resolveRetryBudgetLimits("balanced"),
|
|
100
|
+
retryProfile: "balanced",
|
|
101
|
+
lastRetryBudgetExhaustedClass: null,
|
|
102
|
+
lastRetryBudgetReason: null,
|
|
85
103
|
lastRequestAt: null,
|
|
86
104
|
lastError: null,
|
|
105
|
+
lastErrorCategory: null,
|
|
106
|
+
lastSelectedAccountIndex: null,
|
|
107
|
+
lastQuotaKey: null,
|
|
108
|
+
lastSelectionSnapshot: null,
|
|
87
109
|
};
|
|
88
110
|
const createSelectionVariant = (tokens, candidate) => ({
|
|
89
111
|
...tokens,
|
|
@@ -767,6 +789,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
767
789
|
const email = account?.email?.trim();
|
|
768
790
|
const workspace = account?.accountLabel?.trim();
|
|
769
791
|
const accountId = formatAccountIdForDisplay(account?.accountId);
|
|
792
|
+
const tags = Array.isArray(account?.accountTags)
|
|
793
|
+
? account.accountTags
|
|
794
|
+
.filter((tag) => typeof tag === "string")
|
|
795
|
+
.map((tag) => tag.trim().toLowerCase())
|
|
796
|
+
.filter((tag) => tag.length > 0)
|
|
797
|
+
: [];
|
|
770
798
|
const details = [];
|
|
771
799
|
if (email)
|
|
772
800
|
details.push(email);
|
|
@@ -774,11 +802,295 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
774
802
|
details.push(`workspace:${workspace}`);
|
|
775
803
|
if (accountId)
|
|
776
804
|
details.push(`id:${accountId}`);
|
|
805
|
+
if (tags.length > 0)
|
|
806
|
+
details.push(`tags:${tags.join(",")}`);
|
|
777
807
|
if (details.length === 0) {
|
|
778
808
|
return `Account ${index + 1}`;
|
|
779
809
|
}
|
|
780
810
|
return `Account ${index + 1} (${details.join(", ")})`;
|
|
781
811
|
};
|
|
812
|
+
const normalizeAccountTags = (raw) => {
|
|
813
|
+
return Array.from(new Set(raw
|
|
814
|
+
.split(",")
|
|
815
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
816
|
+
.filter((entry) => entry.length > 0)));
|
|
817
|
+
};
|
|
818
|
+
const supportsInteractiveMenus = () => {
|
|
819
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
820
|
+
return false;
|
|
821
|
+
if (process.env.OPENCODE_TUI === "1")
|
|
822
|
+
return false;
|
|
823
|
+
if (process.env.OPENCODE_DESKTOP === "1")
|
|
824
|
+
return false;
|
|
825
|
+
if (process.env.TERM_PROGRAM === "opencode")
|
|
826
|
+
return false;
|
|
827
|
+
return true;
|
|
828
|
+
};
|
|
829
|
+
const promptAccountIndexSelection = async (ui, storage, title) => {
|
|
830
|
+
if (!supportsInteractiveMenus())
|
|
831
|
+
return null;
|
|
832
|
+
try {
|
|
833
|
+
const { select } = await import("./lib/ui/select.js");
|
|
834
|
+
const selected = await select(storage.accounts.map((account, index) => ({
|
|
835
|
+
label: formatCommandAccountLabel(account, index),
|
|
836
|
+
value: index,
|
|
837
|
+
})), {
|
|
838
|
+
message: title,
|
|
839
|
+
subtitle: "Select account index",
|
|
840
|
+
help: "Up/Down select | Enter confirm | Esc cancel",
|
|
841
|
+
clearScreen: true,
|
|
842
|
+
variant: ui.v2Enabled ? "codex" : "legacy",
|
|
843
|
+
theme: ui.theme,
|
|
844
|
+
});
|
|
845
|
+
return typeof selected === "number" ? selected : null;
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const toBeginnerAccountSnapshots = (storage, activeIndex, now) => {
|
|
852
|
+
return storage.accounts.map((account, index) => ({
|
|
853
|
+
index,
|
|
854
|
+
label: formatCommandAccountLabel(account, index),
|
|
855
|
+
accountLabel: account.accountLabel,
|
|
856
|
+
enabled: account.enabled !== false,
|
|
857
|
+
isActive: index === activeIndex,
|
|
858
|
+
rateLimitedUntil: getRateLimitResetTimeForFamily(account, now, "codex"),
|
|
859
|
+
coolingDownUntil: typeof account.coolingDownUntil === "number"
|
|
860
|
+
? account.coolingDownUntil
|
|
861
|
+
: null,
|
|
862
|
+
}));
|
|
863
|
+
};
|
|
864
|
+
const getBeginnerRuntimeSnapshot = () => ({
|
|
865
|
+
totalRequests: runtimeMetrics.totalRequests,
|
|
866
|
+
failedRequests: runtimeMetrics.failedRequests,
|
|
867
|
+
rateLimitedResponses: runtimeMetrics.rateLimitedResponses,
|
|
868
|
+
authRefreshFailures: runtimeMetrics.authRefreshFailures,
|
|
869
|
+
serverErrors: runtimeMetrics.serverErrors,
|
|
870
|
+
networkErrors: runtimeMetrics.networkErrors,
|
|
871
|
+
lastErrorCategory: runtimeMetrics.lastErrorCategory,
|
|
872
|
+
});
|
|
873
|
+
const formatDoctorSeverity = (ui, severity) => {
|
|
874
|
+
if (severity === "ok")
|
|
875
|
+
return formatUiBadge(ui, "ok", "success");
|
|
876
|
+
if (severity === "warning")
|
|
877
|
+
return formatUiBadge(ui, "warning", "warning");
|
|
878
|
+
return formatUiBadge(ui, "error", "danger");
|
|
879
|
+
};
|
|
880
|
+
const formatDoctorSeverityText = (severity) => {
|
|
881
|
+
if (severity === "ok")
|
|
882
|
+
return "[ok]";
|
|
883
|
+
if (severity === "warning")
|
|
884
|
+
return "[warning]";
|
|
885
|
+
return "[error]";
|
|
886
|
+
};
|
|
887
|
+
const buildSetupChecklistState = async () => {
|
|
888
|
+
const storage = await loadAccounts();
|
|
889
|
+
const now = Date.now();
|
|
890
|
+
const activeIndex = storage && storage.accounts.length > 0
|
|
891
|
+
? resolveActiveIndex(storage, "codex")
|
|
892
|
+
: 0;
|
|
893
|
+
const snapshots = storage
|
|
894
|
+
? toBeginnerAccountSnapshots(storage, activeIndex, now)
|
|
895
|
+
: [];
|
|
896
|
+
const runtime = getBeginnerRuntimeSnapshot();
|
|
897
|
+
const checklist = buildBeginnerChecklist(snapshots, now);
|
|
898
|
+
const summary = summarizeBeginnerAccounts(snapshots, now);
|
|
899
|
+
const nextAction = recommendBeginnerNextAction({
|
|
900
|
+
accounts: snapshots,
|
|
901
|
+
now,
|
|
902
|
+
runtime,
|
|
903
|
+
});
|
|
904
|
+
return {
|
|
905
|
+
now,
|
|
906
|
+
storage,
|
|
907
|
+
activeIndex,
|
|
908
|
+
snapshots,
|
|
909
|
+
runtime,
|
|
910
|
+
checklist,
|
|
911
|
+
summary,
|
|
912
|
+
nextAction,
|
|
913
|
+
};
|
|
914
|
+
};
|
|
915
|
+
const renderSetupChecklistOutput = (ui, state) => {
|
|
916
|
+
if (ui.v2Enabled) {
|
|
917
|
+
const lines = [
|
|
918
|
+
...formatUiHeader(ui, "Setup checklist"),
|
|
919
|
+
formatUiKeyValue(ui, "Accounts", String(state.summary.total)),
|
|
920
|
+
formatUiKeyValue(ui, "Healthy", String(state.summary.healthy), state.summary.healthy > 0 ? "success" : "warning"),
|
|
921
|
+
formatUiKeyValue(ui, "Blocked", String(state.summary.blocked), state.summary.blocked > 0 ? "warning" : "muted"),
|
|
922
|
+
"",
|
|
923
|
+
];
|
|
924
|
+
for (const item of state.checklist) {
|
|
925
|
+
const marker = item.done
|
|
926
|
+
? getStatusMarker(ui, "ok")
|
|
927
|
+
: getStatusMarker(ui, "warning");
|
|
928
|
+
lines.push(formatUiItem(ui, `${marker} ${item.label} - ${item.detail}`, item.done ? "success" : "warning"));
|
|
929
|
+
if (item.command) {
|
|
930
|
+
lines.push(` ${formatUiKeyValue(ui, "command", item.command, "muted")}`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
lines.push("");
|
|
934
|
+
lines.push(...formatUiSection(ui, "Recommended next step"));
|
|
935
|
+
lines.push(formatUiItem(ui, state.nextAction, "accent"));
|
|
936
|
+
lines.push(formatUiItem(ui, "Guided wizard: codex-setup --wizard", "muted"));
|
|
937
|
+
return lines.join("\n");
|
|
938
|
+
}
|
|
939
|
+
const lines = [
|
|
940
|
+
"Setup Checklist:",
|
|
941
|
+
`Accounts: ${state.summary.total}`,
|
|
942
|
+
`Healthy accounts: ${state.summary.healthy}`,
|
|
943
|
+
`Blocked accounts: ${state.summary.blocked}`,
|
|
944
|
+
"",
|
|
945
|
+
];
|
|
946
|
+
for (const item of state.checklist) {
|
|
947
|
+
const marker = item.done ? "[x]" : "[ ]";
|
|
948
|
+
lines.push(`${marker} ${item.label} - ${item.detail}`);
|
|
949
|
+
if (item.command)
|
|
950
|
+
lines.push(` command: ${item.command}`);
|
|
951
|
+
}
|
|
952
|
+
lines.push("");
|
|
953
|
+
lines.push(`Recommended next step: ${state.nextAction}`);
|
|
954
|
+
lines.push("Guided wizard: codex-setup --wizard");
|
|
955
|
+
return lines.join("\n");
|
|
956
|
+
};
|
|
957
|
+
const runSetupWizard = async (ui, state) => {
|
|
958
|
+
if (!supportsInteractiveMenus()) {
|
|
959
|
+
return [
|
|
960
|
+
ui.v2Enabled
|
|
961
|
+
? formatUiItem(ui, "Interactive wizard mode is unavailable in this session.", "warning")
|
|
962
|
+
: "Interactive wizard mode is unavailable in this session.",
|
|
963
|
+
ui.v2Enabled
|
|
964
|
+
? formatUiItem(ui, "Showing checklist view instead.", "muted")
|
|
965
|
+
: "Showing checklist view instead.",
|
|
966
|
+
"",
|
|
967
|
+
renderSetupChecklistOutput(ui, state),
|
|
968
|
+
].join("\n");
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const { select } = await import("./lib/ui/select.js");
|
|
972
|
+
const labels = {
|
|
973
|
+
checklist: "Show setup checklist",
|
|
974
|
+
next: "Show best next action",
|
|
975
|
+
"add-account": "Add account now",
|
|
976
|
+
health: "Run health check",
|
|
977
|
+
switch: "Switch active account",
|
|
978
|
+
label: "Set account label",
|
|
979
|
+
doctor: "Run doctor diagnostics",
|
|
980
|
+
dashboard: "Open live dashboard",
|
|
981
|
+
metrics: "Open runtime metrics",
|
|
982
|
+
backup: "Backup accounts",
|
|
983
|
+
"safe-mode": "Enable beginner safe mode",
|
|
984
|
+
help: "Open command help",
|
|
985
|
+
};
|
|
986
|
+
const commandMap = {
|
|
987
|
+
"add-account": "opencode auth login",
|
|
988
|
+
health: "codex-health",
|
|
989
|
+
switch: "codex-switch index=2",
|
|
990
|
+
label: "codex-label index=2 label=\"Work\"",
|
|
991
|
+
doctor: "codex-doctor",
|
|
992
|
+
dashboard: "codex-dashboard",
|
|
993
|
+
metrics: "codex-metrics",
|
|
994
|
+
backup: "codex-export <path>",
|
|
995
|
+
"safe-mode": "set CODEX_AUTH_BEGINNER_SAFE_MODE=1",
|
|
996
|
+
help: "codex-help",
|
|
997
|
+
};
|
|
998
|
+
const choice = await select([
|
|
999
|
+
{ label: "Setup wizard", value: "exit", kind: "heading" },
|
|
1000
|
+
{ label: labels.checklist, value: "checklist", color: "cyan" },
|
|
1001
|
+
{ label: labels.next, value: "next", color: "green" },
|
|
1002
|
+
{ label: labels["add-account"], value: "add-account", color: "cyan" },
|
|
1003
|
+
{ label: labels.health, value: "health", color: "cyan" },
|
|
1004
|
+
{ label: labels.switch, value: "switch", color: "cyan" },
|
|
1005
|
+
{ label: labels.label, value: "label", color: "cyan" },
|
|
1006
|
+
{ label: labels.doctor, value: "doctor", color: "yellow" },
|
|
1007
|
+
{ label: labels.dashboard, value: "dashboard", color: "cyan" },
|
|
1008
|
+
{ label: labels.metrics, value: "metrics", color: "cyan" },
|
|
1009
|
+
{ label: labels.backup, value: "backup", color: "yellow" },
|
|
1010
|
+
{ label: labels["safe-mode"], value: "safe-mode", color: "yellow" },
|
|
1011
|
+
{ label: labels.help, value: "help", color: "cyan" },
|
|
1012
|
+
{ label: "", value: "exit", separator: true },
|
|
1013
|
+
{ label: "Exit wizard", value: "exit", color: "red" },
|
|
1014
|
+
], {
|
|
1015
|
+
message: "Beginner setup wizard",
|
|
1016
|
+
subtitle: `Accounts: ${state.summary.total} | Healthy: ${state.summary.healthy} | Blocked: ${state.summary.blocked}`,
|
|
1017
|
+
help: "Up/Down select | Enter confirm | Esc exit",
|
|
1018
|
+
clearScreen: true,
|
|
1019
|
+
variant: ui.v2Enabled ? "codex" : "legacy",
|
|
1020
|
+
theme: ui.theme,
|
|
1021
|
+
});
|
|
1022
|
+
if (!choice || choice === "exit") {
|
|
1023
|
+
return ui.v2Enabled
|
|
1024
|
+
? [
|
|
1025
|
+
...formatUiHeader(ui, "Setup wizard"),
|
|
1026
|
+
"",
|
|
1027
|
+
formatUiItem(ui, "Wizard closed.", "muted"),
|
|
1028
|
+
formatUiItem(ui, `Next: ${state.nextAction}`, "accent"),
|
|
1029
|
+
].join("\n")
|
|
1030
|
+
: `Setup wizard closed.\n\nNext: ${state.nextAction}`;
|
|
1031
|
+
}
|
|
1032
|
+
if (choice === "checklist") {
|
|
1033
|
+
return renderSetupChecklistOutput(ui, state);
|
|
1034
|
+
}
|
|
1035
|
+
if (choice === "next") {
|
|
1036
|
+
return ui.v2Enabled
|
|
1037
|
+
? [
|
|
1038
|
+
...formatUiHeader(ui, "Setup wizard"),
|
|
1039
|
+
"",
|
|
1040
|
+
formatUiItem(ui, "Best next action", "accent"),
|
|
1041
|
+
formatUiItem(ui, state.nextAction, "success"),
|
|
1042
|
+
].join("\n")
|
|
1043
|
+
: `Best next action:\n${state.nextAction}`;
|
|
1044
|
+
}
|
|
1045
|
+
const command = commandMap[choice];
|
|
1046
|
+
const selectedLabel = labels[choice];
|
|
1047
|
+
if (ui.v2Enabled) {
|
|
1048
|
+
return [
|
|
1049
|
+
...formatUiHeader(ui, "Setup wizard"),
|
|
1050
|
+
"",
|
|
1051
|
+
formatUiItem(ui, `Selected: ${selectedLabel}`, "accent"),
|
|
1052
|
+
formatUiItem(ui, `Run: ${command}`, "success"),
|
|
1053
|
+
formatUiItem(ui, "Run codex-setup --wizard again to choose another step.", "muted"),
|
|
1054
|
+
].join("\n");
|
|
1055
|
+
}
|
|
1056
|
+
return [
|
|
1057
|
+
"Setup wizard:",
|
|
1058
|
+
`Selected: ${selectedLabel}`,
|
|
1059
|
+
`Run: ${command}`,
|
|
1060
|
+
"",
|
|
1061
|
+
"Run codex-setup --wizard again to choose another step.",
|
|
1062
|
+
].join("\n");
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1066
|
+
return [
|
|
1067
|
+
ui.v2Enabled
|
|
1068
|
+
? formatUiItem(ui, `Wizard failed to open: ${reason}`, "warning")
|
|
1069
|
+
: `Wizard failed to open: ${reason}`,
|
|
1070
|
+
ui.v2Enabled
|
|
1071
|
+
? formatUiItem(ui, "Showing checklist view instead.", "muted")
|
|
1072
|
+
: "Showing checklist view instead.",
|
|
1073
|
+
"",
|
|
1074
|
+
renderSetupChecklistOutput(ui, state),
|
|
1075
|
+
].join("\n");
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
const runStartupPreflight = async () => {
|
|
1079
|
+
if (startupPreflightShown)
|
|
1080
|
+
return;
|
|
1081
|
+
startupPreflightShown = true;
|
|
1082
|
+
try {
|
|
1083
|
+
const state = await buildSetupChecklistState();
|
|
1084
|
+
const message = `Codex preflight: healthy ${state.summary.healthy}/${state.summary.total}, ` +
|
|
1085
|
+
`blocked ${state.summary.blocked}, rate-limited ${state.summary.rateLimited}. ` +
|
|
1086
|
+
`Next: ${state.nextAction}`;
|
|
1087
|
+
await showToast(message, state.summary.healthy > 0 ? "info" : "warning");
|
|
1088
|
+
logInfo(message);
|
|
1089
|
+
}
|
|
1090
|
+
catch (error) {
|
|
1091
|
+
logDebug(`[${PLUGIN_NAME}] Startup preflight skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
782
1094
|
const invalidateAccountManagerCache = () => {
|
|
783
1095
|
cachedAccountManager = null;
|
|
784
1096
|
accountManagerPromise = null;
|
|
@@ -905,11 +1217,26 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
905
1217
|
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
906
1218
|
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
907
1219
|
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1220
|
+
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1221
|
+
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1222
|
+
const retryProfile = beginnerSafeMode
|
|
1223
|
+
? "conservative"
|
|
1224
|
+
: getRetryProfile(pluginConfig);
|
|
1225
|
+
const retryBudgetOverrides = beginnerSafeMode
|
|
1226
|
+
? {}
|
|
1227
|
+
: getRetryBudgetOverrides(pluginConfig);
|
|
1228
|
+
const retryBudgetLimits = resolveRetryBudgetLimits(retryProfile, retryBudgetOverrides);
|
|
1229
|
+
runtimeMetrics.retryProfile = retryProfile;
|
|
1230
|
+
runtimeMetrics.retryBudgetLimits = { ...retryBudgetLimits };
|
|
908
1231
|
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
909
1232
|
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
910
|
-
const retryAllAccountsRateLimited =
|
|
1233
|
+
const retryAllAccountsRateLimited = beginnerSafeMode
|
|
1234
|
+
? false
|
|
1235
|
+
: getRetryAllAccountsRateLimited(pluginConfig);
|
|
911
1236
|
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
912
|
-
const retryAllAccountsMaxRetries =
|
|
1237
|
+
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1238
|
+
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1239
|
+
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
913
1240
|
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
914
1241
|
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
915
1242
|
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
@@ -934,6 +1261,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
934
1261
|
fastSessionMaxInputItems,
|
|
935
1262
|
});
|
|
936
1263
|
}
|
|
1264
|
+
if (beginnerSafeMode) {
|
|
1265
|
+
logInfo("Beginner safe mode enabled", {
|
|
1266
|
+
retryProfile,
|
|
1267
|
+
retryAllAccountsRateLimited,
|
|
1268
|
+
retryAllAccountsMaxRetries,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
937
1271
|
const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
|
|
938
1272
|
process.env.VITEST !== "true" &&
|
|
939
1273
|
process.env.NODE_ENV !== "test";
|
|
@@ -953,6 +1287,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
953
1287
|
}).catch((err) => {
|
|
954
1288
|
logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
955
1289
|
});
|
|
1290
|
+
await runStartupPreflight();
|
|
956
1291
|
// Return SDK configuration
|
|
957
1292
|
return {
|
|
958
1293
|
apiKey: DUMMY_API_KEY,
|
|
@@ -1054,6 +1389,25 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1054
1389
|
.trim() || undefined;
|
|
1055
1390
|
const requestCorrelationId = setCorrelationId(threadIdCandidate ? `${threadIdCandidate}:${Date.now()}` : undefined);
|
|
1056
1391
|
runtimeMetrics.lastRequestAt = Date.now();
|
|
1392
|
+
const retryBudget = new RetryBudgetTracker(retryBudgetLimits);
|
|
1393
|
+
const consumeRetryBudget = (bucket, reason) => {
|
|
1394
|
+
if (retryBudget.consume(bucket)) {
|
|
1395
|
+
runtimeMetrics.retryBudgetUsage[bucket] += 1;
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
runtimeMetrics.retryBudgetExhaustions += 1;
|
|
1399
|
+
runtimeMetrics.lastRetryBudgetExhaustedClass = bucket;
|
|
1400
|
+
runtimeMetrics.lastRetryBudgetReason = reason;
|
|
1401
|
+
runtimeMetrics.lastErrorCategory = "retry-budget";
|
|
1402
|
+
runtimeMetrics.lastError = `Retry budget exhausted (${bucket}): ${reason}`;
|
|
1403
|
+
logWarn(`Retry budget exhausted for ${bucket}`, {
|
|
1404
|
+
reason,
|
|
1405
|
+
profile: retryProfile,
|
|
1406
|
+
limits: retryBudget.getLimits(),
|
|
1407
|
+
usage: retryBudget.getUsage(),
|
|
1408
|
+
});
|
|
1409
|
+
return false;
|
|
1410
|
+
};
|
|
1057
1411
|
const abortSignal = requestInit?.signal ?? init?.signal ?? null;
|
|
1058
1412
|
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
1059
1413
|
if (abortSignal?.aborted) {
|
|
@@ -1104,11 +1458,28 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1104
1458
|
const attempted = new Set();
|
|
1105
1459
|
let restartAccountTraversalWithFallback = false;
|
|
1106
1460
|
while (attempted.size < Math.max(1, accountCount)) {
|
|
1461
|
+
const selectionExplainability = accountManager.getSelectionExplainability(modelFamily, model, Date.now());
|
|
1462
|
+
runtimeMetrics.lastSelectionSnapshot = {
|
|
1463
|
+
timestamp: Date.now(),
|
|
1464
|
+
family: modelFamily,
|
|
1465
|
+
model: model ?? null,
|
|
1466
|
+
selectedAccountIndex: null,
|
|
1467
|
+
quotaKey,
|
|
1468
|
+
explainability: selectionExplainability,
|
|
1469
|
+
};
|
|
1107
1470
|
const account = accountManager.getCurrentOrNextForFamilyHybrid(modelFamily, model, { pidOffsetEnabled });
|
|
1108
1471
|
if (!account || attempted.has(account.index)) {
|
|
1109
1472
|
break;
|
|
1110
1473
|
}
|
|
1111
1474
|
attempted.add(account.index);
|
|
1475
|
+
runtimeMetrics.lastSelectedAccountIndex = account.index;
|
|
1476
|
+
runtimeMetrics.lastQuotaKey = quotaKey;
|
|
1477
|
+
if (runtimeMetrics.lastSelectionSnapshot) {
|
|
1478
|
+
runtimeMetrics.lastSelectionSnapshot = {
|
|
1479
|
+
...runtimeMetrics.lastSelectionSnapshot,
|
|
1480
|
+
selectedAccountIndex: account.index,
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1112
1483
|
// Log account selection for debugging rotation
|
|
1113
1484
|
logDebug(`Using account ${account.index + 1}/${accountCount}: ${account.email ?? "unknown"} for ${modelFamily}`);
|
|
1114
1485
|
let accountAuth = accountManager.toAuthDetails(account);
|
|
@@ -1122,10 +1493,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1122
1493
|
}
|
|
1123
1494
|
catch (err) {
|
|
1124
1495
|
logDebug(`[${PLUGIN_NAME}] Auth refresh failed for account: ${err?.message ?? String(err)}`);
|
|
1496
|
+
if (!consumeRetryBudget("authRefresh", `Auth refresh failed for account ${account.index + 1}`)) {
|
|
1497
|
+
return new Response(JSON.stringify({
|
|
1498
|
+
error: {
|
|
1499
|
+
message: "Auth refresh retry budget exhausted for this request. Try again or switch accounts.",
|
|
1500
|
+
},
|
|
1501
|
+
}), {
|
|
1502
|
+
status: 503,
|
|
1503
|
+
headers: {
|
|
1504
|
+
"content-type": "application/json; charset=utf-8",
|
|
1505
|
+
},
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1125
1508
|
runtimeMetrics.authRefreshFailures++;
|
|
1126
1509
|
runtimeMetrics.failedRequests++;
|
|
1127
1510
|
runtimeMetrics.accountRotations++;
|
|
1128
1511
|
runtimeMetrics.lastError = err?.message ?? String(err);
|
|
1512
|
+
runtimeMetrics.lastErrorCategory = "auth-refresh";
|
|
1129
1513
|
const failures = accountManager.incrementAuthFailures(account);
|
|
1130
1514
|
const accountLabel = formatAccountLabel(account, account.index);
|
|
1131
1515
|
if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
|
|
@@ -1169,6 +1553,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1169
1553
|
runtimeMetrics.accountRotations++;
|
|
1170
1554
|
runtimeMetrics.lastError =
|
|
1171
1555
|
`Local token bucket depleted for account ${account.index + 1} (${modelFamily}${model ? `:${model}` : ""})`;
|
|
1556
|
+
runtimeMetrics.lastErrorCategory = "rate-limit-local";
|
|
1172
1557
|
logWarn(`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`);
|
|
1173
1558
|
break;
|
|
1174
1559
|
}
|
|
@@ -1200,10 +1585,24 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1200
1585
|
catch (networkError) {
|
|
1201
1586
|
const errorMsg = networkError instanceof Error ? networkError.message : String(networkError);
|
|
1202
1587
|
logWarn(`Network error for account ${account.index + 1}: ${errorMsg}`);
|
|
1588
|
+
if (!consumeRetryBudget("network", `Network error on account ${account.index + 1}: ${errorMsg}`)) {
|
|
1589
|
+
accountManager.refundToken(account, modelFamily, model);
|
|
1590
|
+
return new Response(JSON.stringify({
|
|
1591
|
+
error: {
|
|
1592
|
+
message: "Network retry budget exhausted for this request. Try again in a moment.",
|
|
1593
|
+
},
|
|
1594
|
+
}), {
|
|
1595
|
+
status: 503,
|
|
1596
|
+
headers: {
|
|
1597
|
+
"content-type": "application/json; charset=utf-8",
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1203
1601
|
runtimeMetrics.failedRequests++;
|
|
1204
1602
|
runtimeMetrics.networkErrors++;
|
|
1205
1603
|
runtimeMetrics.accountRotations++;
|
|
1206
1604
|
runtimeMetrics.lastError = errorMsg;
|
|
1605
|
+
runtimeMetrics.lastErrorCategory = "network";
|
|
1207
1606
|
accountManager.refundToken(account, modelFamily, model);
|
|
1208
1607
|
accountManager.recordFailure(account, modelFamily, model);
|
|
1209
1608
|
break;
|
|
@@ -1241,6 +1640,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1241
1640
|
accountManager.recordFailure(account, modelFamily, model);
|
|
1242
1641
|
account.lastSwitchReason = "rotation";
|
|
1243
1642
|
runtimeMetrics.lastError = `Unsupported model on account ${account.index + 1}: ${blockedModel}`;
|
|
1643
|
+
runtimeMetrics.lastErrorCategory = "unsupported-model";
|
|
1244
1644
|
logWarn(`Model ${blockedModel} is unsupported for account ${account.index + 1}. Trying next account/workspace before fallback.`, {
|
|
1245
1645
|
unsupportedCodexPolicy,
|
|
1246
1646
|
requestedModel: blockedModel,
|
|
@@ -1288,6 +1688,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1288
1688
|
body: JSON.stringify(transformedBody),
|
|
1289
1689
|
};
|
|
1290
1690
|
runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
|
|
1691
|
+
runtimeMetrics.lastErrorCategory = "model-fallback";
|
|
1291
1692
|
logWarn(`Model ${previousModel} is unsupported for this ChatGPT account. Falling back to ${model}.`, {
|
|
1292
1693
|
unsupportedCodexPolicy,
|
|
1293
1694
|
requestedModel: previousModel,
|
|
@@ -1302,6 +1703,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1302
1703
|
if (unsupportedModelInfo.isUnsupported && !fallbackOnUnsupportedCodexModel) {
|
|
1303
1704
|
const blockedModel = unsupportedModelInfo.unsupportedModel ?? model ?? "requested model";
|
|
1304
1705
|
runtimeMetrics.lastError = `Unsupported model (strict): ${blockedModel}`;
|
|
1706
|
+
runtimeMetrics.lastErrorCategory = "unsupported-model";
|
|
1305
1707
|
logWarn(`Model ${blockedModel} is unsupported for this ChatGPT account. Strict policy blocks automatic fallback.`, {
|
|
1306
1708
|
unsupportedCodexPolicy,
|
|
1307
1709
|
requestedModel: blockedModel,
|
|
@@ -1324,15 +1726,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1324
1726
|
runtimeMetrics.serverErrors++;
|
|
1325
1727
|
runtimeMetrics.accountRotations++;
|
|
1326
1728
|
runtimeMetrics.lastError = `HTTP ${response.status}`;
|
|
1729
|
+
runtimeMetrics.lastErrorCategory = "server";
|
|
1327
1730
|
accountManager.refundToken(account, modelFamily, model);
|
|
1328
1731
|
accountManager.recordFailure(account, modelFamily, model);
|
|
1732
|
+
if (!consumeRetryBudget("server", `Server error ${response.status} on account ${account.index + 1}`)) {
|
|
1733
|
+
return errorResponse;
|
|
1734
|
+
}
|
|
1329
1735
|
break;
|
|
1330
1736
|
}
|
|
1331
1737
|
if (rateLimit) {
|
|
1332
1738
|
runtimeMetrics.rateLimitedResponses++;
|
|
1333
1739
|
const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, rateLimit.retryAfterMs);
|
|
1334
1740
|
const waitLabel = formatWaitTime(delayMs);
|
|
1335
|
-
if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS
|
|
1741
|
+
if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS &&
|
|
1742
|
+
consumeRetryBudget("rateLimitShort", `Short 429 retry for account ${account.index + 1} after ${delayMs}ms`)) {
|
|
1336
1743
|
if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
1337
1744
|
await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });
|
|
1338
1745
|
accountManager.markToastShown(account.index);
|
|
@@ -1344,6 +1751,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1344
1751
|
accountManager.recordRateLimit(account, modelFamily, model);
|
|
1345
1752
|
account.lastSwitchReason = "rate-limit";
|
|
1346
1753
|
runtimeMetrics.accountRotations++;
|
|
1754
|
+
runtimeMetrics.lastErrorCategory = "rate-limit";
|
|
1347
1755
|
accountManager.saveToDiskDebounced();
|
|
1348
1756
|
logWarn(`Rate limited. Rotating account ${account.index + 1} (${account.email ?? "unknown"}).`);
|
|
1349
1757
|
if (accountManager.getAccountCount() > 1 &&
|
|
@@ -1355,6 +1763,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1355
1763
|
}
|
|
1356
1764
|
runtimeMetrics.failedRequests++;
|
|
1357
1765
|
runtimeMetrics.lastError = `HTTP ${response.status}`;
|
|
1766
|
+
runtimeMetrics.lastErrorCategory = "http";
|
|
1358
1767
|
return errorResponse;
|
|
1359
1768
|
}
|
|
1360
1769
|
resetRateLimitBackoff(account.index, quotaKey);
|
|
@@ -1365,6 +1774,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1365
1774
|
if (!successResponse.ok) {
|
|
1366
1775
|
runtimeMetrics.failedRequests++;
|
|
1367
1776
|
runtimeMetrics.lastError = `HTTP ${successResponse.status}`;
|
|
1777
|
+
runtimeMetrics.lastErrorCategory = "http";
|
|
1368
1778
|
return successResponse;
|
|
1369
1779
|
}
|
|
1370
1780
|
if (!isStreaming && emptyResponseMaxRetries > 0) {
|
|
@@ -1373,7 +1783,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1373
1783
|
const bodyText = await clonedResponse.text();
|
|
1374
1784
|
const parsedBody = bodyText ? JSON.parse(bodyText) : null;
|
|
1375
1785
|
if (isEmptyResponse(parsedBody)) {
|
|
1376
|
-
if (emptyResponseRetries < emptyResponseMaxRetries
|
|
1786
|
+
if (emptyResponseRetries < emptyResponseMaxRetries &&
|
|
1787
|
+
consumeRetryBudget("emptyResponse", `Empty response retry ${emptyResponseRetries + 1}/${emptyResponseMaxRetries}`)) {
|
|
1377
1788
|
emptyResponseRetries++;
|
|
1378
1789
|
runtimeMetrics.emptyResponseRetries++;
|
|
1379
1790
|
logWarn(`Empty response received (attempt ${emptyResponseRetries}/${emptyResponseMaxRetries}). Retrying...`);
|
|
@@ -1393,6 +1804,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1393
1804
|
accountManager.recordSuccess(account, modelFamily, model);
|
|
1394
1805
|
runtimeMetrics.successfulRequests++;
|
|
1395
1806
|
runtimeMetrics.lastError = null;
|
|
1807
|
+
runtimeMetrics.lastErrorCategory = null;
|
|
1396
1808
|
return successResponse;
|
|
1397
1809
|
}
|
|
1398
1810
|
if (restartAccountTraversalWithFallback) {
|
|
@@ -1409,7 +1821,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1409
1821
|
waitMs > 0 &&
|
|
1410
1822
|
(retryAllAccountsMaxWaitMs === 0 ||
|
|
1411
1823
|
waitMs <= retryAllAccountsMaxWaitMs) &&
|
|
1412
|
-
allRateLimitedRetries < retryAllAccountsMaxRetries
|
|
1824
|
+
allRateLimitedRetries < retryAllAccountsMaxRetries &&
|
|
1825
|
+
consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms`)) {
|
|
1413
1826
|
const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
|
|
1414
1827
|
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
|
|
1415
1828
|
allRateLimitedRetries++;
|
|
@@ -1423,6 +1836,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1423
1836
|
: `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
|
|
1424
1837
|
runtimeMetrics.failedRequests++;
|
|
1425
1838
|
runtimeMetrics.lastError = message;
|
|
1839
|
+
runtimeMetrics.lastErrorCategory = waitMs > 0 ? "rate-limit" : "account-failure";
|
|
1426
1840
|
return new Response(JSON.stringify({ error: { message } }), {
|
|
1427
1841
|
status: waitMs > 0 ? 429 : 503,
|
|
1428
1842
|
headers: {
|
|
@@ -2282,11 +2696,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2282
2696
|
tool: {
|
|
2283
2697
|
"codex-list": tool({
|
|
2284
2698
|
description: "List all Codex OAuth accounts and the current active index.",
|
|
2285
|
-
args: {
|
|
2286
|
-
|
|
2699
|
+
args: {
|
|
2700
|
+
tag: tool.schema
|
|
2701
|
+
.string()
|
|
2702
|
+
.optional()
|
|
2703
|
+
.describe("Optional tag filter (e.g., work, personal, team-a)."),
|
|
2704
|
+
},
|
|
2705
|
+
async execute({ tag } = {}) {
|
|
2287
2706
|
const ui = resolveUiRuntime();
|
|
2288
2707
|
const storage = await loadAccounts();
|
|
2289
2708
|
const storePath = getStoragePath();
|
|
2709
|
+
const normalizedTag = tag?.trim().toLowerCase() ?? "";
|
|
2290
2710
|
if (!storage || storage.accounts.length === 0) {
|
|
2291
2711
|
if (ui.v2Enabled) {
|
|
2292
2712
|
return [
|
|
@@ -2294,6 +2714,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2294
2714
|
"",
|
|
2295
2715
|
formatUiItem(ui, "No accounts configured.", "warning"),
|
|
2296
2716
|
formatUiItem(ui, "Run: opencode auth login", "accent"),
|
|
2717
|
+
formatUiItem(ui, "Setup checklist: codex-setup"),
|
|
2718
|
+
formatUiItem(ui, "Command guide: codex-help"),
|
|
2297
2719
|
formatUiKeyValue(ui, "Storage", storePath, "muted"),
|
|
2298
2720
|
].join("\n");
|
|
2299
2721
|
}
|
|
@@ -2302,21 +2724,47 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2302
2724
|
"",
|
|
2303
2725
|
"Add accounts:",
|
|
2304
2726
|
" opencode auth login",
|
|
2727
|
+
" codex-setup",
|
|
2728
|
+
" codex-help",
|
|
2305
2729
|
"",
|
|
2306
2730
|
`Storage: ${storePath}`,
|
|
2307
2731
|
].join("\n");
|
|
2308
2732
|
}
|
|
2309
2733
|
const now = Date.now();
|
|
2310
2734
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
2735
|
+
const filteredEntries = storage.accounts
|
|
2736
|
+
.map((account, index) => ({ account, index }))
|
|
2737
|
+
.filter(({ account }) => {
|
|
2738
|
+
if (!normalizedTag)
|
|
2739
|
+
return true;
|
|
2740
|
+
const tags = Array.isArray(account.accountTags)
|
|
2741
|
+
? account.accountTags.map((entry) => entry.trim().toLowerCase())
|
|
2742
|
+
: [];
|
|
2743
|
+
return tags.includes(normalizedTag);
|
|
2744
|
+
});
|
|
2745
|
+
if (normalizedTag && filteredEntries.length === 0) {
|
|
2746
|
+
if (ui.v2Enabled) {
|
|
2747
|
+
return [
|
|
2748
|
+
...formatUiHeader(ui, "Codex accounts"),
|
|
2749
|
+
"",
|
|
2750
|
+
formatUiItem(ui, `No accounts found for tag: ${normalizedTag}`, "warning"),
|
|
2751
|
+
formatUiItem(ui, "Use codex-tag index=2 tags=\"work,team-a\" to add tags.", "accent"),
|
|
2752
|
+
].join("\n");
|
|
2753
|
+
}
|
|
2754
|
+
return `No accounts found for tag: ${normalizedTag}\n\nUse codex-tag index=2 tags="work,team-a" to add tags.`;
|
|
2755
|
+
}
|
|
2311
2756
|
if (ui.v2Enabled) {
|
|
2312
2757
|
const lines = [
|
|
2313
2758
|
...formatUiHeader(ui, "Codex accounts"),
|
|
2314
|
-
formatUiKeyValue(ui, "Total", String(
|
|
2759
|
+
formatUiKeyValue(ui, "Total", String(filteredEntries.length)),
|
|
2760
|
+
normalizedTag
|
|
2761
|
+
? formatUiKeyValue(ui, "Filter tag", normalizedTag, "accent")
|
|
2762
|
+
: formatUiKeyValue(ui, "Filter tag", "none", "muted"),
|
|
2315
2763
|
formatUiKeyValue(ui, "Storage", storePath, "muted"),
|
|
2316
2764
|
"",
|
|
2317
2765
|
...formatUiSection(ui, "Accounts"),
|
|
2318
2766
|
];
|
|
2319
|
-
|
|
2767
|
+
filteredEntries.forEach(({ account, index }) => {
|
|
2320
2768
|
const label = formatCommandAccountLabel(account, index);
|
|
2321
2769
|
const badges = [];
|
|
2322
2770
|
if (index === activeIndex)
|
|
@@ -2341,9 +2789,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2341
2789
|
lines.push("");
|
|
2342
2790
|
lines.push(...formatUiSection(ui, "Commands"));
|
|
2343
2791
|
lines.push(formatUiItem(ui, "Add account: opencode auth login", "accent"));
|
|
2344
|
-
lines.push(formatUiItem(ui, "Switch account: codex-switch
|
|
2792
|
+
lines.push(formatUiItem(ui, "Switch account: codex-switch index=2"));
|
|
2345
2793
|
lines.push(formatUiItem(ui, "Detailed status: codex-status"));
|
|
2794
|
+
lines.push(formatUiItem(ui, "Live dashboard: codex-dashboard"));
|
|
2346
2795
|
lines.push(formatUiItem(ui, "Runtime metrics: codex-metrics"));
|
|
2796
|
+
lines.push(formatUiItem(ui, "Set account tags: codex-tag index=2 tags=\"work,team-a\""));
|
|
2797
|
+
lines.push(formatUiItem(ui, "Set account note: codex-note index=2 note=\"weekday primary\""));
|
|
2798
|
+
lines.push(formatUiItem(ui, "Doctor checks: codex-doctor"));
|
|
2799
|
+
lines.push(formatUiItem(ui, "Onboarding checklist: codex-setup"));
|
|
2800
|
+
lines.push(formatUiItem(ui, "Guided setup wizard: codex-setup --wizard"));
|
|
2801
|
+
lines.push(formatUiItem(ui, "Best next action: codex-next"));
|
|
2802
|
+
lines.push(formatUiItem(ui, "Rename account label: codex-label index=2 label=\"Work\""));
|
|
2803
|
+
lines.push(formatUiItem(ui, "Command guide: codex-help"));
|
|
2347
2804
|
return lines.join("\n");
|
|
2348
2805
|
}
|
|
2349
2806
|
const listTableOptions = {
|
|
@@ -2354,11 +2811,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2354
2811
|
],
|
|
2355
2812
|
};
|
|
2356
2813
|
const lines = [
|
|
2357
|
-
`Codex Accounts (${
|
|
2814
|
+
`Codex Accounts (${filteredEntries.length}):`,
|
|
2358
2815
|
"",
|
|
2359
2816
|
...buildTableHeader(listTableOptions),
|
|
2360
2817
|
];
|
|
2361
|
-
|
|
2818
|
+
filteredEntries.forEach(({ account, index }) => {
|
|
2362
2819
|
const label = formatCommandAccountLabel(account, index);
|
|
2363
2820
|
const statuses = [];
|
|
2364
2821
|
const rateLimit = formatRateLimitEntry(account, now);
|
|
@@ -2376,21 +2833,33 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2376
2833
|
});
|
|
2377
2834
|
lines.push("");
|
|
2378
2835
|
lines.push(`Storage: ${storePath}`);
|
|
2836
|
+
if (normalizedTag) {
|
|
2837
|
+
lines.push(`Filter tag: ${normalizedTag}`);
|
|
2838
|
+
}
|
|
2379
2839
|
lines.push("");
|
|
2380
2840
|
lines.push("Commands:");
|
|
2381
2841
|
lines.push(" - Add account: opencode auth login");
|
|
2382
2842
|
lines.push(" - Switch account: codex-switch");
|
|
2383
2843
|
lines.push(" - Status details: codex-status");
|
|
2844
|
+
lines.push(" - Live dashboard: codex-dashboard");
|
|
2384
2845
|
lines.push(" - Runtime metrics: codex-metrics");
|
|
2846
|
+
lines.push(" - Set account tags: codex-tag");
|
|
2847
|
+
lines.push(" - Set account note: codex-note");
|
|
2848
|
+
lines.push(" - Doctor checks: codex-doctor");
|
|
2849
|
+
lines.push(" - Setup checklist: codex-setup");
|
|
2850
|
+
lines.push(" - Guided setup wizard: codex-setup --wizard");
|
|
2851
|
+
lines.push(" - Best next action: codex-next");
|
|
2852
|
+
lines.push(" - Rename account label: codex-label");
|
|
2853
|
+
lines.push(" - Command guide: codex-help");
|
|
2385
2854
|
return lines.join("\n");
|
|
2386
2855
|
},
|
|
2387
2856
|
}),
|
|
2388
2857
|
"codex-switch": tool({
|
|
2389
|
-
description: "Switch active Codex account by index (1-based).",
|
|
2858
|
+
description: "Switch active Codex account by index (1-based) or interactive picker when index is omitted.",
|
|
2390
2859
|
args: {
|
|
2391
|
-
index: tool.schema.number().describe("Account number to switch to (1-based, e.g., 1 for first account)"),
|
|
2860
|
+
index: tool.schema.number().optional().describe("Account number to switch to (1-based, e.g., 1 for first account)"),
|
|
2392
2861
|
},
|
|
2393
|
-
async execute({ index }) {
|
|
2862
|
+
async execute({ index } = {}) {
|
|
2394
2863
|
const ui = resolveUiRuntime();
|
|
2395
2864
|
const storage = await loadAccounts();
|
|
2396
2865
|
if (!storage || storage.accounts.length === 0) {
|
|
@@ -2404,7 +2873,34 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2404
2873
|
}
|
|
2405
2874
|
return "No Codex accounts configured. Run: opencode auth login";
|
|
2406
2875
|
}
|
|
2407
|
-
|
|
2876
|
+
let resolvedIndex = index;
|
|
2877
|
+
if (resolvedIndex === undefined) {
|
|
2878
|
+
const selectedIndex = await promptAccountIndexSelection(ui, storage, "Switch account");
|
|
2879
|
+
if (selectedIndex === null) {
|
|
2880
|
+
if (supportsInteractiveMenus()) {
|
|
2881
|
+
if (ui.v2Enabled) {
|
|
2882
|
+
return [
|
|
2883
|
+
...formatUiHeader(ui, "Switch account"),
|
|
2884
|
+
"",
|
|
2885
|
+
formatUiItem(ui, "No account selected.", "warning"),
|
|
2886
|
+
formatUiItem(ui, "Run again and pick an account, or pass codex-switch index=2.", "muted"),
|
|
2887
|
+
].join("\n");
|
|
2888
|
+
}
|
|
2889
|
+
return "No account selected.";
|
|
2890
|
+
}
|
|
2891
|
+
if (ui.v2Enabled) {
|
|
2892
|
+
return [
|
|
2893
|
+
...formatUiHeader(ui, "Switch account"),
|
|
2894
|
+
"",
|
|
2895
|
+
formatUiItem(ui, "Missing account number.", "warning"),
|
|
2896
|
+
formatUiItem(ui, "Use: codex-switch index=2", "accent"),
|
|
2897
|
+
].join("\n");
|
|
2898
|
+
}
|
|
2899
|
+
return "Missing account number. Use: codex-switch index=2";
|
|
2900
|
+
}
|
|
2901
|
+
resolvedIndex = selectedIndex + 1;
|
|
2902
|
+
}
|
|
2903
|
+
const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
|
|
2408
2904
|
if (!Number.isFinite(targetIndex) ||
|
|
2409
2905
|
targetIndex < 0 ||
|
|
2410
2906
|
targetIndex >= storage.accounts.length) {
|
|
@@ -2412,11 +2908,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2412
2908
|
return [
|
|
2413
2909
|
...formatUiHeader(ui, "Switch account"),
|
|
2414
2910
|
"",
|
|
2415
|
-
formatUiItem(ui, `Invalid account number: ${
|
|
2911
|
+
formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
|
|
2416
2912
|
formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
|
|
2417
2913
|
].join("\n");
|
|
2418
2914
|
}
|
|
2419
|
-
return `Invalid account number: ${
|
|
2915
|
+
return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
|
|
2420
2916
|
}
|
|
2421
2917
|
const now = Date.now();
|
|
2422
2918
|
const account = storage.accounts[targetIndex];
|
|
@@ -2480,10 +2976,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2480
2976
|
}
|
|
2481
2977
|
const now = Date.now();
|
|
2482
2978
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
2979
|
+
const explainabilityFamily = runtimeMetrics.lastSelectionSnapshot?.family ?? "codex";
|
|
2980
|
+
const explainabilityModel = runtimeMetrics.lastSelectionSnapshot?.model ?? undefined;
|
|
2981
|
+
const managerForExplainability = cachedAccountManager ?? (await AccountManager.loadFromDisk());
|
|
2982
|
+
const explainability = managerForExplainability.getSelectionExplainability(explainabilityFamily, explainabilityModel, now);
|
|
2983
|
+
const explainabilityByIndex = new Map(explainability.map((entry) => [entry.index, entry]));
|
|
2483
2984
|
if (ui.v2Enabled) {
|
|
2484
2985
|
const lines = [
|
|
2485
2986
|
...formatUiHeader(ui, "Account status"),
|
|
2486
2987
|
formatUiKeyValue(ui, "Total", String(storage.accounts.length)),
|
|
2988
|
+
formatUiKeyValue(ui, "Selection view", explainabilityModel
|
|
2989
|
+
? `${explainabilityFamily}:${explainabilityModel}`
|
|
2990
|
+
: explainabilityFamily, "muted"),
|
|
2487
2991
|
"",
|
|
2488
2992
|
...formatUiSection(ui, "Accounts"),
|
|
2489
2993
|
];
|
|
@@ -2524,6 +3028,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2524
3028
|
});
|
|
2525
3029
|
lines.push(formatUiItem(ui, `Account ${index + 1}: ${statuses.join(" | ")}`));
|
|
2526
3030
|
});
|
|
3031
|
+
lines.push("");
|
|
3032
|
+
lines.push(...formatUiSection(ui, "Selection explainability"));
|
|
3033
|
+
for (const entry of explainability) {
|
|
3034
|
+
const state = entry.eligible ? "eligible" : "blocked";
|
|
3035
|
+
const reasons = entry.reasons.join(", ");
|
|
3036
|
+
lines.push(formatUiItem(ui, `Account ${entry.index + 1}: ${state} | health=${Math.round(entry.healthScore)} | tokens=${entry.tokensAvailable.toFixed(1)} | ${reasons}`));
|
|
3037
|
+
}
|
|
3038
|
+
const nextAction = recommendBeginnerNextAction({
|
|
3039
|
+
accounts: toBeginnerAccountSnapshots(storage, activeIndex, now),
|
|
3040
|
+
now,
|
|
3041
|
+
runtime: getBeginnerRuntimeSnapshot(),
|
|
3042
|
+
});
|
|
3043
|
+
lines.push("");
|
|
3044
|
+
lines.push(...formatUiSection(ui, "Recommended next step"));
|
|
3045
|
+
lines.push(formatUiItem(ui, nextAction, "accent"));
|
|
2527
3046
|
return lines.join("\n");
|
|
2528
3047
|
}
|
|
2529
3048
|
const statusTableOptions = {
|
|
@@ -2569,6 +3088,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2569
3088
|
});
|
|
2570
3089
|
lines.push(` Account ${index + 1}: ${statuses.join(" | ")}`);
|
|
2571
3090
|
});
|
|
3091
|
+
lines.push("");
|
|
3092
|
+
lines.push(`Selection explainability (${explainabilityModel ? `${explainabilityFamily}:${explainabilityModel}` : explainabilityFamily}):`);
|
|
3093
|
+
for (const [index] of storage.accounts.entries()) {
|
|
3094
|
+
const details = explainabilityByIndex.get(index);
|
|
3095
|
+
if (!details)
|
|
3096
|
+
continue;
|
|
3097
|
+
const state = details.eligible ? "eligible" : "blocked";
|
|
3098
|
+
lines.push(` Account ${index + 1}: ${state} | health=${Math.round(details.healthScore)} | tokens=${details.tokensAvailable.toFixed(1)} | ${details.reasons.join(", ")}`);
|
|
3099
|
+
}
|
|
3100
|
+
lines.push("");
|
|
3101
|
+
lines.push(`Recommended next step: ${recommendBeginnerNextAction({
|
|
3102
|
+
accounts: toBeginnerAccountSnapshots(storage, activeIndex, now),
|
|
3103
|
+
now,
|
|
3104
|
+
runtime: getBeginnerRuntimeSnapshot(),
|
|
3105
|
+
})}`);
|
|
2572
3106
|
return lines.join("\n");
|
|
2573
3107
|
},
|
|
2574
3108
|
}),
|
|
@@ -2581,6 +3115,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2581
3115
|
const uptimeMs = Math.max(0, now - runtimeMetrics.startedAt);
|
|
2582
3116
|
const total = runtimeMetrics.totalRequests;
|
|
2583
3117
|
const successful = runtimeMetrics.successfulRequests;
|
|
3118
|
+
const refreshMetrics = getRefreshQueueMetrics();
|
|
2584
3119
|
const successRate = total > 0 ? ((successful / total) * 100).toFixed(1) : "0.0";
|
|
2585
3120
|
const avgLatencyMs = successful > 0
|
|
2586
3121
|
? Math.round(runtimeMetrics.cumulativeLatencyMs / successful)
|
|
@@ -2603,11 +3138,41 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2603
3138
|
`Auth refresh failures: ${runtimeMetrics.authRefreshFailures}`,
|
|
2604
3139
|
`Account rotations: ${runtimeMetrics.accountRotations}`,
|
|
2605
3140
|
`Empty-response retries: ${runtimeMetrics.emptyResponseRetries}`,
|
|
3141
|
+
`Retry profile: ${runtimeMetrics.retryProfile}`,
|
|
3142
|
+
`Beginner safe mode: ${beginnerSafeModeEnabled ? "on" : "off"}`,
|
|
3143
|
+
`Retry budget exhaustions: ${runtimeMetrics.retryBudgetExhaustions}`,
|
|
3144
|
+
`Retry budget usage (auth/network/server/short/global/empty): ` +
|
|
3145
|
+
`${runtimeMetrics.retryBudgetUsage.authRefresh}/` +
|
|
3146
|
+
`${runtimeMetrics.retryBudgetUsage.network}/` +
|
|
3147
|
+
`${runtimeMetrics.retryBudgetUsage.server}/` +
|
|
3148
|
+
`${runtimeMetrics.retryBudgetUsage.rateLimitShort}/` +
|
|
3149
|
+
`${runtimeMetrics.retryBudgetUsage.rateLimitGlobal}/` +
|
|
3150
|
+
`${runtimeMetrics.retryBudgetUsage.emptyResponse}`,
|
|
3151
|
+
`Refresh queue (started/success/failed/pending): ` +
|
|
3152
|
+
`${refreshMetrics.started}/` +
|
|
3153
|
+
`${refreshMetrics.succeeded}/` +
|
|
3154
|
+
`${refreshMetrics.failed}/` +
|
|
3155
|
+
`${refreshMetrics.pending}`,
|
|
2606
3156
|
`Last upstream request: ${lastRequest}`,
|
|
2607
3157
|
];
|
|
2608
3158
|
if (runtimeMetrics.lastError) {
|
|
2609
3159
|
lines.push(`Last error: ${runtimeMetrics.lastError}`);
|
|
2610
3160
|
}
|
|
3161
|
+
if (runtimeMetrics.lastErrorCategory) {
|
|
3162
|
+
lines.push(`Last error category: ${runtimeMetrics.lastErrorCategory}`);
|
|
3163
|
+
}
|
|
3164
|
+
if (runtimeMetrics.lastSelectedAccountIndex !== null) {
|
|
3165
|
+
lines.push(`Last selected account: ${runtimeMetrics.lastSelectedAccountIndex + 1}`);
|
|
3166
|
+
}
|
|
3167
|
+
if (runtimeMetrics.lastQuotaKey) {
|
|
3168
|
+
lines.push(`Last quota key: ${runtimeMetrics.lastQuotaKey}`);
|
|
3169
|
+
}
|
|
3170
|
+
if (runtimeMetrics.lastRetryBudgetExhaustedClass) {
|
|
3171
|
+
lines.push(`Last budget exhaustion: ${runtimeMetrics.lastRetryBudgetExhaustedClass}` +
|
|
3172
|
+
(runtimeMetrics.lastRetryBudgetReason
|
|
3173
|
+
? ` (${runtimeMetrics.lastRetryBudgetReason})`
|
|
3174
|
+
: ""));
|
|
3175
|
+
}
|
|
2611
3176
|
if (ui.v2Enabled) {
|
|
2612
3177
|
const styled = [
|
|
2613
3178
|
...formatUiHeader(ui, "Codex plugin metrics"),
|
|
@@ -2623,16 +3188,730 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2623
3188
|
formatUiKeyValue(ui, "Auth refresh failures", String(runtimeMetrics.authRefreshFailures), "warning"),
|
|
2624
3189
|
formatUiKeyValue(ui, "Account rotations", String(runtimeMetrics.accountRotations), "accent"),
|
|
2625
3190
|
formatUiKeyValue(ui, "Empty-response retries", String(runtimeMetrics.emptyResponseRetries), "warning"),
|
|
3191
|
+
formatUiKeyValue(ui, "Retry profile", runtimeMetrics.retryProfile, "muted"),
|
|
3192
|
+
formatUiKeyValue(ui, "Beginner safe mode", beginnerSafeModeEnabled ? "on" : "off", beginnerSafeModeEnabled ? "accent" : "muted"),
|
|
3193
|
+
formatUiKeyValue(ui, "Retry budget exhaustions", String(runtimeMetrics.retryBudgetExhaustions), "warning"),
|
|
3194
|
+
formatUiKeyValue(ui, "Retry budget usage", `A${runtimeMetrics.retryBudgetUsage.authRefresh} N${runtimeMetrics.retryBudgetUsage.network} S${runtimeMetrics.retryBudgetUsage.server} RS${runtimeMetrics.retryBudgetUsage.rateLimitShort} RG${runtimeMetrics.retryBudgetUsage.rateLimitGlobal} E${runtimeMetrics.retryBudgetUsage.emptyResponse}`, "muted"),
|
|
3195
|
+
formatUiKeyValue(ui, "Retry budget limits", `A${runtimeMetrics.retryBudgetLimits.authRefresh} N${runtimeMetrics.retryBudgetLimits.network} S${runtimeMetrics.retryBudgetLimits.server} RS${runtimeMetrics.retryBudgetLimits.rateLimitShort} RG${runtimeMetrics.retryBudgetLimits.rateLimitGlobal} E${runtimeMetrics.retryBudgetLimits.emptyResponse}`, "muted"),
|
|
3196
|
+
formatUiKeyValue(ui, "Refresh queue", `started=${refreshMetrics.started} dedup=${refreshMetrics.deduplicated} reuse=${refreshMetrics.rotationReused} success=${refreshMetrics.succeeded} failed=${refreshMetrics.failed} pending=${refreshMetrics.pending}`, "muted"),
|
|
2626
3197
|
formatUiKeyValue(ui, "Last upstream request", lastRequest, "muted"),
|
|
2627
3198
|
];
|
|
2628
3199
|
if (runtimeMetrics.lastError) {
|
|
2629
3200
|
styled.push(formatUiKeyValue(ui, "Last error", runtimeMetrics.lastError, "danger"));
|
|
2630
3201
|
}
|
|
3202
|
+
if (runtimeMetrics.lastErrorCategory) {
|
|
3203
|
+
styled.push(formatUiKeyValue(ui, "Last error category", runtimeMetrics.lastErrorCategory, "warning"));
|
|
3204
|
+
}
|
|
3205
|
+
if (runtimeMetrics.lastSelectedAccountIndex !== null) {
|
|
3206
|
+
styled.push(formatUiKeyValue(ui, "Last selected account", String(runtimeMetrics.lastSelectedAccountIndex + 1), "accent"));
|
|
3207
|
+
}
|
|
3208
|
+
if (runtimeMetrics.lastQuotaKey) {
|
|
3209
|
+
styled.push(formatUiKeyValue(ui, "Last quota key", runtimeMetrics.lastQuotaKey, "muted"));
|
|
3210
|
+
}
|
|
3211
|
+
if (runtimeMetrics.lastRetryBudgetExhaustedClass) {
|
|
3212
|
+
styled.push(formatUiKeyValue(ui, "Last budget exhaustion", runtimeMetrics.lastRetryBudgetReason
|
|
3213
|
+
? `${runtimeMetrics.lastRetryBudgetExhaustedClass} (${runtimeMetrics.lastRetryBudgetReason})`
|
|
3214
|
+
: runtimeMetrics.lastRetryBudgetExhaustedClass, "warning"));
|
|
3215
|
+
}
|
|
2631
3216
|
return Promise.resolve(styled.join("\n"));
|
|
2632
3217
|
}
|
|
2633
3218
|
return Promise.resolve(lines.join("\n"));
|
|
2634
3219
|
},
|
|
2635
3220
|
}),
|
|
3221
|
+
"codex-help": tool({
|
|
3222
|
+
description: "Beginner-friendly command guide with quickstart and troubleshooting flows.",
|
|
3223
|
+
args: {
|
|
3224
|
+
topic: tool.schema
|
|
3225
|
+
.string()
|
|
3226
|
+
.optional()
|
|
3227
|
+
.describe("Optional topic: setup, switch, health, backup, dashboard, metrics."),
|
|
3228
|
+
},
|
|
3229
|
+
async execute({ topic }) {
|
|
3230
|
+
const ui = resolveUiRuntime();
|
|
3231
|
+
await Promise.resolve();
|
|
3232
|
+
const normalizedTopic = (topic ?? "").trim().toLowerCase();
|
|
3233
|
+
const sections = [
|
|
3234
|
+
{
|
|
3235
|
+
key: "setup",
|
|
3236
|
+
title: "Quickstart",
|
|
3237
|
+
lines: [
|
|
3238
|
+
"1) Add account: opencode auth login",
|
|
3239
|
+
"2) Verify account health: codex-health",
|
|
3240
|
+
"3) View account list: codex-list",
|
|
3241
|
+
"4) Run checklist: codex-setup",
|
|
3242
|
+
"5) Use guided wizard: codex-setup --wizard",
|
|
3243
|
+
"6) Start requests and monitor: codex-dashboard",
|
|
3244
|
+
],
|
|
3245
|
+
},
|
|
3246
|
+
{
|
|
3247
|
+
key: "switch",
|
|
3248
|
+
title: "Daily account operations",
|
|
3249
|
+
lines: [
|
|
3250
|
+
"List accounts: codex-list",
|
|
3251
|
+
"Switch active account: codex-switch index=2",
|
|
3252
|
+
"Show detailed status: codex-status",
|
|
3253
|
+
"Set account label: codex-label index=2 label=\"Work\"",
|
|
3254
|
+
"Set account tags: codex-tag index=2 tags=\"work,team-a\"",
|
|
3255
|
+
"Set account note: codex-note index=2 note=\"weekday primary\"",
|
|
3256
|
+
"Filter by tag: codex-list tag=\"work\"",
|
|
3257
|
+
"Remove account: codex-remove index=2",
|
|
3258
|
+
],
|
|
3259
|
+
},
|
|
3260
|
+
{
|
|
3261
|
+
key: "health",
|
|
3262
|
+
title: "Health and recovery",
|
|
3263
|
+
lines: [
|
|
3264
|
+
"Verify token health: codex-health",
|
|
3265
|
+
"Refresh all tokens: codex-refresh",
|
|
3266
|
+
"Run diagnostics: codex-doctor",
|
|
3267
|
+
"Run diagnostics with fixes: codex-doctor --fix",
|
|
3268
|
+
"Show best next action: codex-next",
|
|
3269
|
+
"Run guided wizard: codex-setup --wizard",
|
|
3270
|
+
],
|
|
3271
|
+
},
|
|
3272
|
+
{
|
|
3273
|
+
key: "dashboard",
|
|
3274
|
+
title: "Monitoring",
|
|
3275
|
+
lines: [
|
|
3276
|
+
"Live dashboard: codex-dashboard",
|
|
3277
|
+
"Runtime metrics: codex-metrics",
|
|
3278
|
+
"Per-account status detail: codex-status",
|
|
3279
|
+
],
|
|
3280
|
+
},
|
|
3281
|
+
{
|
|
3282
|
+
key: "backup",
|
|
3283
|
+
title: "Backup and migration",
|
|
3284
|
+
lines: [
|
|
3285
|
+
"Export accounts: codex-export <path>",
|
|
3286
|
+
"Auto backup export: codex-export",
|
|
3287
|
+
"Import preview: codex-import <path> --dryRun",
|
|
3288
|
+
"Import apply: codex-import <path>",
|
|
3289
|
+
"Setup checklist: codex-setup",
|
|
3290
|
+
],
|
|
3291
|
+
},
|
|
3292
|
+
];
|
|
3293
|
+
const visibleSections = normalizedTopic.length === 0
|
|
3294
|
+
? sections
|
|
3295
|
+
: sections.filter((section) => section.key.includes(normalizedTopic));
|
|
3296
|
+
if (visibleSections.length === 0) {
|
|
3297
|
+
const available = sections.map((section) => section.key).join(", ");
|
|
3298
|
+
if (ui.v2Enabled) {
|
|
3299
|
+
return [
|
|
3300
|
+
...formatUiHeader(ui, "Codex help"),
|
|
3301
|
+
"",
|
|
3302
|
+
formatUiItem(ui, `Unknown topic: ${normalizedTopic}`, "warning"),
|
|
3303
|
+
formatUiItem(ui, `Available topics: ${available}`, "muted"),
|
|
3304
|
+
].join("\n");
|
|
3305
|
+
}
|
|
3306
|
+
return `Unknown topic: ${normalizedTopic}\n\nAvailable topics: ${available}`;
|
|
3307
|
+
}
|
|
3308
|
+
if (ui.v2Enabled) {
|
|
3309
|
+
const lines = [...formatUiHeader(ui, "Codex help"), ""];
|
|
3310
|
+
for (const section of visibleSections) {
|
|
3311
|
+
lines.push(...formatUiSection(ui, section.title));
|
|
3312
|
+
for (const line of section.lines) {
|
|
3313
|
+
lines.push(formatUiItem(ui, line));
|
|
3314
|
+
}
|
|
3315
|
+
lines.push("");
|
|
3316
|
+
}
|
|
3317
|
+
lines.push(...formatUiSection(ui, "Tips"));
|
|
3318
|
+
lines.push(formatUiItem(ui, "Run codex-setup after adding accounts."));
|
|
3319
|
+
lines.push(formatUiItem(ui, "Use codex-setup --wizard for menu-driven onboarding."));
|
|
3320
|
+
lines.push(formatUiItem(ui, "Use codex-doctor when request failures increase."));
|
|
3321
|
+
return lines.join("\n").trimEnd();
|
|
3322
|
+
}
|
|
3323
|
+
const lines = ["Codex Help:", ""];
|
|
3324
|
+
for (const section of visibleSections) {
|
|
3325
|
+
lines.push(`${section.title}:`);
|
|
3326
|
+
for (const line of section.lines) {
|
|
3327
|
+
lines.push(` - ${line}`);
|
|
3328
|
+
}
|
|
3329
|
+
lines.push("");
|
|
3330
|
+
}
|
|
3331
|
+
lines.push("Tips:");
|
|
3332
|
+
lines.push(" - Run codex-setup after adding accounts.");
|
|
3333
|
+
lines.push(" - Use codex-setup --wizard for menu-driven onboarding.");
|
|
3334
|
+
lines.push(" - Use codex-doctor when request failures increase.");
|
|
3335
|
+
return lines.join("\n");
|
|
3336
|
+
},
|
|
3337
|
+
}),
|
|
3338
|
+
"codex-setup": tool({
|
|
3339
|
+
description: "Beginner checklist for first-time setup and account readiness.",
|
|
3340
|
+
args: {
|
|
3341
|
+
wizard: tool.schema
|
|
3342
|
+
.boolean()
|
|
3343
|
+
.optional()
|
|
3344
|
+
.describe("Launch menu-driven setup wizard when terminal supports it."),
|
|
3345
|
+
},
|
|
3346
|
+
async execute({ wizard } = {}) {
|
|
3347
|
+
const ui = resolveUiRuntime();
|
|
3348
|
+
const state = await buildSetupChecklistState();
|
|
3349
|
+
if (wizard) {
|
|
3350
|
+
return runSetupWizard(ui, state);
|
|
3351
|
+
}
|
|
3352
|
+
return renderSetupChecklistOutput(ui, state);
|
|
3353
|
+
},
|
|
3354
|
+
}),
|
|
3355
|
+
"codex-doctor": tool({
|
|
3356
|
+
description: "Run beginner-friendly diagnostics with clear fixes.",
|
|
3357
|
+
args: {
|
|
3358
|
+
deep: tool.schema
|
|
3359
|
+
.boolean()
|
|
3360
|
+
.optional()
|
|
3361
|
+
.describe("Include technical snapshot details (default: false)."),
|
|
3362
|
+
fix: tool.schema
|
|
3363
|
+
.boolean()
|
|
3364
|
+
.optional()
|
|
3365
|
+
.describe("Apply safe automated fixes (refresh tokens and switch to healthiest eligible account)."),
|
|
3366
|
+
},
|
|
3367
|
+
async execute({ deep, fix } = {}) {
|
|
3368
|
+
const ui = resolveUiRuntime();
|
|
3369
|
+
const storage = await loadAccounts();
|
|
3370
|
+
const now = Date.now();
|
|
3371
|
+
const activeIndex = storage && storage.accounts.length > 0
|
|
3372
|
+
? resolveActiveIndex(storage, "codex")
|
|
3373
|
+
: 0;
|
|
3374
|
+
const snapshots = storage
|
|
3375
|
+
? toBeginnerAccountSnapshots(storage, activeIndex, now)
|
|
3376
|
+
: [];
|
|
3377
|
+
const runtime = getBeginnerRuntimeSnapshot();
|
|
3378
|
+
const summary = summarizeBeginnerAccounts(snapshots, now);
|
|
3379
|
+
const findings = buildBeginnerDoctorFindings({
|
|
3380
|
+
accounts: snapshots,
|
|
3381
|
+
now,
|
|
3382
|
+
runtime,
|
|
3383
|
+
});
|
|
3384
|
+
const nextAction = recommendBeginnerNextAction({ accounts: snapshots, now, runtime });
|
|
3385
|
+
const appliedFixes = [];
|
|
3386
|
+
const fixErrors = [];
|
|
3387
|
+
if (fix && storage && storage.accounts.length > 0) {
|
|
3388
|
+
let changedByRefresh = false;
|
|
3389
|
+
let refreshedCount = 0;
|
|
3390
|
+
for (const account of storage.accounts) {
|
|
3391
|
+
try {
|
|
3392
|
+
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
3393
|
+
if (refreshResult.type === "success") {
|
|
3394
|
+
account.refreshToken = refreshResult.refresh;
|
|
3395
|
+
account.accessToken = refreshResult.access;
|
|
3396
|
+
account.expiresAt = refreshResult.expires;
|
|
3397
|
+
changedByRefresh = true;
|
|
3398
|
+
refreshedCount += 1;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
catch (error) {
|
|
3402
|
+
fixErrors.push(error instanceof Error ? error.message : String(error));
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
if (changedByRefresh) {
|
|
3406
|
+
try {
|
|
3407
|
+
await saveAccounts(storage);
|
|
3408
|
+
appliedFixes.push(`Refreshed ${refreshedCount} account token(s).`);
|
|
3409
|
+
}
|
|
3410
|
+
catch (error) {
|
|
3411
|
+
fixErrors.push(`Failed to persist refresh updates: ${error instanceof Error ? error.message : String(error)}`);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
try {
|
|
3415
|
+
const managerForFix = await AccountManager.loadFromDisk();
|
|
3416
|
+
const explainability = managerForFix.getSelectionExplainability("codex", undefined, Date.now());
|
|
3417
|
+
const eligible = explainability
|
|
3418
|
+
.filter((entry) => entry.eligible)
|
|
3419
|
+
.sort((a, b) => {
|
|
3420
|
+
if (b.healthScore !== a.healthScore)
|
|
3421
|
+
return b.healthScore - a.healthScore;
|
|
3422
|
+
return b.tokensAvailable - a.tokensAvailable;
|
|
3423
|
+
});
|
|
3424
|
+
const best = eligible[0];
|
|
3425
|
+
if (best) {
|
|
3426
|
+
const currentActive = resolveActiveIndex(storage, "codex");
|
|
3427
|
+
if (best.index !== currentActive) {
|
|
3428
|
+
storage.activeIndex = best.index;
|
|
3429
|
+
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
3430
|
+
for (const family of MODEL_FAMILIES) {
|
|
3431
|
+
storage.activeIndexByFamily[family] = best.index;
|
|
3432
|
+
}
|
|
3433
|
+
await saveAccounts(storage);
|
|
3434
|
+
appliedFixes.push(`Switched active account to ${best.index + 1} (best eligible).`);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
else {
|
|
3438
|
+
appliedFixes.push("No eligible account available for auto-switch.");
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
catch (error) {
|
|
3442
|
+
fixErrors.push(`Auto-switch evaluation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3443
|
+
}
|
|
3444
|
+
if (cachedAccountManager) {
|
|
3445
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
3446
|
+
cachedAccountManager = reloadedManager;
|
|
3447
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
if (ui.v2Enabled) {
|
|
3451
|
+
const lines = [
|
|
3452
|
+
...formatUiHeader(ui, "Codex doctor"),
|
|
3453
|
+
formatUiKeyValue(ui, "Accounts", String(summary.total)),
|
|
3454
|
+
formatUiKeyValue(ui, "Healthy", String(summary.healthy), summary.healthy > 0 ? "success" : "warning"),
|
|
3455
|
+
formatUiKeyValue(ui, "Blocked", String(summary.blocked), summary.blocked > 0 ? "warning" : "muted"),
|
|
3456
|
+
formatUiKeyValue(ui, "Failure rate", runtime.totalRequests > 0 ? `${Math.round((runtime.failedRequests / runtime.totalRequests) * 100)}%` : "0%"),
|
|
3457
|
+
"",
|
|
3458
|
+
...formatUiSection(ui, "Findings"),
|
|
3459
|
+
];
|
|
3460
|
+
for (const finding of findings) {
|
|
3461
|
+
const tone = finding.severity === "ok"
|
|
3462
|
+
? "success"
|
|
3463
|
+
: finding.severity === "warning"
|
|
3464
|
+
? "warning"
|
|
3465
|
+
: "danger";
|
|
3466
|
+
lines.push(formatUiItem(ui, `${formatDoctorSeverity(ui, finding.severity)} ${finding.summary}`, tone));
|
|
3467
|
+
lines.push(` ${formatUiKeyValue(ui, "fix", finding.action, "muted")}`);
|
|
3468
|
+
}
|
|
3469
|
+
lines.push("");
|
|
3470
|
+
lines.push(...formatUiSection(ui, "Recommended next step"));
|
|
3471
|
+
lines.push(formatUiItem(ui, nextAction, "accent"));
|
|
3472
|
+
if (fix) {
|
|
3473
|
+
lines.push("");
|
|
3474
|
+
lines.push(...formatUiSection(ui, "Auto-fix"));
|
|
3475
|
+
if (appliedFixes.length === 0) {
|
|
3476
|
+
lines.push(formatUiItem(ui, "No safe fixes were applied.", "muted"));
|
|
3477
|
+
}
|
|
3478
|
+
else {
|
|
3479
|
+
for (const entry of appliedFixes) {
|
|
3480
|
+
lines.push(formatUiItem(ui, entry, "success"));
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
for (const error of fixErrors) {
|
|
3484
|
+
lines.push(formatUiItem(ui, error, "warning"));
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
if (deep) {
|
|
3488
|
+
lines.push("");
|
|
3489
|
+
lines.push(...formatUiSection(ui, "Technical snapshot"));
|
|
3490
|
+
lines.push(formatUiKeyValue(ui, "Storage", getStoragePath(), "muted"));
|
|
3491
|
+
lines.push(formatUiKeyValue(ui, "Runtime failures", `failed=${runtime.failedRequests}, rateLimited=${runtime.rateLimitedResponses}, authRefreshFailed=${runtime.authRefreshFailures}, server=${runtime.serverErrors}, network=${runtime.networkErrors}`, "muted"));
|
|
3492
|
+
}
|
|
3493
|
+
return lines.join("\n");
|
|
3494
|
+
}
|
|
3495
|
+
const lines = [
|
|
3496
|
+
"Codex Doctor:",
|
|
3497
|
+
`Accounts: ${summary.total} (healthy=${summary.healthy}, blocked=${summary.blocked})`,
|
|
3498
|
+
`Failure rate: ${runtime.totalRequests > 0 ? Math.round((runtime.failedRequests / runtime.totalRequests) * 100) : 0}%`,
|
|
3499
|
+
"",
|
|
3500
|
+
"Findings:",
|
|
3501
|
+
];
|
|
3502
|
+
for (const finding of findings) {
|
|
3503
|
+
lines.push(` ${formatDoctorSeverityText(finding.severity)} ${finding.summary}`);
|
|
3504
|
+
lines.push(` fix: ${finding.action}`);
|
|
3505
|
+
}
|
|
3506
|
+
lines.push("");
|
|
3507
|
+
lines.push(`Recommended next step: ${nextAction}`);
|
|
3508
|
+
if (fix) {
|
|
3509
|
+
lines.push("");
|
|
3510
|
+
lines.push("Auto-fix:");
|
|
3511
|
+
if (appliedFixes.length === 0) {
|
|
3512
|
+
lines.push(" - No safe fixes were applied.");
|
|
3513
|
+
}
|
|
3514
|
+
else {
|
|
3515
|
+
for (const entry of appliedFixes) {
|
|
3516
|
+
lines.push(` - ${entry}`);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
for (const error of fixErrors) {
|
|
3520
|
+
lines.push(` - warning: ${error}`);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
if (deep) {
|
|
3524
|
+
lines.push("");
|
|
3525
|
+
lines.push("Technical snapshot:");
|
|
3526
|
+
lines.push(` Storage: ${getStoragePath()}`);
|
|
3527
|
+
lines.push(` Runtime failures: failed=${runtime.failedRequests}, rateLimited=${runtime.rateLimitedResponses}, authRefreshFailed=${runtime.authRefreshFailures}, server=${runtime.serverErrors}, network=${runtime.networkErrors}`);
|
|
3528
|
+
}
|
|
3529
|
+
return lines.join("\n");
|
|
3530
|
+
},
|
|
3531
|
+
}),
|
|
3532
|
+
"codex-next": tool({
|
|
3533
|
+
description: "Show the single most recommended next action for beginners.",
|
|
3534
|
+
args: {},
|
|
3535
|
+
async execute() {
|
|
3536
|
+
const ui = resolveUiRuntime();
|
|
3537
|
+
const storage = await loadAccounts();
|
|
3538
|
+
const now = Date.now();
|
|
3539
|
+
const activeIndex = storage && storage.accounts.length > 0
|
|
3540
|
+
? resolveActiveIndex(storage, "codex")
|
|
3541
|
+
: 0;
|
|
3542
|
+
const snapshots = storage
|
|
3543
|
+
? toBeginnerAccountSnapshots(storage, activeIndex, now)
|
|
3544
|
+
: [];
|
|
3545
|
+
const action = recommendBeginnerNextAction({
|
|
3546
|
+
accounts: snapshots,
|
|
3547
|
+
now,
|
|
3548
|
+
runtime: getBeginnerRuntimeSnapshot(),
|
|
3549
|
+
});
|
|
3550
|
+
if (ui.v2Enabled) {
|
|
3551
|
+
return [
|
|
3552
|
+
...formatUiHeader(ui, "Recommended next action"),
|
|
3553
|
+
"",
|
|
3554
|
+
formatUiItem(ui, action, "accent"),
|
|
3555
|
+
].join("\n");
|
|
3556
|
+
}
|
|
3557
|
+
return `Recommended next action:\n${action}`;
|
|
3558
|
+
},
|
|
3559
|
+
}),
|
|
3560
|
+
"codex-label": tool({
|
|
3561
|
+
description: "Set or clear a beginner-friendly display label for an account (interactive picker when index is omitted).",
|
|
3562
|
+
args: {
|
|
3563
|
+
index: tool.schema.number().optional().describe("Account number to update (1-based, e.g., 1 for first account)"),
|
|
3564
|
+
label: tool.schema.string().describe("Display label. Use an empty string to clear (e.g., Work, Personal, Team A)"),
|
|
3565
|
+
},
|
|
3566
|
+
async execute({ index, label }) {
|
|
3567
|
+
const ui = resolveUiRuntime();
|
|
3568
|
+
const storage = await loadAccounts();
|
|
3569
|
+
if (!storage || storage.accounts.length === 0) {
|
|
3570
|
+
if (ui.v2Enabled) {
|
|
3571
|
+
return [
|
|
3572
|
+
...formatUiHeader(ui, "Set account label"),
|
|
3573
|
+
"",
|
|
3574
|
+
formatUiItem(ui, "No accounts configured.", "warning"),
|
|
3575
|
+
formatUiItem(ui, "Run: opencode auth login", "accent"),
|
|
3576
|
+
].join("\n");
|
|
3577
|
+
}
|
|
3578
|
+
return "No Codex accounts configured. Run: opencode auth login";
|
|
3579
|
+
}
|
|
3580
|
+
let resolvedIndex = index;
|
|
3581
|
+
if (resolvedIndex === undefined) {
|
|
3582
|
+
const selectedIndex = await promptAccountIndexSelection(ui, storage, "Set account label");
|
|
3583
|
+
if (selectedIndex === null) {
|
|
3584
|
+
if (supportsInteractiveMenus()) {
|
|
3585
|
+
if (ui.v2Enabled) {
|
|
3586
|
+
return [
|
|
3587
|
+
...formatUiHeader(ui, "Set account label"),
|
|
3588
|
+
"",
|
|
3589
|
+
formatUiItem(ui, "No account selected.", "warning"),
|
|
3590
|
+
formatUiItem(ui, "Run again and pick an account, or pass codex-label index=2 label=\"Work\".", "muted"),
|
|
3591
|
+
].join("\n");
|
|
3592
|
+
}
|
|
3593
|
+
return "No account selected.";
|
|
3594
|
+
}
|
|
3595
|
+
if (ui.v2Enabled) {
|
|
3596
|
+
return [
|
|
3597
|
+
...formatUiHeader(ui, "Set account label"),
|
|
3598
|
+
"",
|
|
3599
|
+
formatUiItem(ui, "Missing account number.", "warning"),
|
|
3600
|
+
formatUiItem(ui, "Use: codex-label index=2 label=\"Work\"", "accent"),
|
|
3601
|
+
].join("\n");
|
|
3602
|
+
}
|
|
3603
|
+
return "Missing account number. Use: codex-label index=2 label=\"Work\"";
|
|
3604
|
+
}
|
|
3605
|
+
resolvedIndex = selectedIndex + 1;
|
|
3606
|
+
}
|
|
3607
|
+
const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
|
|
3608
|
+
if (!Number.isFinite(targetIndex) ||
|
|
3609
|
+
targetIndex < 0 ||
|
|
3610
|
+
targetIndex >= storage.accounts.length) {
|
|
3611
|
+
if (ui.v2Enabled) {
|
|
3612
|
+
return [
|
|
3613
|
+
...formatUiHeader(ui, "Set account label"),
|
|
3614
|
+
"",
|
|
3615
|
+
formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
|
|
3616
|
+
formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
|
|
3617
|
+
].join("\n");
|
|
3618
|
+
}
|
|
3619
|
+
return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
|
|
3620
|
+
}
|
|
3621
|
+
const normalizedLabel = (label ?? "").trim().replace(/\s+/g, " ");
|
|
3622
|
+
if (normalizedLabel.length > 60) {
|
|
3623
|
+
if (ui.v2Enabled) {
|
|
3624
|
+
return [
|
|
3625
|
+
...formatUiHeader(ui, "Set account label"),
|
|
3626
|
+
"",
|
|
3627
|
+
formatUiItem(ui, "Label is too long (max 60 characters).", "danger"),
|
|
3628
|
+
].join("\n");
|
|
3629
|
+
}
|
|
3630
|
+
return "Label is too long (max 60 characters).";
|
|
3631
|
+
}
|
|
3632
|
+
const account = storage.accounts[targetIndex];
|
|
3633
|
+
if (!account) {
|
|
3634
|
+
return `Account ${resolvedIndex} not found.`;
|
|
3635
|
+
}
|
|
3636
|
+
const previousLabel = account.accountLabel?.trim() ?? "";
|
|
3637
|
+
if (normalizedLabel.length === 0) {
|
|
3638
|
+
delete account.accountLabel;
|
|
3639
|
+
}
|
|
3640
|
+
else {
|
|
3641
|
+
account.accountLabel = normalizedLabel;
|
|
3642
|
+
}
|
|
3643
|
+
try {
|
|
3644
|
+
await saveAccounts(storage);
|
|
3645
|
+
}
|
|
3646
|
+
catch (saveError) {
|
|
3647
|
+
logWarn("Failed to save account label update", { error: String(saveError) });
|
|
3648
|
+
if (ui.v2Enabled) {
|
|
3649
|
+
return [
|
|
3650
|
+
...formatUiHeader(ui, "Set account label"),
|
|
3651
|
+
"",
|
|
3652
|
+
formatUiItem(ui, "Label updated in memory but failed to persist.", "danger"),
|
|
3653
|
+
].join("\n");
|
|
3654
|
+
}
|
|
3655
|
+
return "Label updated in memory but failed to persist. Changes may be lost on restart.";
|
|
3656
|
+
}
|
|
3657
|
+
if (cachedAccountManager) {
|
|
3658
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
3659
|
+
cachedAccountManager = reloadedManager;
|
|
3660
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
3661
|
+
}
|
|
3662
|
+
const accountLabel = formatCommandAccountLabel(account, targetIndex);
|
|
3663
|
+
if (ui.v2Enabled) {
|
|
3664
|
+
const statusText = normalizedLabel.length === 0
|
|
3665
|
+
? `Cleared label for ${accountLabel}`
|
|
3666
|
+
: `Set label for ${accountLabel} to "${normalizedLabel}"`;
|
|
3667
|
+
const previousText = previousLabel.length > 0
|
|
3668
|
+
? formatUiKeyValue(ui, "Previous label", previousLabel, "muted")
|
|
3669
|
+
: formatUiKeyValue(ui, "Previous label", "none", "muted");
|
|
3670
|
+
return [
|
|
3671
|
+
...formatUiHeader(ui, "Set account label"),
|
|
3672
|
+
"",
|
|
3673
|
+
formatUiItem(ui, `${getStatusMarker(ui, "ok")} ${statusText}`, "success"),
|
|
3674
|
+
previousText,
|
|
3675
|
+
].join("\n");
|
|
3676
|
+
}
|
|
3677
|
+
if (normalizedLabel.length === 0) {
|
|
3678
|
+
return `Cleared label for ${accountLabel}`;
|
|
3679
|
+
}
|
|
3680
|
+
return `Set label for ${accountLabel} to "${normalizedLabel}"`;
|
|
3681
|
+
},
|
|
3682
|
+
}),
|
|
3683
|
+
"codex-tag": tool({
|
|
3684
|
+
description: "Set or clear account tags for filtering and grouping.",
|
|
3685
|
+
args: {
|
|
3686
|
+
index: tool.schema.number().optional().describe("Account number to update (1-based, e.g., 1 for first account)"),
|
|
3687
|
+
tags: tool.schema.string().describe("Comma-separated tags (e.g., work,team-a). Empty string clears tags."),
|
|
3688
|
+
},
|
|
3689
|
+
async execute({ index, tags }) {
|
|
3690
|
+
const ui = resolveUiRuntime();
|
|
3691
|
+
const storage = await loadAccounts();
|
|
3692
|
+
if (!storage || storage.accounts.length === 0) {
|
|
3693
|
+
if (ui.v2Enabled) {
|
|
3694
|
+
return [
|
|
3695
|
+
...formatUiHeader(ui, "Set account tags"),
|
|
3696
|
+
"",
|
|
3697
|
+
formatUiItem(ui, "No accounts configured.", "warning"),
|
|
3698
|
+
formatUiItem(ui, "Run: opencode auth login", "accent"),
|
|
3699
|
+
].join("\n");
|
|
3700
|
+
}
|
|
3701
|
+
return "No Codex accounts configured. Run: opencode auth login";
|
|
3702
|
+
}
|
|
3703
|
+
let resolvedIndex = index;
|
|
3704
|
+
if (resolvedIndex === undefined) {
|
|
3705
|
+
const selectedIndex = await promptAccountIndexSelection(ui, storage, "Set account tags");
|
|
3706
|
+
if (selectedIndex === null) {
|
|
3707
|
+
if (supportsInteractiveMenus()) {
|
|
3708
|
+
return ui.v2Enabled
|
|
3709
|
+
? [
|
|
3710
|
+
...formatUiHeader(ui, "Set account tags"),
|
|
3711
|
+
"",
|
|
3712
|
+
formatUiItem(ui, "No account selected.", "warning"),
|
|
3713
|
+
].join("\n")
|
|
3714
|
+
: "No account selected.";
|
|
3715
|
+
}
|
|
3716
|
+
return "Missing account number. Use: codex-tag index=2 tags=\"work,team-a\"";
|
|
3717
|
+
}
|
|
3718
|
+
resolvedIndex = selectedIndex + 1;
|
|
3719
|
+
}
|
|
3720
|
+
const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
|
|
3721
|
+
if (!Number.isFinite(targetIndex) ||
|
|
3722
|
+
targetIndex < 0 ||
|
|
3723
|
+
targetIndex >= storage.accounts.length) {
|
|
3724
|
+
return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
|
|
3725
|
+
}
|
|
3726
|
+
const account = storage.accounts[targetIndex];
|
|
3727
|
+
if (!account)
|
|
3728
|
+
return `Account ${resolvedIndex} not found.`;
|
|
3729
|
+
const normalizedTags = normalizeAccountTags(tags ?? "");
|
|
3730
|
+
const previousTags = Array.isArray(account.accountTags)
|
|
3731
|
+
? [...account.accountTags]
|
|
3732
|
+
: [];
|
|
3733
|
+
if (normalizedTags.length === 0) {
|
|
3734
|
+
delete account.accountTags;
|
|
3735
|
+
}
|
|
3736
|
+
else {
|
|
3737
|
+
account.accountTags = normalizedTags;
|
|
3738
|
+
}
|
|
3739
|
+
try {
|
|
3740
|
+
await saveAccounts(storage);
|
|
3741
|
+
}
|
|
3742
|
+
catch (error) {
|
|
3743
|
+
logWarn("Failed to save account tag update", { error: String(error) });
|
|
3744
|
+
return "Tag update failed to persist. Changes may be lost on restart.";
|
|
3745
|
+
}
|
|
3746
|
+
if (cachedAccountManager) {
|
|
3747
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
3748
|
+
cachedAccountManager = reloadedManager;
|
|
3749
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
3750
|
+
}
|
|
3751
|
+
const accountLabel = formatCommandAccountLabel(account, targetIndex);
|
|
3752
|
+
const previousText = previousTags.length > 0 ? previousTags.join(", ") : "none";
|
|
3753
|
+
const nextText = normalizedTags.length > 0 ? normalizedTags.join(", ") : "none";
|
|
3754
|
+
if (ui.v2Enabled) {
|
|
3755
|
+
return [
|
|
3756
|
+
...formatUiHeader(ui, "Set account tags"),
|
|
3757
|
+
"",
|
|
3758
|
+
formatUiItem(ui, `${getStatusMarker(ui, "ok")} Updated tags for ${accountLabel}`, "success"),
|
|
3759
|
+
formatUiKeyValue(ui, "Previous tags", previousText, "muted"),
|
|
3760
|
+
formatUiKeyValue(ui, "Current tags", nextText, normalizedTags.length > 0 ? "accent" : "muted"),
|
|
3761
|
+
].join("\n");
|
|
3762
|
+
}
|
|
3763
|
+
return `Updated tags for ${accountLabel}\nPrevious tags: ${previousText}\nCurrent tags: ${nextText}`;
|
|
3764
|
+
},
|
|
3765
|
+
}),
|
|
3766
|
+
"codex-note": tool({
|
|
3767
|
+
description: "Set or clear an account note for reminders.",
|
|
3768
|
+
args: {
|
|
3769
|
+
index: tool.schema.number().optional().describe("Account number to update (1-based, e.g., 1 for first account)"),
|
|
3770
|
+
note: tool.schema.string().describe("Short note. Empty string clears the note."),
|
|
3771
|
+
},
|
|
3772
|
+
async execute({ index, note }) {
|
|
3773
|
+
const ui = resolveUiRuntime();
|
|
3774
|
+
const storage = await loadAccounts();
|
|
3775
|
+
if (!storage || storage.accounts.length === 0) {
|
|
3776
|
+
return "No Codex accounts configured. Run: opencode auth login";
|
|
3777
|
+
}
|
|
3778
|
+
let resolvedIndex = index;
|
|
3779
|
+
if (resolvedIndex === undefined) {
|
|
3780
|
+
const selectedIndex = await promptAccountIndexSelection(ui, storage, "Set account note");
|
|
3781
|
+
if (selectedIndex === null) {
|
|
3782
|
+
if (supportsInteractiveMenus())
|
|
3783
|
+
return "No account selected.";
|
|
3784
|
+
return "Missing account number. Use: codex-note index=2 note=\"weekday primary\"";
|
|
3785
|
+
}
|
|
3786
|
+
resolvedIndex = selectedIndex + 1;
|
|
3787
|
+
}
|
|
3788
|
+
const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
|
|
3789
|
+
if (!Number.isFinite(targetIndex) ||
|
|
3790
|
+
targetIndex < 0 ||
|
|
3791
|
+
targetIndex >= storage.accounts.length) {
|
|
3792
|
+
return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
|
|
3793
|
+
}
|
|
3794
|
+
const account = storage.accounts[targetIndex];
|
|
3795
|
+
if (!account)
|
|
3796
|
+
return `Account ${resolvedIndex} not found.`;
|
|
3797
|
+
const normalizedNote = (note ?? "").trim();
|
|
3798
|
+
if (normalizedNote.length > 240) {
|
|
3799
|
+
return "Note is too long (max 240 characters).";
|
|
3800
|
+
}
|
|
3801
|
+
if (normalizedNote.length === 0) {
|
|
3802
|
+
delete account.accountNote;
|
|
3803
|
+
}
|
|
3804
|
+
else {
|
|
3805
|
+
account.accountNote = normalizedNote;
|
|
3806
|
+
}
|
|
3807
|
+
try {
|
|
3808
|
+
await saveAccounts(storage);
|
|
3809
|
+
}
|
|
3810
|
+
catch (error) {
|
|
3811
|
+
logWarn("Failed to save account note update", { error: String(error) });
|
|
3812
|
+
return "Note update failed to persist. Changes may be lost on restart.";
|
|
3813
|
+
}
|
|
3814
|
+
if (cachedAccountManager) {
|
|
3815
|
+
const reloadedManager = await AccountManager.loadFromDisk();
|
|
3816
|
+
cachedAccountManager = reloadedManager;
|
|
3817
|
+
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
3818
|
+
}
|
|
3819
|
+
const accountLabel = formatCommandAccountLabel(account, targetIndex);
|
|
3820
|
+
if (normalizedNote.length === 0) {
|
|
3821
|
+
return `Cleared note for ${accountLabel}`;
|
|
3822
|
+
}
|
|
3823
|
+
return `Saved note for ${accountLabel}: ${normalizedNote}`;
|
|
3824
|
+
},
|
|
3825
|
+
}),
|
|
3826
|
+
"codex-dashboard": tool({
|
|
3827
|
+
description: "Show a live Codex dashboard: account eligibility, retry budgets, and refresh queue health.",
|
|
3828
|
+
args: {},
|
|
3829
|
+
async execute() {
|
|
3830
|
+
const ui = resolveUiRuntime();
|
|
3831
|
+
const storage = await loadAccounts();
|
|
3832
|
+
if (!storage || storage.accounts.length === 0) {
|
|
3833
|
+
if (ui.v2Enabled) {
|
|
3834
|
+
return [
|
|
3835
|
+
...formatUiHeader(ui, "Codex dashboard"),
|
|
3836
|
+
"",
|
|
3837
|
+
formatUiItem(ui, "No accounts configured.", "warning"),
|
|
3838
|
+
formatUiItem(ui, "Run: opencode auth login", "accent"),
|
|
3839
|
+
].join("\n");
|
|
3840
|
+
}
|
|
3841
|
+
return "No Codex accounts configured. Run: opencode auth login";
|
|
3842
|
+
}
|
|
3843
|
+
const now = Date.now();
|
|
3844
|
+
const refreshMetrics = getRefreshQueueMetrics();
|
|
3845
|
+
const family = runtimeMetrics.lastSelectionSnapshot?.family ?? "codex";
|
|
3846
|
+
const model = runtimeMetrics.lastSelectionSnapshot?.model ?? undefined;
|
|
3847
|
+
const manager = cachedAccountManager ?? (await AccountManager.loadFromDisk());
|
|
3848
|
+
const explainability = manager.getSelectionExplainability(family, model, now);
|
|
3849
|
+
const selectionLabel = model ? `${family}:${model}` : family;
|
|
3850
|
+
if (ui.v2Enabled) {
|
|
3851
|
+
const lines = [
|
|
3852
|
+
...formatUiHeader(ui, "Codex dashboard"),
|
|
3853
|
+
formatUiKeyValue(ui, "Accounts", String(storage.accounts.length)),
|
|
3854
|
+
formatUiKeyValue(ui, "Selection lens", selectionLabel, "muted"),
|
|
3855
|
+
formatUiKeyValue(ui, "Retry profile", runtimeMetrics.retryProfile, "muted"),
|
|
3856
|
+
formatUiKeyValue(ui, "Beginner safe mode", beginnerSafeModeEnabled ? "on" : "off", beginnerSafeModeEnabled ? "accent" : "muted"),
|
|
3857
|
+
formatUiKeyValue(ui, "Retry usage", `A${runtimeMetrics.retryBudgetUsage.authRefresh} N${runtimeMetrics.retryBudgetUsage.network} S${runtimeMetrics.retryBudgetUsage.server} RS${runtimeMetrics.retryBudgetUsage.rateLimitShort} RG${runtimeMetrics.retryBudgetUsage.rateLimitGlobal} E${runtimeMetrics.retryBudgetUsage.emptyResponse}`, "muted"),
|
|
3858
|
+
formatUiKeyValue(ui, "Refresh queue", `pending=${refreshMetrics.pending}, success=${refreshMetrics.succeeded}, failed=${refreshMetrics.failed}`, "muted"),
|
|
3859
|
+
"",
|
|
3860
|
+
...formatUiSection(ui, "Account eligibility"),
|
|
3861
|
+
];
|
|
3862
|
+
for (const entry of explainability) {
|
|
3863
|
+
const label = formatCommandAccountLabel(storage.accounts[entry.index], entry.index);
|
|
3864
|
+
const state = entry.eligible ? formatUiBadge(ui, "eligible", "success") : formatUiBadge(ui, "blocked", "warning");
|
|
3865
|
+
lines.push(formatUiItem(ui, `${label} ${state} health=${Math.round(entry.healthScore)} tokens=${entry.tokensAvailable.toFixed(1)} reasons=${entry.reasons.join(", ")}`));
|
|
3866
|
+
}
|
|
3867
|
+
lines.push("");
|
|
3868
|
+
lines.push(...formatUiSection(ui, "Recommended next step"));
|
|
3869
|
+
lines.push(formatUiItem(ui, recommendBeginnerNextAction({
|
|
3870
|
+
accounts: toBeginnerAccountSnapshots(storage, resolveActiveIndex(storage, "codex"), now),
|
|
3871
|
+
now,
|
|
3872
|
+
runtime: getBeginnerRuntimeSnapshot(),
|
|
3873
|
+
}), "accent"));
|
|
3874
|
+
if (runtimeMetrics.lastError) {
|
|
3875
|
+
lines.push("");
|
|
3876
|
+
lines.push(...formatUiSection(ui, "Last error"));
|
|
3877
|
+
lines.push(formatUiItem(ui, runtimeMetrics.lastError, "danger"));
|
|
3878
|
+
if (runtimeMetrics.lastErrorCategory) {
|
|
3879
|
+
lines.push(formatUiKeyValue(ui, "Category", runtimeMetrics.lastErrorCategory, "warning"));
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
return lines.join("\n");
|
|
3883
|
+
}
|
|
3884
|
+
const lines = [
|
|
3885
|
+
"Codex Dashboard:",
|
|
3886
|
+
`Accounts: ${storage.accounts.length}`,
|
|
3887
|
+
`Selection lens: ${selectionLabel}`,
|
|
3888
|
+
`Retry profile: ${runtimeMetrics.retryProfile}`,
|
|
3889
|
+
`Beginner safe mode: ${beginnerSafeModeEnabled ? "on" : "off"}`,
|
|
3890
|
+
`Retry usage: auth=${runtimeMetrics.retryBudgetUsage.authRefresh}, network=${runtimeMetrics.retryBudgetUsage.network}, server=${runtimeMetrics.retryBudgetUsage.server}, short429=${runtimeMetrics.retryBudgetUsage.rateLimitShort}, global429=${runtimeMetrics.retryBudgetUsage.rateLimitGlobal}, empty=${runtimeMetrics.retryBudgetUsage.emptyResponse}`,
|
|
3891
|
+
`Refresh queue: pending=${refreshMetrics.pending}, success=${refreshMetrics.succeeded}, failed=${refreshMetrics.failed}`,
|
|
3892
|
+
"",
|
|
3893
|
+
"Account eligibility:",
|
|
3894
|
+
];
|
|
3895
|
+
for (const entry of explainability) {
|
|
3896
|
+
const label = formatCommandAccountLabel(storage.accounts[entry.index], entry.index);
|
|
3897
|
+
lines.push(` - ${label}: ${entry.eligible ? "eligible" : "blocked"} | health=${Math.round(entry.healthScore)} | tokens=${entry.tokensAvailable.toFixed(1)} | reasons=${entry.reasons.join(", ")}`);
|
|
3898
|
+
}
|
|
3899
|
+
lines.push("");
|
|
3900
|
+
lines.push(`Recommended next step: ${recommendBeginnerNextAction({
|
|
3901
|
+
accounts: toBeginnerAccountSnapshots(storage, resolveActiveIndex(storage, "codex"), now),
|
|
3902
|
+
now,
|
|
3903
|
+
runtime: getBeginnerRuntimeSnapshot(),
|
|
3904
|
+
})}`);
|
|
3905
|
+
if (runtimeMetrics.lastError) {
|
|
3906
|
+
lines.push("");
|
|
3907
|
+
lines.push(`Last error: ${runtimeMetrics.lastError}`);
|
|
3908
|
+
if (runtimeMetrics.lastErrorCategory) {
|
|
3909
|
+
lines.push(`Category: ${runtimeMetrics.lastErrorCategory}`);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
return lines.join("\n");
|
|
3913
|
+
},
|
|
3914
|
+
}),
|
|
2636
3915
|
"codex-health": tool({
|
|
2637
3916
|
description: "Check health of all Codex accounts by validating refresh tokens.",
|
|
2638
3917
|
args: {},
|
|
@@ -2690,11 +3969,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2690
3969
|
},
|
|
2691
3970
|
}),
|
|
2692
3971
|
"codex-remove": tool({
|
|
2693
|
-
description: "Remove one Codex account entry by index (1-based)
|
|
3972
|
+
description: "Remove one Codex account entry by index (1-based) or interactive picker when index is omitted.",
|
|
2694
3973
|
args: {
|
|
2695
|
-
index: tool.schema.number().describe("Account number to remove (1-based, e.g., 1 for first account)"),
|
|
3974
|
+
index: tool.schema.number().optional().describe("Account number to remove (1-based, e.g., 1 for first account)"),
|
|
2696
3975
|
},
|
|
2697
|
-
async execute({ index }) {
|
|
3976
|
+
async execute({ index } = {}) {
|
|
2698
3977
|
const ui = resolveUiRuntime();
|
|
2699
3978
|
const storage = await loadAccounts();
|
|
2700
3979
|
if (!storage || storage.accounts.length === 0) {
|
|
@@ -2707,7 +3986,34 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2707
3986
|
}
|
|
2708
3987
|
return "No Codex accounts configured. Nothing to remove.";
|
|
2709
3988
|
}
|
|
2710
|
-
|
|
3989
|
+
let resolvedIndex = index;
|
|
3990
|
+
if (resolvedIndex === undefined) {
|
|
3991
|
+
const selectedIndex = await promptAccountIndexSelection(ui, storage, "Remove account");
|
|
3992
|
+
if (selectedIndex === null) {
|
|
3993
|
+
if (supportsInteractiveMenus()) {
|
|
3994
|
+
if (ui.v2Enabled) {
|
|
3995
|
+
return [
|
|
3996
|
+
...formatUiHeader(ui, "Remove account"),
|
|
3997
|
+
"",
|
|
3998
|
+
formatUiItem(ui, "No account selected.", "warning"),
|
|
3999
|
+
formatUiItem(ui, "Run again and pick an account, or pass codex-remove index=2.", "muted"),
|
|
4000
|
+
].join("\n");
|
|
4001
|
+
}
|
|
4002
|
+
return "No account selected.";
|
|
4003
|
+
}
|
|
4004
|
+
if (ui.v2Enabled) {
|
|
4005
|
+
return [
|
|
4006
|
+
...formatUiHeader(ui, "Remove account"),
|
|
4007
|
+
"",
|
|
4008
|
+
formatUiItem(ui, "Missing account number.", "warning"),
|
|
4009
|
+
formatUiItem(ui, "Use: codex-remove index=2", "accent"),
|
|
4010
|
+
].join("\n");
|
|
4011
|
+
}
|
|
4012
|
+
return "Missing account number. Use: codex-remove index=2";
|
|
4013
|
+
}
|
|
4014
|
+
resolvedIndex = selectedIndex + 1;
|
|
4015
|
+
}
|
|
4016
|
+
const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
|
|
2711
4017
|
if (!Number.isFinite(targetIndex) ||
|
|
2712
4018
|
targetIndex < 0 ||
|
|
2713
4019
|
targetIndex >= storage.accounts.length) {
|
|
@@ -2715,16 +4021,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2715
4021
|
return [
|
|
2716
4022
|
...formatUiHeader(ui, "Remove account"),
|
|
2717
4023
|
"",
|
|
2718
|
-
formatUiItem(ui, `Invalid account number: ${
|
|
4024
|
+
formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
|
|
2719
4025
|
formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
|
|
2720
4026
|
formatUiItem(ui, "Use codex-list to list all accounts.", "accent"),
|
|
2721
4027
|
].join("\n");
|
|
2722
4028
|
}
|
|
2723
|
-
return `Invalid account number: ${
|
|
4029
|
+
return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}\n\nUse codex-list to list all accounts.`;
|
|
2724
4030
|
}
|
|
2725
4031
|
const account = storage.accounts[targetIndex];
|
|
2726
4032
|
if (!account) {
|
|
2727
|
-
return `Account ${
|
|
4033
|
+
return `Account ${resolvedIndex} not found.`;
|
|
2728
4034
|
}
|
|
2729
4035
|
const label = formatCommandAccountLabel(account, targetIndex);
|
|
2730
4036
|
storage.accounts.splice(targetIndex, 1);
|
|
@@ -2871,15 +4177,22 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2871
4177
|
},
|
|
2872
4178
|
}),
|
|
2873
4179
|
"codex-export": tool({
|
|
2874
|
-
description: "Export accounts to a JSON file for backup or migration
|
|
4180
|
+
description: "Export accounts to a JSON file for backup or migration. Can auto-generate timestamped backup paths.",
|
|
2875
4181
|
args: {
|
|
2876
|
-
path: tool.schema.string().describe("File path to export to (e.g., ~/codex-backup.json)"),
|
|
4182
|
+
path: tool.schema.string().optional().describe("File path to export to (e.g., ~/codex-backup.json). If omitted, a timestamped backup path is used."),
|
|
2877
4183
|
force: tool.schema.boolean().optional().describe("Overwrite existing file (default: true)"),
|
|
4184
|
+
timestamped: tool.schema.boolean().optional().describe("When true (default), omitted paths use a timestamped backup filename."),
|
|
2878
4185
|
},
|
|
2879
|
-
async execute({ path: filePath, force }) {
|
|
4186
|
+
async execute({ path: filePath, force, timestamped, }) {
|
|
2880
4187
|
const ui = resolveUiRuntime();
|
|
4188
|
+
const shouldTimestamp = timestamped ?? true;
|
|
4189
|
+
const resolvedExportPath = filePath && filePath.trim().length > 0
|
|
4190
|
+
? filePath
|
|
4191
|
+
: shouldTimestamp
|
|
4192
|
+
? createTimestampedBackupPath()
|
|
4193
|
+
: "codex-backup.json";
|
|
2881
4194
|
try {
|
|
2882
|
-
await exportAccounts(
|
|
4195
|
+
await exportAccounts(resolvedExportPath, force ?? true);
|
|
2883
4196
|
const storage = await loadAccounts();
|
|
2884
4197
|
const count = storage?.accounts.length ?? 0;
|
|
2885
4198
|
if (ui.v2Enabled) {
|
|
@@ -2887,10 +4200,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2887
4200
|
...formatUiHeader(ui, "Export accounts"),
|
|
2888
4201
|
"",
|
|
2889
4202
|
formatUiItem(ui, `${getStatusMarker(ui, "ok")} Exported ${count} account(s)`, "success"),
|
|
2890
|
-
formatUiKeyValue(ui, "Path",
|
|
4203
|
+
formatUiKeyValue(ui, "Path", resolvedExportPath, "muted"),
|
|
2891
4204
|
].join("\n");
|
|
2892
4205
|
}
|
|
2893
|
-
return `Exported ${count} account(s) to: ${
|
|
4206
|
+
return `Exported ${count} account(s) to: ${resolvedExportPath}`;
|
|
2894
4207
|
}
|
|
2895
4208
|
catch (error) {
|
|
2896
4209
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -2907,16 +4220,49 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2907
4220
|
},
|
|
2908
4221
|
}),
|
|
2909
4222
|
"codex-import": tool({
|
|
2910
|
-
description: "Import accounts from a JSON file,
|
|
4223
|
+
description: "Import accounts from a JSON file, with dry-run preview and automatic timestamped backup before apply.",
|
|
2911
4224
|
args: {
|
|
2912
4225
|
path: tool.schema.string().describe("File path to import from (e.g., ~/codex-backup.json)"),
|
|
4226
|
+
dryRun: tool.schema.boolean().optional().describe("Preview import impact without applying changes."),
|
|
2913
4227
|
},
|
|
2914
|
-
async execute({ path: filePath }) {
|
|
4228
|
+
async execute({ path: filePath, dryRun }) {
|
|
2915
4229
|
const ui = resolveUiRuntime();
|
|
2916
4230
|
try {
|
|
2917
|
-
const
|
|
4231
|
+
const preview = await previewImportAccounts(filePath);
|
|
4232
|
+
if (dryRun) {
|
|
4233
|
+
if (ui.v2Enabled) {
|
|
4234
|
+
return [
|
|
4235
|
+
...formatUiHeader(ui, "Import preview"),
|
|
4236
|
+
"",
|
|
4237
|
+
formatUiItem(ui, "No changes applied (dry run).", "warning"),
|
|
4238
|
+
formatUiKeyValue(ui, "Path", filePath, "muted"),
|
|
4239
|
+
formatUiKeyValue(ui, "New accounts", String(preview.imported), preview.imported > 0 ? "success" : "muted"),
|
|
4240
|
+
formatUiKeyValue(ui, "Duplicates skipped", String(preview.skipped), preview.skipped > 0 ? "warning" : "muted"),
|
|
4241
|
+
formatUiKeyValue(ui, "Resulting total", String(preview.total), "accent"),
|
|
4242
|
+
].join("\n");
|
|
4243
|
+
}
|
|
4244
|
+
return [
|
|
4245
|
+
"Import preview (dry run):",
|
|
4246
|
+
`Path: ${filePath}`,
|
|
4247
|
+
`New accounts: ${preview.imported}`,
|
|
4248
|
+
`Duplicates skipped: ${preview.skipped}`,
|
|
4249
|
+
`Resulting total: ${preview.total}`,
|
|
4250
|
+
].join("\n");
|
|
4251
|
+
}
|
|
4252
|
+
const result = await importAccounts(filePath, {
|
|
4253
|
+
preImportBackupPrefix: "codex-pre-import-backup",
|
|
4254
|
+
backupMode: "required",
|
|
4255
|
+
});
|
|
4256
|
+
const backupSummary = result.backupStatus === "created"
|
|
4257
|
+
? result.backupPath ?? "created"
|
|
4258
|
+
: result.backupStatus === "failed"
|
|
4259
|
+
? `failed (${result.backupError ?? "unknown error"})`
|
|
4260
|
+
: "skipped (no existing accounts)";
|
|
4261
|
+
const backupStatus = result.backupStatus === "created" ? "ok" : "warning";
|
|
2918
4262
|
invalidateAccountManagerCache();
|
|
2919
4263
|
const lines = [`Import complete.`, ``];
|
|
4264
|
+
lines.push(`Preview: +${preview.imported} new, ${preview.skipped} skipped, ${preview.total} total`);
|
|
4265
|
+
lines.push(`Auto-backup: ${backupSummary}`);
|
|
2920
4266
|
if (result.imported > 0) {
|
|
2921
4267
|
lines.push(`New accounts: ${result.imported}`);
|
|
2922
4268
|
}
|
|
@@ -2930,6 +4276,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2930
4276
|
"",
|
|
2931
4277
|
formatUiItem(ui, `${getStatusMarker(ui, "ok")} Import complete`, "success"),
|
|
2932
4278
|
formatUiKeyValue(ui, "Path", filePath, "muted"),
|
|
4279
|
+
formatUiKeyValue(ui, "Auto-backup", backupSummary, backupStatus === "ok" ? "muted" : "warning"),
|
|
4280
|
+
formatUiKeyValue(ui, "Preview", `+${preview.imported}, skipped=${preview.skipped}, total=${preview.total}`, "muted"),
|
|
2933
4281
|
formatUiKeyValue(ui, "New accounts", String(result.imported), result.imported > 0 ? "success" : "muted"),
|
|
2934
4282
|
formatUiKeyValue(ui, "Duplicates skipped", String(result.skipped), result.skipped > 0 ? "warning" : "muted"),
|
|
2935
4283
|
formatUiKeyValue(ui, "Total accounts", String(result.total), "accent"),
|