ai_agent/
session_discovery.rs1use crate::constants::env::{ai, ai_code};
5use crate::utils::http::get_user_agent;
6use serde::{Deserialize, Serialize};
7
8#[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#[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
35fn get_discovery_api_base_url() -> String {
37 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
47fn get_oauth_token() -> Option<String> {
49 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
62fn 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
92pub 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 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
150pub 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
237pub 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 let available = is_discovery_available();
250 assert!(!available);
251 }
252
253 #[test]
254 fn test_get_api_base_url_default() {
255 let url = get_discovery_api_base_url();
257 assert_eq!(url, "https://api.anthropic.com");
258 }
259}