Skip to main content

ai_agent/
session_discovery.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/assistant/sessionDiscovery.ts
2//! Session discovery for assistant sessions - discover sessions from the remote API
3
4use crate::constants::env::{ai, ai_code};
5use crate::utils::http::get_user_agent;
6use serde::{Deserialize, Serialize};
7
8/// Assistant session discovered from the remote API
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AssistantSession {
11    pub id: String,
12    #[serde(rename = "createdAt")]
13    pub created_at: String,
14    #[serde(rename = "updatedAt")]
15    pub updated_at: String,
16    pub model: Option<String>,
17    pub summary: Option<String>,
18    pub tag: Option<String>,
19    #[serde(rename = "messageCount")]
20    pub message_count: Option<u32>,
21    #[serde(rename = "cwd")]
22    pub working_directory: Option<String>,
23}
24
25/// Discovery result with metadata
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct DiscoveryResult {
28    pub sessions: Vec<AssistantSession>,
29    pub success: bool,
30    pub error: Option<String>,
31    #[serde(rename = "totalCount")]
32    pub total_count: Option<u32>,
33}
34
35/// Get the base URL for the session discovery API
36fn get_discovery_api_base_url() -> String {
37    // Check for custom API base URL
38    if let Ok(base_url) = std::env::var(ai::API_BASE_URL) {
39        return base_url.trim_end_matches('/').to_string();
40    }
41    if let Ok(base_url) = std::env::var(ai::BASE_URL) {
42        return base_url.trim_end_matches('/').to_string();
43    }
44    "https://api.anthropic.com".to_string()
45}
46
47/// Get the OAuth token for authentication
48fn get_oauth_token() -> Option<String> {
49    // Check various environment variables for the token
50    std::env::var(ai_code::OAUTH_TOKEN)
51        .ok()
52        .filter(|t| !t.is_empty())
53        .or_else(|| {
54            std::env::var(ai::OAUTH_TOKEN)
55                .ok()
56                .filter(|t| !t.is_empty())
57        })
58        .or_else(|| std::env::var(ai::AUTH_TOKEN).ok().filter(|t| !t.is_empty()))
59        .or_else(|| std::env::var(ai::API_KEY).ok().filter(|t| !t.is_empty()))
60}
61
62/// Build HTTP headers for the discovery request
63fn build_discovery_headers() -> Result<reqwest::header::HeaderMap, String> {
64    let mut headers = reqwest::header::HeaderMap::new();
65    headers.insert(
66        "Content-Type",
67        reqwest::header::HeaderValue::from_static("application/json"),
68    );
69    headers.insert(
70        "anthropic-version",
71        reqwest::header::HeaderValue::from_static("2025-04-20"),
72    );
73
74    if let Some(token) = get_oauth_token() {
75        let auth_value = format!("Bearer {}", token);
76        headers.insert(
77            "Authorization",
78            reqwest::header::HeaderValue::from_str(&auth_value)
79                .map_err(|e| format!("Invalid auth header: {}", e))?,
80        );
81    }
82
83    headers.insert(
84        "User-Agent",
85        reqwest::header::HeaderValue::from_str(&get_user_agent())
86            .map_err(|e| format!("Invalid User-Agent header: {}", e))?,
87    );
88
89    Ok(headers)
90}
91
92/// Discover assistant sessions from the remote API
93pub async fn discover_assistant_sessions() -> Vec<serde_json::Value> {
94    let base_url = get_discovery_api_base_url();
95    let url = format!("{}/api/claude_code/sessions", base_url);
96
97    let headers = match build_discovery_headers() {
98        Ok(h) => h,
99        Err(e) => {
100            log::warn!("Failed to build discovery headers: {}", e);
101            return Vec::new();
102        }
103    };
104
105    let client = match reqwest::Client::builder()
106        .timeout(std::time::Duration::from_secs(30))
107        .build()
108    {
109        Ok(c) => c,
110        Err(e) => {
111            log::warn!("Failed to build HTTP client: {}", e);
112            return Vec::new();
113        }
114    };
115
116    let response = match client.get(&url).headers(headers).send().await {
117        Ok(r) => r,
118        Err(e) => {
119            log::debug!("Session discovery request failed: {}", e);
120            return Vec::new();
121        }
122    };
123
124    if !response.status().is_success() {
125        log::debug!(
126            "Session discovery returned non-success status: {}",
127            response.status()
128        );
129        return Vec::new();
130    }
131
132    match response.json::<serde_json::Value>().await {
133        Ok(json) => {
134            // Extract sessions array from response
135            if let Some(sessions) = json.get("sessions").and_then(|s| s.as_array()) {
136                sessions.clone()
137            } else if json.is_array() {
138                json.as_array().cloned().unwrap_or_default()
139            } else {
140                Vec::new()
141            }
142        }
143        Err(e) => {
144            log::debug!("Failed to parse discovery response: {}", e);
145            Vec::new()
146        }
147    }
148}
149
150/// Discover sessions with structured result
151pub async fn discover_sessions_with_result() -> DiscoveryResult {
152    let base_url = get_discovery_api_base_url();
153    let url = format!("{}/api/claude_code/sessions", base_url);
154
155    let headers = match build_discovery_headers() {
156        Ok(h) => h,
157        Err(e) => {
158            return DiscoveryResult {
159                sessions: Vec::new(),
160                success: false,
161                error: Some(format!("Failed to build headers: {}", e)),
162                total_count: None,
163            };
164        }
165    };
166
167    let client = match reqwest::Client::builder()
168        .timeout(std::time::Duration::from_secs(30))
169        .build()
170    {
171        Ok(c) => c,
172        Err(e) => {
173            return DiscoveryResult {
174                sessions: Vec::new(),
175                success: false,
176                error: Some(format!("Failed to build client: {}", e)),
177                total_count: None,
178            };
179        }
180    };
181
182    let response = match client.get(&url).headers(headers).send().await {
183        Ok(r) => r,
184        Err(e) => {
185            return DiscoveryResult {
186                sessions: Vec::new(),
187                success: false,
188                error: Some(format!("Request failed: {}", e)),
189                total_count: None,
190            };
191        }
192    };
193
194    if !response.status().is_success() {
195        let status = response.status().as_u16();
196        return DiscoveryResult {
197            sessions: Vec::new(),
198            success: false,
199            error: Some(format!("API returned status: {}", status)),
200            total_count: None,
201        };
202    }
203
204    match response.json::<serde_json::Value>().await {
205        Ok(json) => {
206            let sessions =
207                if let Some(sessions_arr) = json.get("sessions").and_then(|s| s.as_array()) {
208                    sessions_arr
209                        .iter()
210                        .filter_map(|s| serde_json::from_value::<AssistantSession>(s.clone()).ok())
211                        .collect()
212                } else {
213                    Vec::new()
214                };
215
216            let total_count = json
217                .get("totalCount")
218                .and_then(|v| v.as_u64())
219                .map(|n| n as u32);
220
221            DiscoveryResult {
222                sessions,
223                success: true,
224                error: None,
225                total_count,
226            }
227        }
228        Err(e) => DiscoveryResult {
229            sessions: Vec::new(),
230            success: false,
231            error: Some(format!("Failed to parse response: {}", e)),
232            total_count: None,
233        },
234    }
235}
236
237/// Check if session discovery is available (has auth credentials)
238pub fn is_discovery_available() -> bool {
239    get_oauth_token().is_some()
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_is_discovery_available_no_token() {
248        // By default, no token should be set in test environment
249        let available = is_discovery_available();
250        assert!(!available);
251    }
252
253    #[test]
254    fn test_get_api_base_url_default() {
255        // Without env vars set, should return default
256        let url = get_discovery_api_base_url();
257        assert_eq!(url, "https://api.anthropic.com");
258    }
259}