oc-chatgpt-multi-auth 5.3.2 → 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 +1623 -50
- 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 +354 -71
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +23 -5
- package/dist/lib/ui/auth-menu.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,
|
|
@@ -226,7 +248,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
226
248
|
await withAccountStorageTransaction(async (loadedStorage, persist) => {
|
|
227
249
|
const now = Date.now();
|
|
228
250
|
const stored = replaceAll ? null : loadedStorage;
|
|
229
|
-
|
|
251
|
+
let accounts = stored?.accounts ? [...stored.accounts] : [];
|
|
230
252
|
const pushIndex = (map, key, index) => {
|
|
231
253
|
const existing = map.get(key);
|
|
232
254
|
if (existing) {
|
|
@@ -241,6 +263,93 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
241
263
|
const [onlyIndex] = indices;
|
|
242
264
|
return typeof onlyIndex === "number" ? onlyIndex : undefined;
|
|
243
265
|
};
|
|
266
|
+
const pickNewestAccountIndex = (existingIndex, candidateIndex) => {
|
|
267
|
+
const existing = accounts[existingIndex];
|
|
268
|
+
const candidate = accounts[candidateIndex];
|
|
269
|
+
if (!existing)
|
|
270
|
+
return candidateIndex;
|
|
271
|
+
if (!candidate)
|
|
272
|
+
return existingIndex;
|
|
273
|
+
const existingLastUsed = existing.lastUsed ?? 0;
|
|
274
|
+
const candidateLastUsed = candidate.lastUsed ?? 0;
|
|
275
|
+
if (candidateLastUsed > existingLastUsed)
|
|
276
|
+
return candidateIndex;
|
|
277
|
+
if (candidateLastUsed < existingLastUsed)
|
|
278
|
+
return existingIndex;
|
|
279
|
+
const existingAddedAt = existing.addedAt ?? 0;
|
|
280
|
+
const candidateAddedAt = candidate.addedAt ?? 0;
|
|
281
|
+
return candidateAddedAt >= existingAddedAt ? candidateIndex : existingIndex;
|
|
282
|
+
};
|
|
283
|
+
const mergeAccountRecords = (targetIndex, sourceIndex) => {
|
|
284
|
+
const target = accounts[targetIndex];
|
|
285
|
+
const source = accounts[sourceIndex];
|
|
286
|
+
if (!target || !source)
|
|
287
|
+
return;
|
|
288
|
+
const targetLastUsed = target.lastUsed ?? 0;
|
|
289
|
+
const sourceLastUsed = source.lastUsed ?? 0;
|
|
290
|
+
const targetAddedAt = target.addedAt ?? 0;
|
|
291
|
+
const sourceAddedAt = source.addedAt ?? 0;
|
|
292
|
+
const sourceIsNewer = sourceLastUsed > targetLastUsed ||
|
|
293
|
+
(sourceLastUsed === targetLastUsed && sourceAddedAt > targetAddedAt);
|
|
294
|
+
const newer = sourceIsNewer ? source : target;
|
|
295
|
+
const older = sourceIsNewer ? target : source;
|
|
296
|
+
const mergedRateLimitResetTimes = {};
|
|
297
|
+
const rateLimitResetKeys = new Set([
|
|
298
|
+
...Object.keys(older.rateLimitResetTimes ?? {}),
|
|
299
|
+
...Object.keys(newer.rateLimitResetTimes ?? {}),
|
|
300
|
+
]);
|
|
301
|
+
for (const key of rateLimitResetKeys) {
|
|
302
|
+
const olderRaw = older.rateLimitResetTimes?.[key];
|
|
303
|
+
const newerRaw = newer.rateLimitResetTimes?.[key];
|
|
304
|
+
const olderValue = typeof olderRaw === "number" && Number.isFinite(olderRaw) ? olderRaw : 0;
|
|
305
|
+
const newerValue = typeof newerRaw === "number" && Number.isFinite(newerRaw) ? newerRaw : 0;
|
|
306
|
+
const resolved = Math.max(olderValue, newerValue);
|
|
307
|
+
if (resolved > 0) {
|
|
308
|
+
mergedRateLimitResetTimes[key] = resolved;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const mergedEnabled = target.enabled === false || source.enabled === false
|
|
312
|
+
? false
|
|
313
|
+
: target.enabled ?? source.enabled;
|
|
314
|
+
const targetCoolingDownUntil = typeof target.coolingDownUntil === "number" && Number.isFinite(target.coolingDownUntil)
|
|
315
|
+
? target.coolingDownUntil
|
|
316
|
+
: 0;
|
|
317
|
+
const sourceCoolingDownUntil = typeof source.coolingDownUntil === "number" && Number.isFinite(source.coolingDownUntil)
|
|
318
|
+
? source.coolingDownUntil
|
|
319
|
+
: 0;
|
|
320
|
+
const mergedCoolingDownUntilValue = Math.max(targetCoolingDownUntil, sourceCoolingDownUntil);
|
|
321
|
+
const mergedCoolingDownUntil = mergedCoolingDownUntilValue > 0 ? mergedCoolingDownUntilValue : undefined;
|
|
322
|
+
const mergedCooldownReason = (() => {
|
|
323
|
+
if (mergedCoolingDownUntilValue <= 0) {
|
|
324
|
+
return target.cooldownReason ?? source.cooldownReason;
|
|
325
|
+
}
|
|
326
|
+
if (sourceCoolingDownUntil > targetCoolingDownUntil) {
|
|
327
|
+
return source.cooldownReason ?? target.cooldownReason;
|
|
328
|
+
}
|
|
329
|
+
if (targetCoolingDownUntil > sourceCoolingDownUntil) {
|
|
330
|
+
return target.cooldownReason ?? source.cooldownReason;
|
|
331
|
+
}
|
|
332
|
+
return source.cooldownReason ?? target.cooldownReason;
|
|
333
|
+
})();
|
|
334
|
+
accounts[targetIndex] = {
|
|
335
|
+
...target,
|
|
336
|
+
accountId: target.accountId ?? source.accountId,
|
|
337
|
+
organizationId: target.organizationId ?? source.organizationId,
|
|
338
|
+
accountIdSource: target.accountIdSource ?? source.accountIdSource,
|
|
339
|
+
accountLabel: target.accountLabel ?? source.accountLabel,
|
|
340
|
+
email: target.email ?? source.email,
|
|
341
|
+
refreshToken: newer.refreshToken || older.refreshToken,
|
|
342
|
+
accessToken: newer.accessToken || older.accessToken,
|
|
343
|
+
expiresAt: newer.expiresAt ?? older.expiresAt,
|
|
344
|
+
enabled: mergedEnabled,
|
|
345
|
+
addedAt: Math.max(target.addedAt ?? 0, source.addedAt ?? 0),
|
|
346
|
+
lastUsed: Math.max(target.lastUsed ?? 0, source.lastUsed ?? 0),
|
|
347
|
+
lastSwitchReason: target.lastSwitchReason ?? source.lastSwitchReason,
|
|
348
|
+
rateLimitResetTimes: mergedRateLimitResetTimes,
|
|
349
|
+
coolingDownUntil: mergedCoolingDownUntil,
|
|
350
|
+
cooldownReason: mergedCooldownReason,
|
|
351
|
+
};
|
|
352
|
+
};
|
|
244
353
|
const resolveUniqueOrgScopedMatch = (indexes, accountId, refreshToken) => {
|
|
245
354
|
const byAccountId = accountId
|
|
246
355
|
? asUniqueIndex(indexes.byAccountIdOrgScoped.get(accountId))
|
|
@@ -260,6 +369,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
260
369
|
const byEmailNoOrg = new Map();
|
|
261
370
|
const byAccountIdOrgScoped = new Map();
|
|
262
371
|
const byRefreshTokenOrgScoped = new Map();
|
|
372
|
+
const byRefreshTokenGlobal = new Map();
|
|
263
373
|
for (let i = 0; i < accounts.length; i += 1) {
|
|
264
374
|
const account = accounts[i];
|
|
265
375
|
if (!account)
|
|
@@ -268,6 +378,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
268
378
|
const accountId = account.accountId?.trim();
|
|
269
379
|
const refreshToken = account.refreshToken?.trim();
|
|
270
380
|
const email = account.email?.trim();
|
|
381
|
+
// Global refresh token index: first entry wins (keeps earliest/primary).
|
|
382
|
+
if (refreshToken && !byRefreshTokenGlobal.has(refreshToken)) {
|
|
383
|
+
byRefreshTokenGlobal.set(refreshToken, i);
|
|
384
|
+
}
|
|
271
385
|
if (organizationId) {
|
|
272
386
|
byOrganizationId.set(organizationId, i);
|
|
273
387
|
if (accountId) {
|
|
@@ -295,6 +409,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
295
409
|
byEmailNoOrg,
|
|
296
410
|
byAccountIdOrgScoped,
|
|
297
411
|
byRefreshTokenOrgScoped,
|
|
412
|
+
byRefreshTokenGlobal,
|
|
298
413
|
};
|
|
299
414
|
};
|
|
300
415
|
let identityIndexes = buildIdentityIndexes();
|
|
@@ -328,7 +443,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
328
443
|
return byEmail;
|
|
329
444
|
}
|
|
330
445
|
}
|
|
331
|
-
|
|
446
|
+
const orgScoped = resolveUniqueOrgScopedMatch(identityIndexes, normalizedAccountId, result.refresh);
|
|
447
|
+
if (orgScoped !== undefined)
|
|
448
|
+
return orgScoped;
|
|
449
|
+
// Absolute last resort: same refresh token = same human account.
|
|
450
|
+
// Catches org-scoped entries invisible to no-org lookups.
|
|
451
|
+
return identityIndexes.byRefreshTokenGlobal.get(result.refresh);
|
|
332
452
|
})();
|
|
333
453
|
if (existingIndex === undefined) {
|
|
334
454
|
accounts.push({
|
|
@@ -351,11 +471,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
351
471
|
continue;
|
|
352
472
|
const nextEmail = accountEmail ?? existing.email;
|
|
353
473
|
const nextOrganizationId = organizationId ?? existing.organizationId;
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
474
|
+
const preserveOrgIdentity = typeof existing.organizationId === "string" &&
|
|
475
|
+
existing.organizationId.trim().length > 0 &&
|
|
476
|
+
!organizationId;
|
|
477
|
+
const nextAccountId = preserveOrgIdentity
|
|
478
|
+
? existing.accountId ?? normalizedAccountId
|
|
479
|
+
: normalizedAccountId ?? existing.accountId;
|
|
480
|
+
const nextAccountIdSource = preserveOrgIdentity
|
|
481
|
+
? existing.accountIdSource ?? accountIdSource
|
|
482
|
+
: normalizedAccountId
|
|
483
|
+
? accountIdSource ?? existing.accountIdSource
|
|
484
|
+
: existing.accountIdSource;
|
|
485
|
+
const nextAccountLabel = preserveOrgIdentity
|
|
486
|
+
? existing.accountLabel ?? accountLabel
|
|
487
|
+
: accountLabel ?? existing.accountLabel;
|
|
359
488
|
accounts[existingIndex] = {
|
|
360
489
|
...existing,
|
|
361
490
|
accountId: nextAccountId,
|
|
@@ -370,22 +499,140 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
370
499
|
};
|
|
371
500
|
identityIndexes = buildIdentityIndexes();
|
|
372
501
|
}
|
|
502
|
+
const pruneRefreshTokenCollisions = () => {
|
|
503
|
+
const indicesToRemove = new Set();
|
|
504
|
+
const refreshMap = new Map();
|
|
505
|
+
for (let i = 0; i < accounts.length; i += 1) {
|
|
506
|
+
const account = accounts[i];
|
|
507
|
+
if (!account)
|
|
508
|
+
continue;
|
|
509
|
+
const refreshToken = account.refreshToken?.trim();
|
|
510
|
+
if (!refreshToken)
|
|
511
|
+
continue;
|
|
512
|
+
const orgKey = account.organizationId?.trim() ?? "";
|
|
513
|
+
let entry = refreshMap.get(refreshToken);
|
|
514
|
+
if (!entry) {
|
|
515
|
+
entry = { byOrg: new Map(), fallbackIndex: undefined };
|
|
516
|
+
refreshMap.set(refreshToken, entry);
|
|
517
|
+
}
|
|
518
|
+
if (orgKey) {
|
|
519
|
+
const existingIndex = entry.byOrg.get(orgKey);
|
|
520
|
+
if (existingIndex !== undefined) {
|
|
521
|
+
const newestIndex = pickNewestAccountIndex(existingIndex, i);
|
|
522
|
+
const obsoleteIndex = newestIndex === existingIndex ? i : existingIndex;
|
|
523
|
+
mergeAccountRecords(newestIndex, obsoleteIndex);
|
|
524
|
+
indicesToRemove.add(obsoleteIndex);
|
|
525
|
+
entry.byOrg.set(orgKey, newestIndex);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
entry.byOrg.set(orgKey, i);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const existingFallback = entry.fallbackIndex;
|
|
532
|
+
if (typeof existingFallback === "number") {
|
|
533
|
+
const newestIndex = pickNewestAccountIndex(existingFallback, i);
|
|
534
|
+
const obsoleteIndex = newestIndex === existingFallback ? i : existingFallback;
|
|
535
|
+
mergeAccountRecords(newestIndex, obsoleteIndex);
|
|
536
|
+
indicesToRemove.add(obsoleteIndex);
|
|
537
|
+
entry.fallbackIndex = newestIndex;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
entry.fallbackIndex = i;
|
|
541
|
+
}
|
|
542
|
+
for (const entry of refreshMap.values()) {
|
|
543
|
+
const fallbackIndex = entry.fallbackIndex;
|
|
544
|
+
if (typeof fallbackIndex !== "number")
|
|
545
|
+
continue;
|
|
546
|
+
const orgIndices = Array.from(entry.byOrg.values());
|
|
547
|
+
if (orgIndices.length === 0)
|
|
548
|
+
continue;
|
|
549
|
+
const [firstOrgIndex, ...otherOrgIndices] = orgIndices;
|
|
550
|
+
if (typeof firstOrgIndex !== "number")
|
|
551
|
+
continue;
|
|
552
|
+
let preferredOrgIndex = firstOrgIndex;
|
|
553
|
+
for (const orgIndex of otherOrgIndices) {
|
|
554
|
+
preferredOrgIndex = pickNewestAccountIndex(preferredOrgIndex, orgIndex);
|
|
555
|
+
}
|
|
556
|
+
mergeAccountRecords(preferredOrgIndex, fallbackIndex);
|
|
557
|
+
indicesToRemove.add(fallbackIndex);
|
|
558
|
+
}
|
|
559
|
+
if (indicesToRemove.size > 0) {
|
|
560
|
+
accounts = accounts.filter((_, index) => !indicesToRemove.has(index));
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
const collectIdentityKeys = (account) => {
|
|
564
|
+
const keys = [];
|
|
565
|
+
const organizationId = account?.organizationId?.trim();
|
|
566
|
+
if (organizationId)
|
|
567
|
+
keys.push(`org:${organizationId}`);
|
|
568
|
+
const accountId = account?.accountId?.trim();
|
|
569
|
+
if (accountId)
|
|
570
|
+
keys.push(`account:${accountId}`);
|
|
571
|
+
const refreshToken = account?.refreshToken?.trim();
|
|
572
|
+
if (refreshToken)
|
|
573
|
+
keys.push(`refresh:${refreshToken}`);
|
|
574
|
+
return keys;
|
|
575
|
+
};
|
|
576
|
+
const getStoredAccountAtIndex = (rawIndex) => {
|
|
577
|
+
const storedAccounts = stored?.accounts;
|
|
578
|
+
if (!storedAccounts)
|
|
579
|
+
return undefined;
|
|
580
|
+
if (typeof rawIndex !== "number" || !Number.isFinite(rawIndex))
|
|
581
|
+
return undefined;
|
|
582
|
+
const candidate = Math.floor(rawIndex);
|
|
583
|
+
if (candidate < 0 || candidate >= storedAccounts.length)
|
|
584
|
+
return undefined;
|
|
585
|
+
return storedAccounts[candidate];
|
|
586
|
+
};
|
|
587
|
+
const storedActiveKeys = replaceAll
|
|
588
|
+
? []
|
|
589
|
+
: collectIdentityKeys(getStoredAccountAtIndex(stored?.activeIndex));
|
|
590
|
+
const storedActiveKeysByFamily = {};
|
|
591
|
+
if (!replaceAll) {
|
|
592
|
+
for (const family of MODEL_FAMILIES) {
|
|
593
|
+
const familyKeys = collectIdentityKeys(getStoredAccountAtIndex(stored?.activeIndexByFamily?.[family]));
|
|
594
|
+
if (familyKeys.length > 0) {
|
|
595
|
+
storedActiveKeysByFamily[family] = familyKeys;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
pruneRefreshTokenCollisions();
|
|
373
600
|
if (accounts.length === 0)
|
|
374
601
|
return;
|
|
375
|
-
const
|
|
602
|
+
const resolveIndexByIdentityKeys = (identityKeys) => {
|
|
603
|
+
if (!identityKeys || identityKeys.length === 0)
|
|
604
|
+
return undefined;
|
|
605
|
+
for (const identityKey of identityKeys) {
|
|
606
|
+
const index = accounts.findIndex((account) => collectIdentityKeys(account).includes(identityKey));
|
|
607
|
+
if (index >= 0) {
|
|
608
|
+
return index;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return undefined;
|
|
612
|
+
};
|
|
613
|
+
const fallbackActiveIndex = replaceAll
|
|
376
614
|
? 0
|
|
377
615
|
: typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
|
|
378
616
|
? stored.activeIndex
|
|
379
617
|
: 0;
|
|
380
|
-
const
|
|
618
|
+
const remappedActiveIndex = replaceAll
|
|
619
|
+
? undefined
|
|
620
|
+
: resolveIndexByIdentityKeys(storedActiveKeys);
|
|
621
|
+
const activeIndex = remappedActiveIndex ?? fallbackActiveIndex;
|
|
622
|
+
const clampedActiveIndex = Math.max(0, Math.min(Math.floor(activeIndex), accounts.length - 1));
|
|
381
623
|
const activeIndexByFamily = {};
|
|
382
624
|
for (const family of MODEL_FAMILIES) {
|
|
383
625
|
const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
|
|
626
|
+
const remappedFamilyIndex = replaceAll
|
|
627
|
+
? undefined
|
|
628
|
+
: resolveIndexByIdentityKeys(storedActiveKeysByFamily[family]);
|
|
384
629
|
const rawFamilyIndex = replaceAll
|
|
385
630
|
? 0
|
|
386
|
-
: typeof
|
|
387
|
-
?
|
|
388
|
-
:
|
|
631
|
+
: typeof remappedFamilyIndex === "number"
|
|
632
|
+
? remappedFamilyIndex
|
|
633
|
+
: typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
|
|
634
|
+
? storedFamilyIndex
|
|
635
|
+
: clampedActiveIndex;
|
|
389
636
|
activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
|
|
390
637
|
}
|
|
391
638
|
await persist({
|
|
@@ -542,6 +789,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
542
789
|
const email = account?.email?.trim();
|
|
543
790
|
const workspace = account?.accountLabel?.trim();
|
|
544
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
|
+
: [];
|
|
545
798
|
const details = [];
|
|
546
799
|
if (email)
|
|
547
800
|
details.push(email);
|
|
@@ -549,11 +802,295 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
549
802
|
details.push(`workspace:${workspace}`);
|
|
550
803
|
if (accountId)
|
|
551
804
|
details.push(`id:${accountId}`);
|
|
805
|
+
if (tags.length > 0)
|
|
806
|
+
details.push(`tags:${tags.join(",")}`);
|
|
552
807
|
if (details.length === 0) {
|
|
553
808
|
return `Account ${index + 1}`;
|
|
554
809
|
}
|
|
555
810
|
return `Account ${index + 1} (${details.join(", ")})`;
|
|
556
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
|
+
};
|
|
557
1094
|
const invalidateAccountManagerCache = () => {
|
|
558
1095
|
cachedAccountManager = null;
|
|
559
1096
|
accountManagerPromise = null;
|
|
@@ -680,11 +1217,26 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
680
1217
|
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
681
1218
|
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
682
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 };
|
|
683
1231
|
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
684
1232
|
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
685
|
-
const retryAllAccountsRateLimited =
|
|
1233
|
+
const retryAllAccountsRateLimited = beginnerSafeMode
|
|
1234
|
+
? false
|
|
1235
|
+
: getRetryAllAccountsRateLimited(pluginConfig);
|
|
686
1236
|
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
687
|
-
const retryAllAccountsMaxRetries =
|
|
1237
|
+
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1238
|
+
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1239
|
+
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
688
1240
|
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
689
1241
|
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
690
1242
|
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
@@ -709,6 +1261,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
709
1261
|
fastSessionMaxInputItems,
|
|
710
1262
|
});
|
|
711
1263
|
}
|
|
1264
|
+
if (beginnerSafeMode) {
|
|
1265
|
+
logInfo("Beginner safe mode enabled", {
|
|
1266
|
+
retryProfile,
|
|
1267
|
+
retryAllAccountsRateLimited,
|
|
1268
|
+
retryAllAccountsMaxRetries,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
712
1271
|
const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
|
|
713
1272
|
process.env.VITEST !== "true" &&
|
|
714
1273
|
process.env.NODE_ENV !== "test";
|
|
@@ -728,6 +1287,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
728
1287
|
}).catch((err) => {
|
|
729
1288
|
logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
730
1289
|
});
|
|
1290
|
+
await runStartupPreflight();
|
|
731
1291
|
// Return SDK configuration
|
|
732
1292
|
return {
|
|
733
1293
|
apiKey: DUMMY_API_KEY,
|
|
@@ -829,6 +1389,25 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
829
1389
|
.trim() || undefined;
|
|
830
1390
|
const requestCorrelationId = setCorrelationId(threadIdCandidate ? `${threadIdCandidate}:${Date.now()}` : undefined);
|
|
831
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
|
+
};
|
|
832
1411
|
const abortSignal = requestInit?.signal ?? init?.signal ?? null;
|
|
833
1412
|
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
834
1413
|
if (abortSignal?.aborted) {
|
|
@@ -879,11 +1458,28 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
879
1458
|
const attempted = new Set();
|
|
880
1459
|
let restartAccountTraversalWithFallback = false;
|
|
881
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
|
+
};
|
|
882
1470
|
const account = accountManager.getCurrentOrNextForFamilyHybrid(modelFamily, model, { pidOffsetEnabled });
|
|
883
1471
|
if (!account || attempted.has(account.index)) {
|
|
884
1472
|
break;
|
|
885
1473
|
}
|
|
886
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
|
+
}
|
|
887
1483
|
// Log account selection for debugging rotation
|
|
888
1484
|
logDebug(`Using account ${account.index + 1}/${accountCount}: ${account.email ?? "unknown"} for ${modelFamily}`);
|
|
889
1485
|
let accountAuth = accountManager.toAuthDetails(account);
|
|
@@ -897,10 +1493,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
897
1493
|
}
|
|
898
1494
|
catch (err) {
|
|
899
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
|
+
}
|
|
900
1508
|
runtimeMetrics.authRefreshFailures++;
|
|
901
1509
|
runtimeMetrics.failedRequests++;
|
|
902
1510
|
runtimeMetrics.accountRotations++;
|
|
903
1511
|
runtimeMetrics.lastError = err?.message ?? String(err);
|
|
1512
|
+
runtimeMetrics.lastErrorCategory = "auth-refresh";
|
|
904
1513
|
const failures = accountManager.incrementAuthFailures(account);
|
|
905
1514
|
const accountLabel = formatAccountLabel(account, account.index);
|
|
906
1515
|
if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
|
|
@@ -944,6 +1553,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
944
1553
|
runtimeMetrics.accountRotations++;
|
|
945
1554
|
runtimeMetrics.lastError =
|
|
946
1555
|
`Local token bucket depleted for account ${account.index + 1} (${modelFamily}${model ? `:${model}` : ""})`;
|
|
1556
|
+
runtimeMetrics.lastErrorCategory = "rate-limit-local";
|
|
947
1557
|
logWarn(`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`);
|
|
948
1558
|
break;
|
|
949
1559
|
}
|
|
@@ -975,10 +1585,24 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
975
1585
|
catch (networkError) {
|
|
976
1586
|
const errorMsg = networkError instanceof Error ? networkError.message : String(networkError);
|
|
977
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
|
+
}
|
|
978
1601
|
runtimeMetrics.failedRequests++;
|
|
979
1602
|
runtimeMetrics.networkErrors++;
|
|
980
1603
|
runtimeMetrics.accountRotations++;
|
|
981
1604
|
runtimeMetrics.lastError = errorMsg;
|
|
1605
|
+
runtimeMetrics.lastErrorCategory = "network";
|
|
982
1606
|
accountManager.refundToken(account, modelFamily, model);
|
|
983
1607
|
accountManager.recordFailure(account, modelFamily, model);
|
|
984
1608
|
break;
|
|
@@ -1016,6 +1640,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1016
1640
|
accountManager.recordFailure(account, modelFamily, model);
|
|
1017
1641
|
account.lastSwitchReason = "rotation";
|
|
1018
1642
|
runtimeMetrics.lastError = `Unsupported model on account ${account.index + 1}: ${blockedModel}`;
|
|
1643
|
+
runtimeMetrics.lastErrorCategory = "unsupported-model";
|
|
1019
1644
|
logWarn(`Model ${blockedModel} is unsupported for account ${account.index + 1}. Trying next account/workspace before fallback.`, {
|
|
1020
1645
|
unsupportedCodexPolicy,
|
|
1021
1646
|
requestedModel: blockedModel,
|
|
@@ -1063,6 +1688,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1063
1688
|
body: JSON.stringify(transformedBody),
|
|
1064
1689
|
};
|
|
1065
1690
|
runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
|
|
1691
|
+
runtimeMetrics.lastErrorCategory = "model-fallback";
|
|
1066
1692
|
logWarn(`Model ${previousModel} is unsupported for this ChatGPT account. Falling back to ${model}.`, {
|
|
1067
1693
|
unsupportedCodexPolicy,
|
|
1068
1694
|
requestedModel: previousModel,
|
|
@@ -1077,6 +1703,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1077
1703
|
if (unsupportedModelInfo.isUnsupported && !fallbackOnUnsupportedCodexModel) {
|
|
1078
1704
|
const blockedModel = unsupportedModelInfo.unsupportedModel ?? model ?? "requested model";
|
|
1079
1705
|
runtimeMetrics.lastError = `Unsupported model (strict): ${blockedModel}`;
|
|
1706
|
+
runtimeMetrics.lastErrorCategory = "unsupported-model";
|
|
1080
1707
|
logWarn(`Model ${blockedModel} is unsupported for this ChatGPT account. Strict policy blocks automatic fallback.`, {
|
|
1081
1708
|
unsupportedCodexPolicy,
|
|
1082
1709
|
requestedModel: blockedModel,
|
|
@@ -1099,15 +1726,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1099
1726
|
runtimeMetrics.serverErrors++;
|
|
1100
1727
|
runtimeMetrics.accountRotations++;
|
|
1101
1728
|
runtimeMetrics.lastError = `HTTP ${response.status}`;
|
|
1729
|
+
runtimeMetrics.lastErrorCategory = "server";
|
|
1102
1730
|
accountManager.refundToken(account, modelFamily, model);
|
|
1103
1731
|
accountManager.recordFailure(account, modelFamily, model);
|
|
1732
|
+
if (!consumeRetryBudget("server", `Server error ${response.status} on account ${account.index + 1}`)) {
|
|
1733
|
+
return errorResponse;
|
|
1734
|
+
}
|
|
1104
1735
|
break;
|
|
1105
1736
|
}
|
|
1106
1737
|
if (rateLimit) {
|
|
1107
1738
|
runtimeMetrics.rateLimitedResponses++;
|
|
1108
1739
|
const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, rateLimit.retryAfterMs);
|
|
1109
1740
|
const waitLabel = formatWaitTime(delayMs);
|
|
1110
|
-
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`)) {
|
|
1111
1743
|
if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
1112
1744
|
await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });
|
|
1113
1745
|
accountManager.markToastShown(account.index);
|
|
@@ -1119,6 +1751,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1119
1751
|
accountManager.recordRateLimit(account, modelFamily, model);
|
|
1120
1752
|
account.lastSwitchReason = "rate-limit";
|
|
1121
1753
|
runtimeMetrics.accountRotations++;
|
|
1754
|
+
runtimeMetrics.lastErrorCategory = "rate-limit";
|
|
1122
1755
|
accountManager.saveToDiskDebounced();
|
|
1123
1756
|
logWarn(`Rate limited. Rotating account ${account.index + 1} (${account.email ?? "unknown"}).`);
|
|
1124
1757
|
if (accountManager.getAccountCount() > 1 &&
|
|
@@ -1130,6 +1763,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1130
1763
|
}
|
|
1131
1764
|
runtimeMetrics.failedRequests++;
|
|
1132
1765
|
runtimeMetrics.lastError = `HTTP ${response.status}`;
|
|
1766
|
+
runtimeMetrics.lastErrorCategory = "http";
|
|
1133
1767
|
return errorResponse;
|
|
1134
1768
|
}
|
|
1135
1769
|
resetRateLimitBackoff(account.index, quotaKey);
|
|
@@ -1140,6 +1774,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1140
1774
|
if (!successResponse.ok) {
|
|
1141
1775
|
runtimeMetrics.failedRequests++;
|
|
1142
1776
|
runtimeMetrics.lastError = `HTTP ${successResponse.status}`;
|
|
1777
|
+
runtimeMetrics.lastErrorCategory = "http";
|
|
1143
1778
|
return successResponse;
|
|
1144
1779
|
}
|
|
1145
1780
|
if (!isStreaming && emptyResponseMaxRetries > 0) {
|
|
@@ -1148,7 +1783,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1148
1783
|
const bodyText = await clonedResponse.text();
|
|
1149
1784
|
const parsedBody = bodyText ? JSON.parse(bodyText) : null;
|
|
1150
1785
|
if (isEmptyResponse(parsedBody)) {
|
|
1151
|
-
if (emptyResponseRetries < emptyResponseMaxRetries
|
|
1786
|
+
if (emptyResponseRetries < emptyResponseMaxRetries &&
|
|
1787
|
+
consumeRetryBudget("emptyResponse", `Empty response retry ${emptyResponseRetries + 1}/${emptyResponseMaxRetries}`)) {
|
|
1152
1788
|
emptyResponseRetries++;
|
|
1153
1789
|
runtimeMetrics.emptyResponseRetries++;
|
|
1154
1790
|
logWarn(`Empty response received (attempt ${emptyResponseRetries}/${emptyResponseMaxRetries}). Retrying...`);
|
|
@@ -1168,6 +1804,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1168
1804
|
accountManager.recordSuccess(account, modelFamily, model);
|
|
1169
1805
|
runtimeMetrics.successfulRequests++;
|
|
1170
1806
|
runtimeMetrics.lastError = null;
|
|
1807
|
+
runtimeMetrics.lastErrorCategory = null;
|
|
1171
1808
|
return successResponse;
|
|
1172
1809
|
}
|
|
1173
1810
|
if (restartAccountTraversalWithFallback) {
|
|
@@ -1184,7 +1821,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1184
1821
|
waitMs > 0 &&
|
|
1185
1822
|
(retryAllAccountsMaxWaitMs === 0 ||
|
|
1186
1823
|
waitMs <= retryAllAccountsMaxWaitMs) &&
|
|
1187
|
-
allRateLimitedRetries < retryAllAccountsMaxRetries
|
|
1824
|
+
allRateLimitedRetries < retryAllAccountsMaxRetries &&
|
|
1825
|
+
consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms`)) {
|
|
1188
1826
|
const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
|
|
1189
1827
|
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
|
|
1190
1828
|
allRateLimitedRetries++;
|
|
@@ -1198,6 +1836,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1198
1836
|
: `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
|
|
1199
1837
|
runtimeMetrics.failedRequests++;
|
|
1200
1838
|
runtimeMetrics.lastError = message;
|
|
1839
|
+
runtimeMetrics.lastErrorCategory = waitMs > 0 ? "rate-limit" : "account-failure";
|
|
1201
1840
|
return new Response(JSON.stringify({ error: { message } }), {
|
|
1202
1841
|
status: waitMs > 0 ? 429 : 503,
|
|
1203
1842
|
headers: {
|
|
@@ -1414,7 +2053,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1414
2053
|
const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
|
|
1415
2054
|
model,
|
|
1416
2055
|
});
|
|
1417
|
-
headers.set("content-type", "application/json
|
|
2056
|
+
headers.set("content-type", "application/json");
|
|
1418
2057
|
const controller = new AbortController();
|
|
1419
2058
|
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
1420
2059
|
let response;
|
|
@@ -2057,11 +2696,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2057
2696
|
tool: {
|
|
2058
2697
|
"codex-list": tool({
|
|
2059
2698
|
description: "List all Codex OAuth accounts and the current active index.",
|
|
2060
|
-
args: {
|
|
2061
|
-
|
|
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 } = {}) {
|
|
2062
2706
|
const ui = resolveUiRuntime();
|
|
2063
2707
|
const storage = await loadAccounts();
|
|
2064
2708
|
const storePath = getStoragePath();
|
|
2709
|
+
const normalizedTag = tag?.trim().toLowerCase() ?? "";
|
|
2065
2710
|
if (!storage || storage.accounts.length === 0) {
|
|
2066
2711
|
if (ui.v2Enabled) {
|
|
2067
2712
|
return [
|
|
@@ -2069,6 +2714,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2069
2714
|
"",
|
|
2070
2715
|
formatUiItem(ui, "No accounts configured.", "warning"),
|
|
2071
2716
|
formatUiItem(ui, "Run: opencode auth login", "accent"),
|
|
2717
|
+
formatUiItem(ui, "Setup checklist: codex-setup"),
|
|
2718
|
+
formatUiItem(ui, "Command guide: codex-help"),
|
|
2072
2719
|
formatUiKeyValue(ui, "Storage", storePath, "muted"),
|
|
2073
2720
|
].join("\n");
|
|
2074
2721
|
}
|
|
@@ -2077,21 +2724,47 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2077
2724
|
"",
|
|
2078
2725
|
"Add accounts:",
|
|
2079
2726
|
" opencode auth login",
|
|
2727
|
+
" codex-setup",
|
|
2728
|
+
" codex-help",
|
|
2080
2729
|
"",
|
|
2081
2730
|
`Storage: ${storePath}`,
|
|
2082
2731
|
].join("\n");
|
|
2083
2732
|
}
|
|
2084
2733
|
const now = Date.now();
|
|
2085
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
|
+
}
|
|
2086
2756
|
if (ui.v2Enabled) {
|
|
2087
2757
|
const lines = [
|
|
2088
2758
|
...formatUiHeader(ui, "Codex accounts"),
|
|
2089
|
-
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"),
|
|
2090
2763
|
formatUiKeyValue(ui, "Storage", storePath, "muted"),
|
|
2091
2764
|
"",
|
|
2092
2765
|
...formatUiSection(ui, "Accounts"),
|
|
2093
2766
|
];
|
|
2094
|
-
|
|
2767
|
+
filteredEntries.forEach(({ account, index }) => {
|
|
2095
2768
|
const label = formatCommandAccountLabel(account, index);
|
|
2096
2769
|
const badges = [];
|
|
2097
2770
|
if (index === activeIndex)
|
|
@@ -2116,9 +2789,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2116
2789
|
lines.push("");
|
|
2117
2790
|
lines.push(...formatUiSection(ui, "Commands"));
|
|
2118
2791
|
lines.push(formatUiItem(ui, "Add account: opencode auth login", "accent"));
|
|
2119
|
-
lines.push(formatUiItem(ui, "Switch account: codex-switch
|
|
2792
|
+
lines.push(formatUiItem(ui, "Switch account: codex-switch index=2"));
|
|
2120
2793
|
lines.push(formatUiItem(ui, "Detailed status: codex-status"));
|
|
2794
|
+
lines.push(formatUiItem(ui, "Live dashboard: codex-dashboard"));
|
|
2121
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"));
|
|
2122
2804
|
return lines.join("\n");
|
|
2123
2805
|
}
|
|
2124
2806
|
const listTableOptions = {
|
|
@@ -2129,11 +2811,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2129
2811
|
],
|
|
2130
2812
|
};
|
|
2131
2813
|
const lines = [
|
|
2132
|
-
`Codex Accounts (${
|
|
2814
|
+
`Codex Accounts (${filteredEntries.length}):`,
|
|
2133
2815
|
"",
|
|
2134
2816
|
...buildTableHeader(listTableOptions),
|
|
2135
2817
|
];
|
|
2136
|
-
|
|
2818
|
+
filteredEntries.forEach(({ account, index }) => {
|
|
2137
2819
|
const label = formatCommandAccountLabel(account, index);
|
|
2138
2820
|
const statuses = [];
|
|
2139
2821
|
const rateLimit = formatRateLimitEntry(account, now);
|
|
@@ -2151,21 +2833,33 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2151
2833
|
});
|
|
2152
2834
|
lines.push("");
|
|
2153
2835
|
lines.push(`Storage: ${storePath}`);
|
|
2836
|
+
if (normalizedTag) {
|
|
2837
|
+
lines.push(`Filter tag: ${normalizedTag}`);
|
|
2838
|
+
}
|
|
2154
2839
|
lines.push("");
|
|
2155
2840
|
lines.push("Commands:");
|
|
2156
2841
|
lines.push(" - Add account: opencode auth login");
|
|
2157
2842
|
lines.push(" - Switch account: codex-switch");
|
|
2158
2843
|
lines.push(" - Status details: codex-status");
|
|
2844
|
+
lines.push(" - Live dashboard: codex-dashboard");
|
|
2159
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");
|
|
2160
2854
|
return lines.join("\n");
|
|
2161
2855
|
},
|
|
2162
2856
|
}),
|
|
2163
2857
|
"codex-switch": tool({
|
|
2164
|
-
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.",
|
|
2165
2859
|
args: {
|
|
2166
|
-
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)"),
|
|
2167
2861
|
},
|
|
2168
|
-
async execute({ index }) {
|
|
2862
|
+
async execute({ index } = {}) {
|
|
2169
2863
|
const ui = resolveUiRuntime();
|
|
2170
2864
|
const storage = await loadAccounts();
|
|
2171
2865
|
if (!storage || storage.accounts.length === 0) {
|
|
@@ -2179,7 +2873,34 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2179
2873
|
}
|
|
2180
2874
|
return "No Codex accounts configured. Run: opencode auth login";
|
|
2181
2875
|
}
|
|
2182
|
-
|
|
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);
|
|
2183
2904
|
if (!Number.isFinite(targetIndex) ||
|
|
2184
2905
|
targetIndex < 0 ||
|
|
2185
2906
|
targetIndex >= storage.accounts.length) {
|
|
@@ -2187,11 +2908,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2187
2908
|
return [
|
|
2188
2909
|
...formatUiHeader(ui, "Switch account"),
|
|
2189
2910
|
"",
|
|
2190
|
-
formatUiItem(ui, `Invalid account number: ${
|
|
2911
|
+
formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
|
|
2191
2912
|
formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
|
|
2192
2913
|
].join("\n");
|
|
2193
2914
|
}
|
|
2194
|
-
return `Invalid account number: ${
|
|
2915
|
+
return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
|
|
2195
2916
|
}
|
|
2196
2917
|
const now = Date.now();
|
|
2197
2918
|
const account = storage.accounts[targetIndex];
|
|
@@ -2255,10 +2976,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2255
2976
|
}
|
|
2256
2977
|
const now = Date.now();
|
|
2257
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]));
|
|
2258
2984
|
if (ui.v2Enabled) {
|
|
2259
2985
|
const lines = [
|
|
2260
2986
|
...formatUiHeader(ui, "Account status"),
|
|
2261
2987
|
formatUiKeyValue(ui, "Total", String(storage.accounts.length)),
|
|
2988
|
+
formatUiKeyValue(ui, "Selection view", explainabilityModel
|
|
2989
|
+
? `${explainabilityFamily}:${explainabilityModel}`
|
|
2990
|
+
: explainabilityFamily, "muted"),
|
|
2262
2991
|
"",
|
|
2263
2992
|
...formatUiSection(ui, "Accounts"),
|
|
2264
2993
|
];
|
|
@@ -2299,6 +3028,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2299
3028
|
});
|
|
2300
3029
|
lines.push(formatUiItem(ui, `Account ${index + 1}: ${statuses.join(" | ")}`));
|
|
2301
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"));
|
|
2302
3046
|
return lines.join("\n");
|
|
2303
3047
|
}
|
|
2304
3048
|
const statusTableOptions = {
|
|
@@ -2344,6 +3088,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2344
3088
|
});
|
|
2345
3089
|
lines.push(` Account ${index + 1}: ${statuses.join(" | ")}`);
|
|
2346
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
|
+
})}`);
|
|
2347
3106
|
return lines.join("\n");
|
|
2348
3107
|
},
|
|
2349
3108
|
}),
|
|
@@ -2356,6 +3115,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2356
3115
|
const uptimeMs = Math.max(0, now - runtimeMetrics.startedAt);
|
|
2357
3116
|
const total = runtimeMetrics.totalRequests;
|
|
2358
3117
|
const successful = runtimeMetrics.successfulRequests;
|
|
3118
|
+
const refreshMetrics = getRefreshQueueMetrics();
|
|
2359
3119
|
const successRate = total > 0 ? ((successful / total) * 100).toFixed(1) : "0.0";
|
|
2360
3120
|
const avgLatencyMs = successful > 0
|
|
2361
3121
|
? Math.round(runtimeMetrics.cumulativeLatencyMs / successful)
|
|
@@ -2378,11 +3138,41 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2378
3138
|
`Auth refresh failures: ${runtimeMetrics.authRefreshFailures}`,
|
|
2379
3139
|
`Account rotations: ${runtimeMetrics.accountRotations}`,
|
|
2380
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}`,
|
|
2381
3156
|
`Last upstream request: ${lastRequest}`,
|
|
2382
3157
|
];
|
|
2383
3158
|
if (runtimeMetrics.lastError) {
|
|
2384
3159
|
lines.push(`Last error: ${runtimeMetrics.lastError}`);
|
|
2385
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
|
+
}
|
|
2386
3176
|
if (ui.v2Enabled) {
|
|
2387
3177
|
const styled = [
|
|
2388
3178
|
...formatUiHeader(ui, "Codex plugin metrics"),
|
|
@@ -2398,16 +3188,730 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2398
3188
|
formatUiKeyValue(ui, "Auth refresh failures", String(runtimeMetrics.authRefreshFailures), "warning"),
|
|
2399
3189
|
formatUiKeyValue(ui, "Account rotations", String(runtimeMetrics.accountRotations), "accent"),
|
|
2400
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"),
|
|
2401
3197
|
formatUiKeyValue(ui, "Last upstream request", lastRequest, "muted"),
|
|
2402
3198
|
];
|
|
2403
3199
|
if (runtimeMetrics.lastError) {
|
|
2404
3200
|
styled.push(formatUiKeyValue(ui, "Last error", runtimeMetrics.lastError, "danger"));
|
|
2405
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
|
+
}
|
|
2406
3216
|
return Promise.resolve(styled.join("\n"));
|
|
2407
3217
|
}
|
|
2408
3218
|
return Promise.resolve(lines.join("\n"));
|
|
2409
3219
|
},
|
|
2410
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
|
+
}),
|
|
2411
3915
|
"codex-health": tool({
|
|
2412
3916
|
description: "Check health of all Codex accounts by validating refresh tokens.",
|
|
2413
3917
|
args: {},
|
|
@@ -2465,11 +3969,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2465
3969
|
},
|
|
2466
3970
|
}),
|
|
2467
3971
|
"codex-remove": tool({
|
|
2468
|
-
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.",
|
|
2469
3973
|
args: {
|
|
2470
|
-
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)"),
|
|
2471
3975
|
},
|
|
2472
|
-
async execute({ index }) {
|
|
3976
|
+
async execute({ index } = {}) {
|
|
2473
3977
|
const ui = resolveUiRuntime();
|
|
2474
3978
|
const storage = await loadAccounts();
|
|
2475
3979
|
if (!storage || storage.accounts.length === 0) {
|
|
@@ -2482,7 +3986,34 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2482
3986
|
}
|
|
2483
3987
|
return "No Codex accounts configured. Nothing to remove.";
|
|
2484
3988
|
}
|
|
2485
|
-
|
|
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);
|
|
2486
4017
|
if (!Number.isFinite(targetIndex) ||
|
|
2487
4018
|
targetIndex < 0 ||
|
|
2488
4019
|
targetIndex >= storage.accounts.length) {
|
|
@@ -2490,16 +4021,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2490
4021
|
return [
|
|
2491
4022
|
...formatUiHeader(ui, "Remove account"),
|
|
2492
4023
|
"",
|
|
2493
|
-
formatUiItem(ui, `Invalid account number: ${
|
|
4024
|
+
formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
|
|
2494
4025
|
formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
|
|
2495
4026
|
formatUiItem(ui, "Use codex-list to list all accounts.", "accent"),
|
|
2496
4027
|
].join("\n");
|
|
2497
4028
|
}
|
|
2498
|
-
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.`;
|
|
2499
4030
|
}
|
|
2500
4031
|
const account = storage.accounts[targetIndex];
|
|
2501
4032
|
if (!account) {
|
|
2502
|
-
return `Account ${
|
|
4033
|
+
return `Account ${resolvedIndex} not found.`;
|
|
2503
4034
|
}
|
|
2504
4035
|
const label = formatCommandAccountLabel(account, targetIndex);
|
|
2505
4036
|
storage.accounts.splice(targetIndex, 1);
|
|
@@ -2646,15 +4177,22 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2646
4177
|
},
|
|
2647
4178
|
}),
|
|
2648
4179
|
"codex-export": tool({
|
|
2649
|
-
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.",
|
|
2650
4181
|
args: {
|
|
2651
|
-
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."),
|
|
2652
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."),
|
|
2653
4185
|
},
|
|
2654
|
-
async execute({ path: filePath, force }) {
|
|
4186
|
+
async execute({ path: filePath, force, timestamped, }) {
|
|
2655
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";
|
|
2656
4194
|
try {
|
|
2657
|
-
await exportAccounts(
|
|
4195
|
+
await exportAccounts(resolvedExportPath, force ?? true);
|
|
2658
4196
|
const storage = await loadAccounts();
|
|
2659
4197
|
const count = storage?.accounts.length ?? 0;
|
|
2660
4198
|
if (ui.v2Enabled) {
|
|
@@ -2662,10 +4200,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2662
4200
|
...formatUiHeader(ui, "Export accounts"),
|
|
2663
4201
|
"",
|
|
2664
4202
|
formatUiItem(ui, `${getStatusMarker(ui, "ok")} Exported ${count} account(s)`, "success"),
|
|
2665
|
-
formatUiKeyValue(ui, "Path",
|
|
4203
|
+
formatUiKeyValue(ui, "Path", resolvedExportPath, "muted"),
|
|
2666
4204
|
].join("\n");
|
|
2667
4205
|
}
|
|
2668
|
-
return `Exported ${count} account(s) to: ${
|
|
4206
|
+
return `Exported ${count} account(s) to: ${resolvedExportPath}`;
|
|
2669
4207
|
}
|
|
2670
4208
|
catch (error) {
|
|
2671
4209
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -2682,16 +4220,49 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2682
4220
|
},
|
|
2683
4221
|
}),
|
|
2684
4222
|
"codex-import": tool({
|
|
2685
|
-
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.",
|
|
2686
4224
|
args: {
|
|
2687
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."),
|
|
2688
4227
|
},
|
|
2689
|
-
async execute({ path: filePath }) {
|
|
4228
|
+
async execute({ path: filePath, dryRun }) {
|
|
2690
4229
|
const ui = resolveUiRuntime();
|
|
2691
4230
|
try {
|
|
2692
|
-
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";
|
|
2693
4262
|
invalidateAccountManagerCache();
|
|
2694
4263
|
const lines = [`Import complete.`, ``];
|
|
4264
|
+
lines.push(`Preview: +${preview.imported} new, ${preview.skipped} skipped, ${preview.total} total`);
|
|
4265
|
+
lines.push(`Auto-backup: ${backupSummary}`);
|
|
2695
4266
|
if (result.imported > 0) {
|
|
2696
4267
|
lines.push(`New accounts: ${result.imported}`);
|
|
2697
4268
|
}
|
|
@@ -2705,6 +4276,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2705
4276
|
"",
|
|
2706
4277
|
formatUiItem(ui, `${getStatusMarker(ui, "ok")} Import complete`, "success"),
|
|
2707
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"),
|
|
2708
4281
|
formatUiKeyValue(ui, "New accounts", String(result.imported), result.imported > 0 ? "success" : "muted"),
|
|
2709
4282
|
formatUiKeyValue(ui, "Duplicates skipped", String(result.skipped), result.skipped > 0 ? "warning" : "muted"),
|
|
2710
4283
|
formatUiKeyValue(ui, "Total accounts", String(result.total), "accent"),
|