@@ -19,13 +19,20 @@ pub enum PollError {
1919}
2020
2121pub fn poll ( ) -> Result < UsageData , PollError > {
22- let mut creds = read_credentials ( ) . ok_or ( PollError :: NoCredentials ) ?;
22+ let mut creds = match read_credentials ( ) {
23+ Some ( c) => c,
24+ None => return Err ( PollError :: NoCredentials ) ,
25+ } ;
2326
2427 if is_token_expired ( creds. expires_at ) {
25- // Token expired — ask the Claude CLI to refresh its own credentials
2628 cli_refresh_token ( ) ;
27- // Re-read the credentials file after CLI refresh
28- creds = read_credentials ( ) . ok_or ( PollError :: NoCredentials ) ?;
29+
30+ // Re-read credentials in case the CLI refreshed them
31+ match read_credentials ( ) {
32+ Some ( refreshed) => creds = refreshed,
33+ None => return Err ( PollError :: NoCredentials ) ,
34+ }
35+
2936 if is_token_expired ( creds. expires_at ) {
3037 return Err ( PollError :: TokenExpired ) ;
3138 }
@@ -34,15 +41,70 @@ pub fn poll() -> Result<UsageData, PollError> {
3441 fetch_usage_with_fallback ( & creds. access_token )
3542}
3643
37- /// Invoke the Claude CLI to trigger its internal token refresh.
38- /// `claude auth status` checks auth state, which causes the CLI to
39- /// refresh expired tokens and write updated credentials to disk.
44+ /// Invoke the Claude CLI with a minimal prompt to force its internal
45+ /// OAuth token refresh. `claude -p "."` makes the CLI
46+ /// authenticate (refreshing the access token if expired), perform a
47+ /// tiny API call, and exit — updating the credentials file on disk.
4048fn cli_refresh_token ( ) {
41- let _ = Command :: new ( "claude" )
42- . args ( [ "auth" , "status" ] )
49+ let claude_path = resolve_claude_path ( ) ;
50+ let is_cmd = claude_path. to_lowercase ( ) . ends_with ( ".cmd" ) ;
51+
52+ let args: & [ & str ] = & [ "-p" , "." ] ;
53+
54+ // Clear env vars that prevent nested Claude Code sessions
55+ let mut cmd = if is_cmd {
56+ let mut c = Command :: new ( "cmd.exe" ) ;
57+ c. arg ( "/c" ) . arg ( & claude_path) . args ( args) ;
58+ c
59+ } else {
60+ let mut c = Command :: new ( & claude_path) ;
61+ c. args ( args) ;
62+ c
63+ } ;
64+ cmd. env_remove ( "CLAUDECODE" )
65+ . env_remove ( "CLAUDE_CODE_ENTRYPOINT" )
4366 . stdout ( std:: process:: Stdio :: null ( ) )
44- . stderr ( std:: process:: Stdio :: null ( ) )
45- . status ( ) ;
67+ . stderr ( std:: process:: Stdio :: null ( ) ) ;
68+
69+ let _ = cmd. status ( ) ;
70+ }
71+
72+ /// Resolve the full path to the `claude` CLI executable.
73+ /// First tries the bare command name (works if on PATH), then falls back
74+ /// to `where.exe claude` which searches the system/user PATH from the
75+ /// registry — important for processes started via the Windows Run key
76+ /// that may not inherit the full shell PATH.
77+ fn resolve_claude_path ( ) -> String {
78+ // Quick check: try claude.cmd first (Windows npm wrapper), then bare "claude"
79+ for name in & [ "claude.cmd" , "claude" ] {
80+ if Command :: new ( name)
81+ . arg ( "--version" )
82+ . stdout ( std:: process:: Stdio :: null ( ) )
83+ . stderr ( std:: process:: Stdio :: null ( ) )
84+ . status ( )
85+ . is_ok ( )
86+ {
87+ return name. to_string ( ) ;
88+ }
89+ }
90+
91+ // Use where.exe to search the system/user PATH from the registry.
92+ // Try claude.cmd first (the Windows batch wrapper npm creates).
93+ for name in & [ "claude.cmd" , "claude" ] {
94+ if let Ok ( output) = Command :: new ( "where.exe" ) . arg ( name) . output ( ) {
95+ if output. status . success ( ) {
96+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
97+ if let Some ( first_line) = stdout. lines ( ) . next ( ) {
98+ let path = first_line. trim ( ) . to_string ( ) ;
99+ if !path. is_empty ( ) {
100+ return path;
101+ }
102+ }
103+ }
104+ }
105+ }
106+
107+ "claude.cmd" . to_string ( )
46108}
47109
48110fn fetch_usage_with_fallback ( token : & str ) -> Result < UsageData , PollError > {
@@ -68,9 +130,12 @@ fn read_credentials() -> Option<Credentials> {
68130 let json: serde_json:: Value = serde_json:: from_str ( & content) . ok ( ) ?;
69131
70132 let oauth = json. get ( "claudeAiOauth" ) ?;
133+ let access_token = oauth. get ( "accessToken" ) . and_then ( |v| v. as_str ( ) ) ?. to_string ( ) ;
134+ let expires_at = oauth. get ( "expiresAt" ) . and_then ( |v| v. as_i64 ( ) ) ;
135+
71136 Some ( Credentials {
72- access_token : oauth . get ( "accessToken" ) ? . as_str ( ) ? . to_string ( ) ,
73- expires_at : oauth . get ( "expiresAt" ) . and_then ( |v| v . as_i64 ( ) ) ,
137+ access_token,
138+ expires_at,
74139 } )
75140}
76141
@@ -84,12 +149,10 @@ fn is_token_expired(expires_at: Option<i64>) -> bool {
84149}
85150
86151fn try_model ( token : & str , model : & str ) -> Option < UsageData > {
87- let tls = std:: sync:: Arc :: new (
88- native_tls:: TlsConnector :: new ( ) . ok ( ) ?
89- ) ;
152+ let tls = native_tls:: TlsConnector :: new ( ) . ok ( ) ?;
90153 let agent = ureq:: AgentBuilder :: new ( )
91154 . timeout ( Duration :: from_secs ( 30 ) )
92- . tls_connector ( tls)
155+ . tls_connector ( std :: sync :: Arc :: new ( tls) )
93156 . build ( ) ;
94157
95158 let body = serde_json:: json!( {
@@ -106,13 +169,15 @@ fn try_model(token: &str, model: &str) -> Option<UsageData> {
106169 . send_json ( & body)
107170 {
108171 Ok ( resp) => resp,
109- Err ( ureq:: Error :: Status ( _ , resp) ) => resp,
172+ Err ( ureq:: Error :: Status ( _code , resp) ) => resp,
110173 Err ( _) => return None ,
111174 } ;
112175
113- let has_rate_limit_headers = response. header ( "anthropic-ratelimit-unified-5h-utilization" ) . is_some ( )
114- || response. header ( "anthropic-ratelimit-unified-7d-utilization" ) . is_some ( )
115- || response. header ( "anthropic-ratelimit-unified-status" ) . is_some ( ) ;
176+ let h5 = response. header ( "anthropic-ratelimit-unified-5h-utilization" ) ;
177+ let h7 = response. header ( "anthropic-ratelimit-unified-7d-utilization" ) ;
178+ let hs = response. header ( "anthropic-ratelimit-unified-status" ) ;
179+
180+ let has_rate_limit_headers = h5. is_some ( ) || h7. is_some ( ) || hs. is_some ( ) ;
116181
117182 if has_rate_limit_headers {
118183 Some ( parse_headers ( & response) )
0 commit comments