Skip to main content

ai_agent/
session_history.rs

1// Session history fetching from remote API
2// Translated from TypeScript: src/assistant/sessionHistory.ts
3//
4// Also includes bridge config functions translated from:
5// openclaudecode/src/bridge/bridgeConfig.ts
6
7use crate::constants::env::{ai, ai_code};
8use crate::utils::http::get_user_agent;
9use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13use std::path::PathBuf;
14
15// =============================================================================
16// BRIDGE CONFIG - translated from openclaudecode/src/bridge/bridgeConfig.ts
17// =============================================================================
18
19/// Ant-only dev override: AI_BRIDGE_OAUTH_TOKEN, else None.
20pub fn get_bridge_token_override() -> Option<String> {
21    if std::env::var(ai::USER_TYPE).ok().as_deref() == Some("ant") {
22        std::env::var(ai::BRIDGE_OAUTH_TOKEN).ok()
23    } else {
24        None
25    }
26}
27
28/// Ant-only dev override: AI_BRIDGE_BASE_URL, else None.
29pub fn get_bridge_base_url_override() -> Option<String> {
30    if std::env::var(ai::USER_TYPE).ok().as_deref() == Some("ant") {
31        std::env::var(ai::BRIDGE_BASE_URL).ok()
32    } else {
33        None
34    }
35}
36
37/// OAuth tokens stored in secure storage
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct OAuthTokens {
40    #[serde(rename = "accessToken")]
41    pub access_token: String,
42    #[serde(rename = "refreshToken")]
43    pub refresh_token: Option<String>,
44    #[serde(rename = "expiresAt")]
45    pub expires_at: Option<String>,
46    pub scopes: Vec<String>,
47    #[serde(rename = "subscriptionType")]
48    pub subscription_type: Option<String>,
49    #[serde(rename = "rateLimitTier")]
50    pub rate_limit_tier: Option<String>,
51}
52
53/// Get OAuth tokens from secure storage (keychain or file)
54pub fn get_claude_ai_oauth_tokens() -> Option<OAuthTokens> {
55    // Check for force-set OAuth token from environment variable
56    if let Ok(token) = std::env::var(ai::OAUTH_TOKEN) {
57        if !token.is_empty() {
58            return Some(OAuthTokens {
59                access_token: token,
60                refresh_token: None,
61                expires_at: None,
62                scopes: vec!["user:inference".to_string()],
63                subscription_type: None,
64                rate_limit_tier: None,
65            });
66        }
67    }
68
69    if let Ok(token) = std::env::var(ai_code::OAUTH_TOKEN) {
70        if !token.is_empty() {
71            return Some(OAuthTokens {
72                access_token: token,
73                refresh_token: None,
74                expires_at: None,
75                scopes: vec!["user:inference".to_string()],
76                subscription_type: None,
77                rate_limit_tier: None,
78            });
79        }
80    }
81
82    // Try to read from secure storage
83    if let Some(home) = dirs::home_dir() {
84        // Try the new path: ~/.ai/oauth/tokens.json
85        let ai_oauth_path = home.join(".ai").join("oauth").join("tokens.json");
86        if let Ok(tokens) = read_oauth_tokens_from_path(&ai_oauth_path) {
87            return Some(tokens);
88        }
89
90        // Fallback to old path: ~/.ai/oauth/tokens.json
91        let claude_oauth_path = home.join(".ai").join("oauth").join("tokens.json");
92        if let Ok(tokens) = read_oauth_tokens_from_path(&claude_oauth_path) {
93            return Some(tokens);
94        }
95    }
96
97    None
98}
99
100fn read_oauth_tokens_from_path(path: &PathBuf) -> Result<OAuthTokens, Box<dyn std::error::Error>> {
101    let content = fs::read_to_string(path)?;
102    let tokens: OAuthTokens = serde_json::from_str(&content)?;
103    Ok(tokens)
104}
105
106/// Access token for bridge API calls: dev override first, then the OAuth
107/// keychain. None means "not logged in".
108pub fn get_bridge_access_token() -> Option<String> {
109    // First check dev override
110    if let Some(token) = get_bridge_token_override() {
111        return Some(token);
112    }
113
114    // Then check OAuth tokens
115    get_claude_ai_oauth_tokens().map(|t| t.access_token)
116}
117
118/// Base URL for bridge API calls: dev override first, then the production
119/// OAuth config. Always returns a URL.
120pub fn get_bridge_base_url() -> String {
121    // First check dev override
122    if let Some(url) = get_bridge_base_url_override() {
123        return url;
124    }
125
126    // Then check OAuth config
127    get_oauth_config().base_api_url
128}
129
130/// Get all bridge-related headers for API calls
131pub fn get_bridge_headers() -> HashMap<String, String> {
132    let mut headers = HashMap::new();
133
134    if let Some(token) = get_bridge_access_token() {
135        headers.insert("Authorization".to_string(), format!("Bearer {}", token));
136    }
137
138    headers.insert("Content-Type".to_string(), "application/json".to_string());
139    headers.insert("anthropic-version".to_string(), "2023-06-01".to_string());
140    headers.insert("User-Agent".to_string(), get_user_agent());
141
142    headers
143}
144
145// =============================================================================
146// SESSION HISTORY
147// =============================================================================
148
149pub const HISTORY_PAGE_SIZE: u32 = 100;
150
151pub type SDKMessage = serde_json::Value;
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct HistoryPage {
155    pub events: Vec<SDKMessage>,
156    #[serde(rename = "firstId")]
157    pub first_id: Option<String>,
158    #[serde(rename = "hasMore")]
159    pub has_more: bool,
160}
161
162#[derive(Debug, Deserialize)]
163struct SessionEventsResponse {
164    data: Vec<SDKMessage>,
165    #[serde(rename = "has_more")]
166    has_more: bool,
167    #[serde(rename = "first_id")]
168    first_id: Option<String>,
169    #[allow(dead_code)]
170    #[serde(rename = "last_id")]
171    last_id: Option<String>,
172}
173
174#[derive(Debug, Clone)]
175pub struct HistoryAuthCtx {
176    pub base_url: String,
177    pub headers: HashMap<String, String>,
178}
179
180pub struct OauthConfig {
181    pub base_api_url: String,
182}
183
184fn get_oauth_config() -> OauthConfig {
185    if std::env::var(ai::USER_TYPE).ok().as_deref() == Some("ant") {
186        if std::env::var(ai::USE_LOCAL_OAUTH)
187            .map(|v| v == "1" || v.to_lowercase() == "true")
188            .unwrap_or(false)
189        {
190            let api = std::env::var(ai::CLAUDE_LOCAL_OAUTH_API_BASE)
191                .unwrap_or_else(|_| "http://localhost:8000".to_string());
192            return OauthConfig {
193                base_api_url: api.trim_end_matches('/').to_string(),
194            };
195        }
196        if std::env::var(ai::USE_STAGING_OAUTH)
197            .map(|v| v == "1" || v.to_lowercase() == "true")
198            .unwrap_or(false)
199        {
200            return OauthConfig {
201                base_api_url: "https://api-staging.anthropic.com".to_string(),
202            };
203        }
204    }
205
206    if let Ok(custom_url) = std::env::var(ai_code::CUSTOM_OAUTH_URL) {
207        let base = custom_url.trim_end_matches('/').to_string();
208        return OauthConfig { base_api_url: base };
209    }
210
211    OauthConfig {
212        base_api_url: "https://api.anthropic.com".to_string(),
213    }
214}
215
216pub async fn prepare_api_request() -> Result<(String, String), crate::AgentError> {
217    let access_token = get_access_token()?;
218    let org_uuid = get_org_uuid()?;
219    Ok((access_token, org_uuid))
220}
221
222fn get_access_token() -> Result<String, crate::AgentError> {
223    if let Ok(token) = std::env::var(ai_code::ACCESS_TOKEN) {
224        if !token.is_empty() {
225            return Ok(token);
226        }
227    }
228
229    if let Some(home) = dirs::home_dir() {
230        let keychain_path = home.join(".ai").join("oauth").join("tokens.json");
231        if let Ok(content) = std::fs::read_to_string(&keychain_path) {
232            if let Ok(tokens) = serde_json::from_str::<serde_json::Value>(&content) {
233                if let Some(token) = tokens.get("accessToken").and_then(|t| t.as_str()) {
234                    return Ok(token.to_string());
235                }
236            }
237        }
238    }
239
240    Err(crate::AgentError::Auth(
241        "Claude Code web sessions require authentication with a Claude.ai account. Please run /login to authenticate, or check your authentication status with /status.".to_string(),
242    ))
243}
244
245fn get_org_uuid() -> Result<String, crate::AgentError> {
246    if let Ok(org) = std::env::var(ai_code::ORG_UUID) {
247        if !org.is_empty() {
248            return Ok(org);
249        }
250    }
251
252    if let Some(home) = dirs::home_dir() {
253        let settings_path = home.join(".ai").join("settings.json");
254        if let Ok(content) = std::fs::read_to_string(&settings_path) {
255            if let Ok(settings) = serde_json::from_str::<serde_json::Value>(&content) {
256                if let Some(org) = settings.get("orgUUID").and_then(|o| o.as_str()) {
257                    return Ok(org.to_string());
258                }
259            }
260        }
261    }
262
263    Err(crate::AgentError::Auth(
264        "Organization UUID not found. Please authenticate with Claude Code.".to_string(),
265    ))
266}
267
268pub fn get_oauth_headers(access_token: &str) -> HashMap<String, String> {
269    let mut headers = HashMap::new();
270    headers.insert(
271        "Authorization".to_string(),
272        format!("Bearer {}", access_token),
273    );
274    headers.insert("Content-Type".to_string(), "application/json".to_string());
275    headers.insert("anthropic-version".to_string(), "2023-06-01".to_string());
276    headers.insert("User-Agent".to_string(), get_user_agent());
277    headers
278}
279
280pub async fn create_history_auth_ctx(
281    session_id: &str,
282) -> Result<HistoryAuthCtx, crate::AgentError> {
283    let (access_token, org_uuid) = prepare_api_request().await?;
284    let oauth_config = get_oauth_config();
285
286    let mut headers = get_oauth_headers(&access_token);
287    headers.insert(
288        "anthropic-beta".to_string(),
289        "ccr-byoc-2025-07-29".to_string(),
290    );
291    headers.insert("x-organization-uuid".to_string(), org_uuid);
292
293    let base_url = format!(
294        "{}/v1/sessions/{}/events",
295        oauth_config.base_api_url, session_id
296    );
297
298    Ok(HistoryAuthCtx { base_url, headers })
299}
300
301async fn fetch_page(
302    ctx: &HistoryAuthCtx,
303    params: &HashMap<String, serde_json::Value>,
304    label: &str,
305) -> Result<Option<HistoryPage>, crate::AgentError> {
306    let client = reqwest::Client::new();
307
308    let mut query_params: Vec<(&str, String)> = Vec::new();
309    for (key, value) in params {
310        query_params.push((key.as_str(), value.to_string()));
311    }
312
313    let mut header_map = HeaderMap::new();
314    for (key, value) in &ctx.headers {
315        if let (Ok(name), Ok(val)) = (key.parse::<HeaderName>(), value.parse::<HeaderValue>()) {
316            header_map.insert(name, val);
317        }
318    }
319
320    let resp = client
321        .get(&ctx.base_url)
322        .headers(header_map)
323        .query(&query_params)
324        .timeout(std::time::Duration::from_secs(15))
325        .send()
326        .await;
327
328    match resp {
329        Ok(response) => {
330            if response.status() == reqwest::StatusCode::OK {
331                let data: SessionEventsResponse = response
332                    .json()
333                    .await
334                    .map_err(|e| crate::AgentError::Http(e))?;
335
336                Ok(Some(HistoryPage {
337                    events: data.data,
338                    first_id: data.first_id,
339                    has_more: data.has_more,
340                }))
341            } else {
342                log_for_debugging(&format!("[{}] HTTP {}", label, response.status()));
343                Ok(None)
344            }
345        }
346        Err(e) => {
347            log_for_debugging(&format!("[{}] error: {}", label, e));
348            Ok(None)
349        }
350    }
351}
352
353fn log_for_debugging(message: &str) {
354    log::debug!("{}", message);
355}
356
357pub async fn fetch_latest_events(
358    ctx: &HistoryAuthCtx,
359    limit: u32,
360) -> Result<Option<HistoryPage>, crate::AgentError> {
361    let mut params = HashMap::new();
362    params.insert("limit".to_string(), serde_json::json!(limit));
363    params.insert("anchor_to_latest".to_string(), serde_json::json!(true));
364
365    fetch_page(ctx, &params, "fetchLatestEvents").await
366}
367
368pub async fn fetch_older_events(
369    ctx: &HistoryAuthCtx,
370    before_id: &str,
371    limit: u32,
372) -> Result<Option<HistoryPage>, crate::AgentError> {
373    let mut params = HashMap::new();
374    params.insert("limit".to_string(), serde_json::json!(limit));
375    params.insert("before_id".to_string(), serde_json::json!(before_id));
376
377    fetch_page(ctx, &params, "fetchOlderEvents").await
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_get_oauth_headers() {
386        let token = "test_token";
387        let headers = get_oauth_headers(token);
388
389        assert_eq!(
390            headers.get("Authorization"),
391            Some(&"Bearer test_token".to_string())
392        );
393        assert_eq!(
394            headers.get("Content-Type"),
395            Some(&"application/json".to_string())
396        );
397        assert_eq!(
398            headers.get("anthropic-version"),
399            Some(&"2023-06-01".to_string())
400        );
401    }
402
403    #[test]
404    fn test_history_page_default() {
405        let page = HistoryPage {
406            events: vec![],
407            first_id: None,
408            has_more: false,
409        };
410
411        assert_eq!(page.events.len(), 0);
412        assert_eq!(page.first_id, None);
413        assert_eq!(page.has_more, false);
414    }
415}