From d54588bf6e0937c9ea72100dfedf226f26331d1a Mon Sep 17 00:00:00 2001 From: Craig Constable Date: Tue, 5 May 2026 18:46:57 +1000 Subject: [PATCH] v1.3.9 feat: check for other credential sources on permanent credential failure. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/poller.rs | 84 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc3d2e0..ff50376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "claude-code-usage-monitor" -version = "1.3.8" +version = "1.3.9" dependencies = [ "dirs", "native-tls", diff --git a/Cargo.toml b/Cargo.toml index 5072028..341e766 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "claude-code-usage-monitor" -version = "1.3.8" +version = "1.3.9" edition = "2021" license = "MIT" description = "Claude Code Usage Monitor" diff --git a/src/poller.rs b/src/poller.rs index 5bd4bfd..39b36b3 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -44,7 +44,7 @@ struct UsageBucket { } pub fn poll() -> Result { - let mut creds = match read_credentials() { + let creds = match read_first_credentials() { Some(c) => c, None => { diagnose::log("poll failed: no Claude credentials found"); @@ -52,26 +52,37 @@ pub fn poll() -> Result { } }; - if is_token_expired(creds.expires_at) { - cli_refresh_token(&creds.source); - - match read_credentials_from_source(&creds.source) { - Some(refreshed) => creds = refreshed, - None => { - diagnose::log("poll failed: credentials still unavailable after refresh attempt"); - return Err(PollError::NoCredentials); - } - } - - if is_token_expired(creds.expires_at) { - diagnose::log("poll failed: token is still expired after refresh attempt"); - return Err(PollError::TokenExpired); - } - } + let creds = refresh_or_fallback(creds)?; fetch_usage_with_fallback(&creds.access_token) } +fn refresh_or_fallback(mut creds: Credentials) -> Result { + loop { + if !is_token_expired(creds.expires_at) { + return Ok(creds); + } + + let source = creds.source.clone(); + cli_refresh_token(&source); + + match read_credentials_from_source(&source) { + Some(refreshed) if !is_token_expired(refreshed.expires_at) => return Ok(refreshed), + Some(_) => diagnose::log(format!( + "credentials from {source:?} still expired after refresh attempt" + )), + None => diagnose::log(format!( + "credentials from {source:?} unavailable after refresh attempt" + )), + } + + match read_next_credentials_after(&source) { + Some(next) => creds = next, + None => return Err(PollError::TokenExpired), + } + } +} + /// Invoke the Claude CLI with a minimal prompt to force its internal /// OAuth token refresh. fn cli_refresh_token(source: &CredentialSource) { @@ -245,7 +256,7 @@ fn build_agent() -> Result { pub fn credential_watch_snapshot(mode: CredentialWatchMode) -> CredentialWatchSnapshot { let sources = match mode { - CredentialWatchMode::ActiveSource => read_credentials() + CredentialWatchMode::ActiveSource => read_first_credentials() .map(|creds| vec![creds.source]) .unwrap_or_else(all_known_credential_sources), CredentialWatchMode::AllSources => all_known_credential_sources(), @@ -506,20 +517,18 @@ enum CredentialSource { Wsl { distro: String }, } -fn read_credentials() -> Option { - let mut candidates = Vec::new(); - +fn read_first_credentials() -> Option { if let Some(creds) = read_windows_credentials() { - candidates.push(creds); + return Some(creds); } for distro in list_wsl_distros() { if let Some(creds) = read_wsl_credentials(&distro) { - candidates.push(creds); + return Some(creds); } } - choose_best_credentials(candidates) + None } fn read_windows_credentials() -> Option { @@ -600,13 +609,30 @@ fn parse_credentials(content: &str, source: CredentialSource) -> Option) -> Option { - if candidates.is_empty() { - return None; +fn read_next_credentials_after(source: &CredentialSource) -> Option { + match source { + CredentialSource::Windows(_) => { + for distro in list_wsl_distros() { + if let Some(creds) = read_wsl_credentials(&distro) { + return Some(creds); + } + } + } + CredentialSource::Wsl { distro } => { + let mut past_current = false; + for candidate_distro in list_wsl_distros() { + if !past_current { + past_current = candidate_distro == *distro; + continue; + } + if let Some(creds) = read_wsl_credentials(&candidate_distro) { + return Some(creds); + } + } + } } - candidates.sort_by_key(|creds| is_token_expired(creds.expires_at)); - candidates.into_iter().next() + None } fn list_wsl_distros() -> Vec {