1use crate::constants::env::ai;
8use crate::utils::http::get_user_agent;
9use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GitSource {
15 #[serde(rename = "type")]
16 pub source_type: String,
17 pub url: String,
18 pub revision: Option<String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct GitOutcome {
24 #[serde(rename = "type")]
25 pub outcome_type: String,
26 #[serde(rename = "git_info")]
27 pub git_info: GitInfo,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct GitInfo {
33 #[serde(rename = "type")]
34 pub info_type: String,
35 pub repo: String,
36 pub branches: Vec<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct SessionEvent {
42 #[serde(rename = "type")]
43 pub event_type: String,
44 pub data: serde_json::Value,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SessionContext {
50 pub sources: Vec<GitSource>,
51 pub outcomes: Vec<GitOutcome>,
52 pub model: Option<String>,
53}
54
55pub async fn create_bridge_session(
63 environment_id: &str,
64 title: Option<&str>,
65 events: Vec<SessionEvent>,
66 git_repo_url: Option<&str>,
67 branch: Option<&str>,
68 base_url: Option<&str>,
69 get_access_token: Option<&dyn Fn() -> Option<String>>,
70 permission_mode: Option<&str>,
71) -> Option<String> {
72 let access_token = get_access_token
74 .and_then(|f| f())
75 .or_else(|| crate::bridge::get_bridge_access_token());
76
77 let access_token = match access_token {
78 Some(t) => t,
79 None => {
80 log_for_debugging("[bridge] No access token for session creation");
81 return None;
82 }
83 };
84
85 let org_uuid = get_organization_uuid();
87 let org_uuid = match org_uuid {
88 Some(uuid) => uuid,
89 None => {
90 log_for_debugging("[bridge] No org UUID for session creation");
91 return None;
92 }
93 };
94
95 let (git_source, git_outcome) = if let Some(repo_url) = git_repo_url {
97 build_git_context(repo_url, branch)
98 } else {
99 (None, None)
100 };
101
102 let mut request_body = serde_json::json!({
104 "events": events,
105 "session_context": {
106 "sources": git_source.map(|s| vec![s]).unwrap_or_default(),
107 "outcomes": git_outcome.map(|o| vec![o]).unwrap_or_default(),
108 "model": get_main_loop_model(),
109 },
110 "environment_id": environment_id,
111 "source": "remote-control",
112 });
113
114 if let Some(t) = title {
115 request_body["title"] = serde_json::json!(t);
116 }
117
118 if let Some(mode) = permission_mode {
119 request_body["permission_mode"] = serde_json::json!(mode);
120 }
121
122 let headers = build_oauth_headers(&access_token, &org_uuid);
123
124 let url = format!("{}/v1/sessions", base_url.unwrap_or(&get_oauth_config()));
125
126 let client = reqwest::Client::new();
127 let response = client
128 .post(&url)
129 .headers(headers)
130 .json(&request_body)
131 .send()
132 .await
133 .ok()?;
134
135 let status = response.status();
136 if status != reqwest::StatusCode::OK && status != reqwest::StatusCode::CREATED {
137 let status_code = status.as_u16();
138 let body = response.text().await.unwrap_or_default();
139 let detail = extract_error_detail_from_text(&body);
140 log_for_debugging(&format!(
141 "[bridge] Session creation failed with status {}{}",
142 status_code,
143 detail.map(|d| format!(": {}", d)).unwrap_or_default()
144 ));
145 return None;
146 }
147
148 let session_data: serde_json::Value = response.json().await.ok()?;
149
150 let session_id = session_data.get("id")?.as_str()?.to_string();
151 Some(session_id)
152}
153
154pub async fn get_bridge_session(
161 session_id: &str,
162 base_url: Option<&str>,
163 get_access_token: Option<&dyn Fn() -> Option<String>>,
164) -> Option<BridgeSessionInfo> {
165 let access_token = get_access_token
167 .and_then(|f| f())
168 .or_else(|| crate::bridge::get_bridge_access_token());
169
170 let access_token = match access_token {
171 Some(t) => t,
172 None => {
173 log_for_debugging("[bridge] No access token for session fetch");
174 return None;
175 }
176 };
177
178 let org_uuid = get_organization_uuid();
180 let org_uuid = match org_uuid {
181 Some(uuid) => uuid,
182 None => {
183 log_for_debugging("[bridge] No org UUID for session fetch");
184 return None;
185 }
186 };
187
188 let headers = build_oauth_headers(&access_token, &org_uuid);
189
190 let url = format!(
191 "{}/v1/sessions/{}",
192 base_url.unwrap_or(&get_oauth_config()),
193 session_id
194 );
195
196 log_for_debugging(&format!("[bridge] Fetching session {}", session_id));
197
198 let client = reqwest::Client::new();
199 let response = client
200 .get(&url)
201 .headers(headers)
202 .timeout(std::time::Duration::from_secs(10))
203 .send()
204 .await
205 .ok()?;
206
207 let status = response.status();
208 if status != reqwest::StatusCode::OK {
209 let status_code = status.as_u16();
210 let body = response.text().await.unwrap_or_default();
211 let detail = extract_error_detail_from_text(&body);
212 log_for_debugging(&format!(
213 "[bridge] Session fetch failed with status {}{}",
214 status_code,
215 detail.map(|d| format!(": {}", d)).unwrap_or_default()
216 ));
217 return None;
218 }
219
220 let data: serde_json::Value = response.json().await.ok()?;
221 Some(BridgeSessionInfo {
222 environment_id: data
223 .get("environment_id")
224 .and_then(|v| v.as_str())
225 .map(String::from),
226 title: data.get("title").and_then(|v| v.as_str()).map(String::from),
227 })
228}
229
230#[derive(Debug, Clone)]
232pub struct BridgeSessionInfo {
233 pub environment_id: Option<String>,
234 pub title: Option<String>,
235}
236
237pub async fn archive_bridge_session(
253 session_id: &str,
254 base_url: Option<&str>,
255 get_access_token: Option<&dyn Fn() -> Option<String>>,
256 timeout_ms: Option<u64>,
257) -> Result<(), String> {
258 let access_token = get_access_token
260 .and_then(|f| f())
261 .or_else(|| crate::bridge::get_bridge_access_token());
262
263 let access_token = match access_token {
264 Some(t) => t,
265 None => {
266 log_for_debugging("[bridge] No access token for session archive");
267 return Err("No access token".to_string());
268 }
269 };
270
271 let org_uuid = get_organization_uuid();
273 let org_uuid = match org_uuid {
274 Some(uuid) => uuid,
275 None => {
276 log_for_debugging("[bridge] No org UUID for session archive");
277 return Err("No org UUID".to_string());
278 }
279 };
280
281 let headers = build_oauth_headers(&access_token, &org_uuid);
282
283 let url = format!(
284 "{}/v1/sessions/{}/archive",
285 base_url.unwrap_or(&get_oauth_config()),
286 session_id
287 );
288
289 log_for_debugging(&format!("[bridge] Archiving session {}", session_id));
290
291 let client = reqwest::Client::new();
292 let response = client
293 .post(&url)
294 .headers(headers)
295 .timeout(std::time::Duration::from_millis(
296 timeout_ms.unwrap_or(10_000),
297 ))
298 .json(&serde_json::json!({}))
299 .send()
300 .await
301 .map_err(|e| format!("Request failed: {}", e))?;
302
303 if response.status() == reqwest::StatusCode::OK {
304 log_for_debugging(&format!(
305 "[bridge] Session {} archived successfully",
306 session_id
307 ));
308 Ok(())
309 } else {
310 let status_code = response.status().as_u16();
311 let body = response.text().await.unwrap_or_default();
312 let detail = extract_error_detail_from_text(&body);
313 Err(format!(
314 "Session archive failed with status {}{}",
315 status_code,
316 detail.map(|d| format!(": {}", d)).unwrap_or_default()
317 ))
318 }
319}
320
321pub async fn update_bridge_session_title(
328 session_id: &str,
329 title: &str,
330 base_url: Option<&str>,
331 get_access_token: Option<&dyn Fn() -> Option<String>>,
332) {
333 let access_token = get_access_token
335 .and_then(|f| f())
336 .or_else(|| crate::bridge::get_bridge_access_token());
337
338 let access_token = match access_token {
339 Some(t) => t,
340 None => {
341 log_for_debugging("[bridge] No access token for session title update");
342 return;
343 }
344 };
345
346 let org_uuid = get_organization_uuid();
348 let org_uuid = match org_uuid {
349 Some(uuid) => uuid,
350 None => {
351 log_for_debugging("[bridge] No org UUID for session title update");
352 return;
353 }
354 };
355
356 let headers = build_oauth_headers(&access_token, &org_uuid);
357
358 let compat_id = crate::bridge::to_compat_session_id(session_id);
362
363 let url = format!(
364 "{}/v1/sessions/{}",
365 base_url.unwrap_or(&get_oauth_config()),
366 compat_id
367 );
368
369 log_for_debugging(&format!(
370 "[bridge] Updating session title: {} → {}",
371 compat_id, title
372 ));
373
374 let client = reqwest::Client::new();
375 match client
376 .patch(&url)
377 .headers(headers)
378 .timeout(std::time::Duration::from_secs(10))
379 .json(&serde_json::json!({ "title": title }))
380 .send()
381 .await
382 {
383 Ok(response) => {
384 if response.status() == reqwest::StatusCode::OK {
385 log_for_debugging("[bridge] Session title updated successfully");
386 } else {
387 let status_code = response.status().as_u16();
388 let body = response.text().await.unwrap_or_default();
389 let detail = extract_error_detail_from_text(&body);
390 log_for_debugging(&format!(
391 "[bridge] Session title update failed with status {}{}",
392 status_code,
393 detail.map(|d| format!(": {}", d)).unwrap_or_default()
394 ));
395 }
396 }
397 Err(e) => {
398 log_for_debugging(&format!(
399 "[bridge] Session title update request failed: {}",
400 e
401 ));
402 }
403 }
404}
405
406fn build_git_context(
412 git_repo_url: &str,
413 branch: Option<&str>,
414) -> (Option<GitSource>, Option<GitOutcome>) {
415 let parsed = parse_git_remote(git_repo_url);
417
418 if let Some((host, owner, name)) = parsed {
419 let revision = branch.map(String::from).or_else(get_default_branch);
420 let source = GitSource {
421 source_type: "git_repository".to_string(),
422 url: format!("https://{}/{}/{}", host, owner, name),
423 revision,
424 };
425 let outcome = GitOutcome {
426 outcome_type: "git_repository".to_string(),
427 git_info: GitInfo {
428 info_type: "github".to_string(),
429 repo: format!("{}/{}", owner, name),
430 branches: vec![format!("claude/{}", branch.unwrap_or("task"))],
431 },
432 };
433 (Some(source), Some(outcome))
434 } else {
435 if let Some((owner, name)) = parse_github_repository(git_repo_url) {
437 let revision = branch.map(String::from).or_else(get_default_branch);
438 let source = GitSource {
439 source_type: "git_repository".to_string(),
440 url: format!("https://github.com/{}/{}", owner, name),
441 revision,
442 };
443 let outcome = GitOutcome {
444 outcome_type: "git_repository".to_string(),
445 git_info: GitInfo {
446 info_type: "github".to_string(),
447 repo: format!("{}/{}", owner, name),
448 branches: vec![format!("claude/{}", branch.unwrap_or("task"))],
449 },
450 };
451 (Some(source), Some(outcome))
452 } else {
453 (None, None)
454 }
455 }
456}
457
458fn parse_git_remote(url: &str) -> Option<(String, String, String)> {
460 let url = url.trim_end_matches(".git");
463
464 let parts: Vec<&str> = url.split('/').collect();
465 if parts.len() >= 3 {
466 let start = if parts[0] == "https:" || parts[0] == "http:" {
468 2
469 } else {
470 0
471 };
472 if parts.len() >= start + 3 {
473 let host = if start == 2 {
474 parts[1].to_string()
475 } else {
476 "github.com".to_string()
477 };
478 let owner = parts[start].to_string();
479 let name = parts[start + 1].to_string();
480 return Some((host, owner, name));
481 }
482 }
483 None
484}
485
486fn parse_github_repository(s: &str) -> Option<(String, String)> {
488 let parts: Vec<&str> = s.split('/').collect();
489 if parts.len() >= 2 {
490 Some((parts[0].to_string(), parts[1].to_string()))
491 } else {
492 None
493 }
494}
495
496fn get_default_branch() -> Option<String> {
498 use std::process::Command;
499 let output = Command::new("git")
500 .args(&["rev-parse", "--abbrev-ref", "HEAD"])
501 .output()
502 .ok()?;
503
504 if output.status.success() {
505 Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
506 } else {
507 None
508 }
509}
510
511fn get_main_loop_model() -> Option<String> {
513 None
515}
516
517fn get_organization_uuid() -> Option<String> {
519 None
521}
522
523struct OAuthConfig {
525 BASE_API_URL: String,
526}
527
528fn get_oauth_config() -> String {
529 std::env::var(ai::API_BASE_URL).unwrap_or_else(|_| "https://api.claude.ai".to_string())
530}
531
532fn build_oauth_headers(access_token: &str, org_uuid: &str) -> HeaderMap {
534 let mut headers = HeaderMap::new();
535 if let Ok(val) = HeaderValue::from_str(&format!("Bearer {}", access_token)) {
536 headers.insert(AUTHORIZATION, val);
537 }
538 headers.insert(
539 HeaderName::from_static("anthropic-beta"),
540 HeaderValue::from_static("ccr-byoc-2025-07-29"),
541 );
542 if let Ok(val) = HeaderValue::from_str(org_uuid) {
543 headers.insert(HeaderName::from_static("x-organization-uuid"), val);
544 }
545 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
547 headers.insert("User-Agent", get_user_agent().parse().unwrap());
548 headers
549}
550
551fn extract_error_detail_from_text(body: &str) -> Option<String> {
553 let data: serde_json::Value = serde_json::from_str(body).ok()?;
554 data.get("message")
555 .and_then(|m| m.as_str())
556 .map(|s| s.to_string())
557}
558
559#[allow(unused_variables)]
561fn log_for_debugging(msg: &str) {
562 eprintln!("{}", msg);
563}