@@ -4156,9 +4156,299 @@ while (attempted.size < Math.max(1, accountCount)) {
41564156 } ) } `,
41574157 ) ;
41584158
4159- return lines . join ( "\n" ) ;
4160- } ,
4161- } ) ,
4159+ return lines . join ( "\n" ) ;
4160+ } ,
4161+ } ) ,
4162+ "codex-limits" : tool ( {
4163+ description : "Show live 5-hour and weekly Codex usage limits for all accounts." ,
4164+ args : { } ,
4165+ async execute ( ) {
4166+ const ui = resolveUiRuntime ( ) ;
4167+ const storage = await loadAccounts ( ) ;
4168+ if ( ! storage || storage . accounts . length === 0 ) {
4169+ if ( ui . v2Enabled ) {
4170+ return [
4171+ ...formatUiHeader ( ui , "Codex limits" ) ,
4172+ "" ,
4173+ formatUiItem ( ui , "No accounts configured." , "warning" ) ,
4174+ formatUiItem ( ui , "Run: opencode auth login" , "accent" ) ,
4175+ ] . join ( "\n" ) ;
4176+ }
4177+ return "No Codex accounts configured. Run: opencode auth login" ;
4178+ }
4179+
4180+ type UsageWindow = {
4181+ used_percent ?: number ;
4182+ limit_window_seconds ?: number ;
4183+ reset_at ?: number ;
4184+ reset_after_seconds ?: number ;
4185+ } | null ;
4186+
4187+ type LimitWindow = {
4188+ usedPercent ?: number ;
4189+ windowMinutes ?: number ;
4190+ resetAtMs ?: number ;
4191+ } ;
4192+
4193+ type UsageRateLimit = {
4194+ primary_window ?: UsageWindow ;
4195+ secondary_window ?: UsageWindow ;
4196+ } | null ;
4197+
4198+ type UsageCredits = {
4199+ has_credits ?: boolean ;
4200+ unlimited ?: boolean ;
4201+ balance ?: string | null ;
4202+ } | null ;
4203+
4204+ type UsagePayload = {
4205+ plan_type ?: string ;
4206+ rate_limit ?: UsageRateLimit ;
4207+ code_review_rate_limit ?: UsageRateLimit ;
4208+ additional_rate_limits ?: Array < {
4209+ limit_name ?: string ;
4210+ metered_feature ?: string ;
4211+ rate_limit ?: UsageRateLimit ;
4212+ } > | null ;
4213+ credits ?: UsageCredits ;
4214+ } ;
4215+
4216+ const formatWindowLabel = ( windowMinutes : number | undefined ) : string => {
4217+ if ( ! windowMinutes || ! Number . isFinite ( windowMinutes ) || windowMinutes <= 0 ) {
4218+ return "quota" ;
4219+ }
4220+ if ( windowMinutes % 1440 === 0 ) return `${ windowMinutes / 1440 } d` ;
4221+ if ( windowMinutes % 60 === 0 ) return `${ windowMinutes / 60 } h` ;
4222+ return `${ windowMinutes } m` ;
4223+ } ;
4224+
4225+ const formatReset = ( resetAtMs : number | undefined ) : string | undefined => {
4226+ if ( ! resetAtMs || ! Number . isFinite ( resetAtMs ) || resetAtMs <= 0 ) return undefined ;
4227+ const date = new Date ( resetAtMs ) ;
4228+ if ( ! Number . isFinite ( date . getTime ( ) ) ) return undefined ;
4229+
4230+ const now = new Date ( ) ;
4231+ const sameDay =
4232+ now . getFullYear ( ) === date . getFullYear ( ) &&
4233+ now . getMonth ( ) === date . getMonth ( ) &&
4234+ now . getDate ( ) === date . getDate ( ) ;
4235+ const time = date . toLocaleTimeString ( undefined , {
4236+ hour : "2-digit" ,
4237+ minute : "2-digit" ,
4238+ hour12 : false ,
4239+ } ) ;
4240+ if ( sameDay ) return time ;
4241+ const day = date . toLocaleDateString ( undefined , { month : "short" , day : "2-digit" } ) ;
4242+ return `${ time } on ${ day } ` ;
4243+ } ;
4244+
4245+ const mapWindow = ( window : UsageWindow ) : LimitWindow => {
4246+ if ( ! window ) return { } ;
4247+ return {
4248+ usedPercent :
4249+ typeof window . used_percent === "number" && Number . isFinite ( window . used_percent )
4250+ ? window . used_percent
4251+ : undefined ,
4252+ windowMinutes :
4253+ typeof window . limit_window_seconds === "number" && Number . isFinite ( window . limit_window_seconds )
4254+ ? Math . max ( 1 , Math . ceil ( window . limit_window_seconds / 60 ) )
4255+ : undefined ,
4256+ resetAtMs :
4257+ typeof window . reset_at === "number" && window . reset_at > 0
4258+ ? window . reset_at * 1000
4259+ : typeof window . reset_after_seconds === "number" && window . reset_after_seconds > 0
4260+ ? Date . now ( ) + window . reset_after_seconds * 1000
4261+ : undefined ,
4262+ } ;
4263+ } ;
4264+
4265+ const formatLimitTitle = ( windowMinutes : number | undefined , fallback = "quota" ) : string => {
4266+ if ( windowMinutes === 300 ) return "5h limit" ;
4267+ if ( windowMinutes === 10080 ) return "Weekly limit" ;
4268+ if ( fallback !== "quota" ) return fallback ;
4269+ return `${ formatWindowLabel ( windowMinutes ) } limit` ;
4270+ } ;
4271+
4272+ const formatLimitSummary = ( window : LimitWindow ) : string => {
4273+ const used = window . usedPercent ;
4274+ const left =
4275+ typeof used === "number" && Number . isFinite ( used )
4276+ ? Math . max ( 0 , Math . min ( 100 , Math . round ( 100 - used ) ) )
4277+ : undefined ;
4278+ const reset = formatReset ( window . resetAtMs ) ;
4279+ if ( left !== undefined && reset ) return `${ left } % left (resets ${ reset } )` ;
4280+ if ( left !== undefined ) return `${ left } % left` ;
4281+ if ( reset ) return `resets ${ reset } ` ;
4282+ return "unavailable" ;
4283+ } ;
4284+
4285+ const formatCredits = ( credits : UsageCredits ) : string | undefined => {
4286+ if ( ! credits ) return undefined ;
4287+ if ( credits . unlimited ) return "unlimited" ;
4288+ if ( typeof credits . balance === "string" && credits . balance . trim ( ) ) {
4289+ return credits . balance . trim ( ) ;
4290+ }
4291+ if ( credits . has_credits ) return "available" ;
4292+ return undefined ;
4293+ } ;
4294+
4295+ const formatExtraName = ( name : string | undefined ) : string => {
4296+ if ( ! name ) return "Additional limit" ;
4297+ if ( name === "code_review_rate_limit" ) return "Code review" ;
4298+ return name . replace ( / [ _ - ] + / g, " " ) . replace ( / \b \w / g, ( match ) => match . toUpperCase ( ) ) ;
4299+ } ;
4300+
4301+ const fetchUsage = async ( params : {
4302+ accountId : string ;
4303+ accessToken : string ;
4304+ organizationId : string | undefined ;
4305+ } ) : Promise < UsagePayload > => {
4306+ const headers = createCodexHeaders ( undefined , params . accountId , params . accessToken , {
4307+ organizationId : params . organizationId ,
4308+ } ) ;
4309+ headers . set ( "accept" , "application/json" ) ;
4310+
4311+ const response = await fetch ( `${ CODEX_BASE_URL } /wham/usage` , {
4312+ method : "GET" ,
4313+ headers,
4314+ } ) ;
4315+ if ( ! response . ok ) {
4316+ const bodyText = await response . text ( ) . catch ( ( ) => "" ) ;
4317+ throw new Error ( bodyText || `HTTP ${ response . status } ` ) ;
4318+ }
4319+ return ( await response . json ( ) ) as UsagePayload ;
4320+ } ;
4321+
4322+ // Deduplicate accounts by refreshToken (same credential = same limits)
4323+ const seenTokens = new Set < string > ( ) ;
4324+ const uniqueIndices : number [ ] = [ ] ;
4325+ for ( let i = 0 ; i < storage . accounts . length ; i ++ ) {
4326+ const acct = storage . accounts [ i ] ;
4327+ if ( ! acct ) continue ;
4328+ if ( seenTokens . has ( acct . refreshToken ) ) continue ;
4329+ seenTokens . add ( acct . refreshToken ) ;
4330+ uniqueIndices . push ( i ) ;
4331+ }
4332+
4333+ const lines : string [ ] = ui . v2Enabled
4334+ ? [ ...formatUiHeader ( ui , "Codex limits" ) , "" ]
4335+ : [ `Codex limits (${ uniqueIndices . length } account${ uniqueIndices . length === 1 ? "" : "s" } ):` , "" ] ;
4336+ const activeIndex = resolveActiveIndex ( storage , "codex" ) ;
4337+ let storageChanged = false ;
4338+
4339+ for ( const i of uniqueIndices ) {
4340+ const account = storage . accounts [ i ] ;
4341+ if ( ! account ) continue ;
4342+ const label = formatCommandAccountLabel ( account , i ) ;
4343+ const activeSuffix = i === activeIndex ? ( ui . v2Enabled ? ` ${ formatUiBadge ( ui , "active" , "accent" ) } ` : " [active]" ) : "" ;
4344+
4345+ try {
4346+ let accessToken = account . accessToken ;
4347+ if (
4348+ typeof accessToken !== "string" ||
4349+ ! accessToken ||
4350+ typeof account . expiresAt !== "number" ||
4351+ account . expiresAt <= Date . now ( ) + 30_000
4352+ ) {
4353+ const refreshResult = await queuedRefresh ( account . refreshToken ) ;
4354+ if ( refreshResult . type !== "success" ) {
4355+ throw new Error ( refreshResult . message ?? refreshResult . reason ) ;
4356+ }
4357+ account . refreshToken = refreshResult . refresh ;
4358+ account . accessToken = refreshResult . access ;
4359+ account . expiresAt = refreshResult . expires ;
4360+ accessToken = refreshResult . access ;
4361+ storageChanged = true ;
4362+ }
4363+
4364+ const accountId = account . accountId ?? extractAccountId ( accessToken ) ;
4365+ if ( ! accountId ) {
4366+ throw new Error ( "Missing account id" ) ;
4367+ }
4368+
4369+ const payload = await fetchUsage ( {
4370+ accountId,
4371+ accessToken,
4372+ organizationId : account . organizationId ,
4373+ } ) ;
4374+
4375+ const primary = mapWindow ( payload . rate_limit ?. primary_window ?? null ) ;
4376+ const secondary = mapWindow ( payload . rate_limit ?. secondary_window ?? null ) ;
4377+ const codeReviewRateLimit =
4378+ payload . code_review_rate_limit ??
4379+ payload . additional_rate_limits ?. find ( ( entry ) => entry . limit_name === "code_review_rate_limit" ) ?. rate_limit ??
4380+ null ;
4381+ const codeReview = mapWindow ( codeReviewRateLimit ?. primary_window ?? null ) ;
4382+ const credits = formatCredits ( payload . credits ?? null ) ;
4383+ const additionalLimits = ( payload . additional_rate_limits ?? [ ] ) . filter (
4384+ ( entry ) => entry . limit_name !== "code_review_rate_limit" ,
4385+ ) ;
4386+
4387+ if ( ui . v2Enabled ) {
4388+ lines . push ( formatUiItem ( ui , `${ label } ${ activeSuffix } ` ) ) ;
4389+ lines . push ( ` ${ formatUiKeyValue ( ui , formatLimitTitle ( primary . windowMinutes ) , formatLimitSummary ( primary ) , "muted" ) } ` ) ;
4390+ lines . push ( ` ${ formatUiKeyValue ( ui , formatLimitTitle ( secondary . windowMinutes ) , formatLimitSummary ( secondary ) , "muted" ) } ` ) ;
4391+ if ( codeReview . windowMinutes || typeof codeReview . usedPercent === "number" || codeReview . resetAtMs ) {
4392+ lines . push ( ` ${ formatUiKeyValue ( ui , "Code review" , formatLimitSummary ( codeReview ) , "muted" ) } ` ) ;
4393+ }
4394+ for ( const limit of additionalLimits ) {
4395+ const extraWindow = mapWindow ( limit . rate_limit ?. primary_window ?? null ) ;
4396+ lines . push ( ` ${ formatUiKeyValue ( ui , formatExtraName ( limit . limit_name ?? limit . metered_feature ) , formatLimitSummary ( extraWindow ) , "muted" ) } ` ) ;
4397+ }
4398+ if ( payload . plan_type ) {
4399+ lines . push ( ` ${ formatUiKeyValue ( ui , "Plan" , payload . plan_type , "muted" ) } ` ) ;
4400+ }
4401+ if ( credits ) {
4402+ lines . push ( ` ${ formatUiKeyValue ( ui , "Credits" , credits , "muted" ) } ` ) ;
4403+ }
4404+ } else {
4405+ lines . push ( `${ label } ${ activeSuffix } :` ) ;
4406+ lines . push ( ` ${ formatLimitTitle ( primary . windowMinutes ) } : ${ formatLimitSummary ( primary ) } ` ) ;
4407+ lines . push ( ` ${ formatLimitTitle ( secondary . windowMinutes ) } : ${ formatLimitSummary ( secondary ) } ` ) ;
4408+ if ( codeReview . windowMinutes || typeof codeReview . usedPercent === "number" || codeReview . resetAtMs ) {
4409+ lines . push ( ` Code review: ${ formatLimitSummary ( codeReview ) } ` ) ;
4410+ }
4411+ for ( const limit of additionalLimits ) {
4412+ const extraWindow = mapWindow ( limit . rate_limit ?. primary_window ?? null ) ;
4413+ lines . push ( ` ${ formatExtraName ( limit . limit_name ?? limit . metered_feature ) } : ${ formatLimitSummary ( extraWindow ) } ` ) ;
4414+ }
4415+ if ( payload . plan_type ) {
4416+ lines . push ( ` Plan: ${ payload . plan_type } ` ) ;
4417+ }
4418+ if ( credits ) {
4419+ lines . push ( ` Credits: ${ credits } ` ) ;
4420+ }
4421+ }
4422+ } catch ( error ) {
4423+ const message = error instanceof Error ? error . message : String ( error ) ;
4424+ if ( ui . v2Enabled ) {
4425+ lines . push ( formatUiItem ( ui , `${ label } ${ activeSuffix } ` ) ) ;
4426+ lines . push ( ` ${ formatUiKeyValue ( ui , "Error" , message . slice ( 0 , 160 ) , "danger" ) } ` ) ;
4427+ } else {
4428+ lines . push ( `${ label } ${ activeSuffix } :` ) ;
4429+ lines . push ( ` Error: ${ message . slice ( 0 , 160 ) } ` ) ;
4430+ }
4431+ }
4432+
4433+ lines . push ( "" ) ;
4434+ }
4435+
4436+ if ( storageChanged ) {
4437+ await saveAccounts ( storage ) ;
4438+ if ( cachedAccountManager ) {
4439+ const reloadedManager = await AccountManager . loadFromDisk ( ) ;
4440+ cachedAccountManager = reloadedManager ;
4441+ accountManagerPromise = Promise . resolve ( reloadedManager ) ;
4442+ }
4443+ }
4444+
4445+ while ( lines . length > 0 && lines [ lines . length - 1 ] === "" ) {
4446+ lines . pop ( ) ;
4447+ }
4448+
4449+ return lines . join ( "\n" ) ;
4450+ } ,
4451+ } ) ,
41624452 "codex-metrics" : tool ( {
41634453 description : "Show runtime request metrics for this plugin process." ,
41644454 args : { } ,
0 commit comments