@@ -292,6 +292,7 @@ function printUsage(): void {
292292 " codex auth list" ,
293293 " codex auth status" ,
294294 " codex auth switch <index>" ,
295+ " codex auth best [--json] [--model <model>]" ,
295296 " codex auth check" ,
296297 " codex auth features" ,
297298 " codex auth verify-flagged [--dry-run] [--json] [--no-restore]" ,
@@ -353,6 +354,7 @@ const IMPLEMENTED_FEATURES: ImplementedFeature[] = [
353354 { id : 38 , name : "Dashboard display customization" } ,
354355 { id : 39 , name : "Unified color/theme runtime (v2 UI)" } ,
355356 { id : 40 , name : "OAuth browser-first flow with manual callback fallback" } ,
357+ { id : 41 , name : "Auto-switch to best account command" } ,
356358] ;
357359
358360function runFeaturesReport ( ) : number {
@@ -3976,6 +3978,207 @@ async function runSwitch(args: string[]): Promise<number> {
39763978 return 0 ;
39773979}
39783980
3981+ async function runBest ( args : string [ ] ) : Promise < number > {
3982+ const options : ForecastCliOptions = {
3983+ live : true , // Always use live probing like the dashboard's "Best Account" option
3984+ json : args . includes ( "--json" ) || args . includes ( "-j" ) ,
3985+ model : "gpt-5-codex" ,
3986+ } ;
3987+
3988+ // Parse model if provided
3989+ for ( let i = 0 ; i < args . length ; i += 1 ) {
3990+ const arg = args [ i ] ;
3991+ if ( ! arg ) continue ;
3992+ if ( arg === "--model" || arg === "-m" ) {
3993+ const value = args [ i + 1 ] ;
3994+ if ( value ) {
3995+ options . model = value ;
3996+ }
3997+ break ;
3998+ }
3999+ if ( arg . startsWith ( "--model=" ) ) {
4000+ const value = arg . slice ( "--model=" . length ) . trim ( ) ;
4001+ if ( value ) {
4002+ options . model = value ;
4003+ }
4004+ break ;
4005+ }
4006+ }
4007+
4008+ setStoragePath ( null ) ;
4009+ const storage = await loadAccounts ( ) ;
4010+ if ( ! storage || storage . accounts . length === 0 ) {
4011+ if ( options . json ) {
4012+ console . log ( JSON . stringify ( { error : "No accounts configured." } , null , 2 ) ) ;
4013+ } else {
4014+ console . log ( "No accounts configured." ) ;
4015+ }
4016+ return 1 ;
4017+ }
4018+
4019+ const now = Date . now ( ) ;
4020+ const refreshFailures = new Map < number , TokenFailure > ( ) ;
4021+ const liveQuotaByIndex = new Map < number , Awaited < ReturnType < typeof fetchCodexQuotaSnapshot > > > ( ) ;
4022+
4023+ // Always do live probing (like dashboard's "Best Account" forecast mode)
4024+ for ( let i = 0 ; i < storage . accounts . length ; i += 1 ) {
4025+ const account = storage . accounts [ i ] ;
4026+ if ( ! account || account . enabled === false ) continue ;
4027+
4028+ let probeAccessToken = account . accessToken ;
4029+ let probeAccountId = account . accountId ?? extractAccountId ( account . accessToken ) ;
4030+ if ( ! hasUsableAccessToken ( account , now ) ) {
4031+ const refreshResult = await queuedRefresh ( account . refreshToken ) ;
4032+ if ( refreshResult . type !== "success" ) {
4033+ refreshFailures . set ( i , {
4034+ ...refreshResult ,
4035+ message : normalizeFailureDetail ( refreshResult . message , refreshResult . reason ) ,
4036+ } ) ;
4037+ continue ;
4038+ }
4039+ probeAccessToken = refreshResult . access ;
4040+ probeAccountId = account . accountId ?? extractAccountId ( refreshResult . access ) ;
4041+ }
4042+
4043+ if ( ! probeAccessToken || ! probeAccountId ) continue ;
4044+
4045+ try {
4046+ const liveQuota = await fetchCodexQuotaSnapshot ( {
4047+ accountId : probeAccountId ,
4048+ accessToken : probeAccessToken ,
4049+ model : options . model ,
4050+ } ) ;
4051+ liveQuotaByIndex . set ( i , liveQuota ) ;
4052+ } catch {
4053+ // Ignore probe errors for best selection
4054+ }
4055+ }
4056+
4057+ const forecastInputs = storage . accounts . map ( ( account , index ) => ( {
4058+ index,
4059+ account,
4060+ isCurrent : index === resolveActiveIndex ( storage , "codex" ) ,
4061+ now,
4062+ refreshFailure : refreshFailures . get ( index ) ,
4063+ liveQuota : liveQuotaByIndex . get ( index ) ,
4064+ } ) ) ;
4065+
4066+ const forecastResults = evaluateForecastAccounts ( forecastInputs ) ;
4067+ const recommendation = recommendForecastAccount ( forecastResults ) ;
4068+
4069+ if ( recommendation . recommendedIndex === null ) {
4070+ if ( options . json ) {
4071+ console . log ( JSON . stringify ( { error : recommendation . reason } , null , 2 ) ) ;
4072+ } else {
4073+ console . log ( `No best account available: ${ recommendation . reason } ` ) ;
4074+ }
4075+ return 1 ;
4076+ }
4077+
4078+ const bestIndex = recommendation . recommendedIndex ;
4079+ const bestAccount = storage . accounts [ bestIndex ] ;
4080+ if ( ! bestAccount ) {
4081+ if ( options . json ) {
4082+ console . log ( JSON . stringify ( { error : "Best account not found." } , null , 2 ) ) ;
4083+ } else {
4084+ console . log ( "Best account not found." ) ;
4085+ }
4086+ return 1 ;
4087+ }
4088+
4089+ // Check if already on best account
4090+ const currentIndex = resolveActiveIndex ( storage , "codex" ) ;
4091+ if ( currentIndex === bestIndex ) {
4092+ if ( options . json ) {
4093+ console . log ( JSON . stringify ( {
4094+ message : `Already on best account: ${ formatAccountLabel ( bestAccount , bestIndex ) } ` ,
4095+ accountIndex : bestIndex + 1 ,
4096+ reason : recommendation . reason ,
4097+ } , null , 2 ) ) ;
4098+ } else {
4099+ console . log ( `Already on best account ${ bestIndex + 1 } : ${ formatAccountLabel ( bestAccount , bestIndex ) } ` ) ;
4100+ console . log ( `Reason: ${ recommendation . reason } ` ) ;
4101+ }
4102+ return 0 ;
4103+ }
4104+
4105+ // Switch to best account (reuse runSwitch logic)
4106+ const targetIndex = bestIndex ;
4107+ const parsed = targetIndex + 1 ;
4108+ storage . activeIndex = targetIndex ;
4109+ storage . activeIndexByFamily = storage . activeIndexByFamily ?? { } ;
4110+ for ( const family of MODEL_FAMILIES ) {
4111+ storage . activeIndexByFamily [ family ] = targetIndex ;
4112+ }
4113+ const wasDisabled = bestAccount . enabled === false ;
4114+ if ( wasDisabled ) {
4115+ bestAccount . enabled = true ;
4116+ }
4117+ const switchNow = Date . now ( ) ;
4118+ let syncAccessToken = bestAccount . accessToken ;
4119+ let syncRefreshToken = bestAccount . refreshToken ;
4120+ let syncExpiresAt = bestAccount . expiresAt ;
4121+ let syncIdToken : string | undefined ;
4122+
4123+ if ( ! hasUsableAccessToken ( bestAccount , switchNow ) ) {
4124+ const refreshResult = await queuedRefresh ( bestAccount . refreshToken ) ;
4125+ if ( refreshResult . type === "success" ) {
4126+ const tokenAccountId = extractAccountId ( refreshResult . access ) ;
4127+ const nextEmail = sanitizeEmail ( extractAccountEmail ( refreshResult . access , refreshResult . idToken ) ) ;
4128+ if ( bestAccount . refreshToken !== refreshResult . refresh ) {
4129+ bestAccount . refreshToken = refreshResult . refresh ;
4130+ }
4131+ if ( bestAccount . accessToken !== refreshResult . access ) {
4132+ bestAccount . accessToken = refreshResult . access ;
4133+ }
4134+ if ( bestAccount . expiresAt !== refreshResult . expires ) {
4135+ bestAccount . expiresAt = refreshResult . expires ;
4136+ }
4137+ if ( nextEmail && nextEmail !== bestAccount . email ) {
4138+ bestAccount . email = nextEmail ;
4139+ }
4140+ if ( tokenAccountId && tokenAccountId !== bestAccount . accountId ) {
4141+ bestAccount . accountId = tokenAccountId ;
4142+ bestAccount . accountIdSource = "token" ;
4143+ }
4144+ syncAccessToken = refreshResult . access ;
4145+ syncRefreshToken = refreshResult . refresh ;
4146+ syncExpiresAt = refreshResult . expires ;
4147+ syncIdToken = refreshResult . idToken ;
4148+ }
4149+ }
4150+
4151+ bestAccount . lastUsed = switchNow ;
4152+ bestAccount . lastSwitchReason = "best" ;
4153+ await saveAccounts ( storage ) ;
4154+
4155+ const synced = await setCodexCliActiveSelection ( {
4156+ accountId : bestAccount . accountId ,
4157+ email : bestAccount . email ,
4158+ accessToken : syncAccessToken ,
4159+ refreshToken : syncRefreshToken ,
4160+ expiresAt : syncExpiresAt ,
4161+ ...( syncIdToken ? { idToken : syncIdToken } : { } ) ,
4162+ } ) ;
4163+
4164+ if ( options . json ) {
4165+ console . log ( JSON . stringify ( {
4166+ message : `Switched to best account: ${ formatAccountLabel ( bestAccount , targetIndex ) } ` ,
4167+ accountIndex : parsed ,
4168+ reason : recommendation . reason ,
4169+ synced,
4170+ wasDisabled,
4171+ } , null , 2 ) ) ;
4172+ } else {
4173+ console . log ( `Switched to best account ${ parsed } : ${ formatAccountLabel ( bestAccount , targetIndex ) } ${ wasDisabled ? " (re-enabled)" : "" } ` ) ;
4174+ console . log ( `Reason: ${ recommendation . reason } ` ) ;
4175+ if ( ! synced ) {
4176+ console . warn ( "Codex auth sync did not complete. Multi-auth routing will still use this account." ) ;
4177+ }
4178+ }
4179+ return 0 ;
4180+ }
4181+
39794182export async function autoSyncActiveAccountToCodex ( ) : Promise < boolean > {
39804183 setStoragePath ( null ) ;
39814184 const storage = await loadAccounts ( ) ;
@@ -4095,6 +4298,9 @@ export async function runCodexMultiAuthCli(rawArgs: string[]): Promise<number> {
40954298 if ( command === "forecast" ) {
40964299 return runForecast ( rest ) ;
40974300 }
4301+ if ( command === "best" ) {
4302+ return runBest ( rest ) ;
4303+ }
40984304 if ( command === "report" ) {
40994305 return runReport ( rest ) ;
41004306 }
0 commit comments