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