Skip to main content

bctx_cloud_core/client/
sync.rs

1#[cfg(not(feature = "cloud-server"))]
2use anyhow::bail;
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use vault::fact::MemoFact;
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct SyncPushPayload {
9    pub project_hash: String,
10    pub facts: Vec<MemoFact>,
11    pub client_version: String,
12}
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct SyncPullResponse {
16    pub facts: Vec<MemoFact>,
17    pub server_version: u64,
18}
19
20#[derive(Debug, Serialize, Deserialize)]
21pub struct SyncPushResponse {
22    pub accepted: usize,
23    pub merged: usize,
24    pub server_version: u64,
25}
26
27/// Push CrystallizedKnowledge facts to the cloud for the given project.
28#[cfg(feature = "cloud-server")]
29pub async fn push_vault(
30    endpoint: &str,
31    token: &str,
32    project_hash: &str,
33    facts: Vec<MemoFact>,
34) -> Result<SyncPushResponse> {
35    let payload = SyncPushPayload {
36        project_hash: project_hash.to_string(),
37        facts,
38        client_version: env!("CARGO_PKG_VERSION").to_string(),
39    };
40    let resp = reqwest::Client::builder()
41        .timeout(std::time::Duration::from_secs(15))
42        .connect_timeout(std::time::Duration::from_secs(8))
43        .build()?
44        .post(format!("{endpoint}/sync/vault"))
45        .bearer_auth(token)
46        .json(&payload)
47        .send()
48        .await?
49        .error_for_status()?
50        .json::<SyncPushResponse>()
51        .await?;
52    Ok(resp)
53}
54
55#[cfg(not(feature = "cloud-server"))]
56pub async fn push_vault(
57    _endpoint: &str,
58    _token: &str,
59    _project_hash: &str,
60    _facts: Vec<MemoFact>,
61) -> Result<SyncPushResponse> {
62    bail!("cloud-server feature not enabled")
63}
64
65/// Pull CrystallizedKnowledge facts from the cloud for the given project.
66#[cfg(feature = "cloud-server")]
67pub async fn pull_vault(
68    endpoint: &str,
69    token: &str,
70    project_hash: &str,
71) -> Result<SyncPullResponse> {
72    let resp = reqwest::Client::builder()
73        .timeout(std::time::Duration::from_secs(15))
74        .connect_timeout(std::time::Duration::from_secs(8))
75        .build()?
76        .get(format!("{endpoint}/sync/vault/{project_hash}"))
77        .bearer_auth(token)
78        .send()
79        .await?
80        .error_for_status()?
81        .json::<SyncPullResponse>()
82        .await?;
83    Ok(resp)
84}
85
86#[cfg(not(feature = "cloud-server"))]
87pub async fn pull_vault(
88    _endpoint: &str,
89    _token: &str,
90    _project_hash: &str,
91) -> Result<SyncPullResponse> {
92    bail!("cloud-server feature not enabled")
93}
94
95/// Record token savings on the cloud server.
96/// Returns a thread handle — caller must join before process::exit.
97/// Errors are silently ignored so they never interrupt the CLI.
98#[cfg(feature = "cloud-server")]
99pub fn push_signals_bg(
100    endpoint: String,
101    token: String,
102    tokens_sent: i64,
103    tokens_saved: i64,
104    skill: String,
105) -> std::thread::JoinHandle<()> {
106    std::thread::spawn(move || {
107        let rt = tokio::runtime::Builder::new_current_thread()
108            .enable_all()
109            .build();
110        if let Ok(rt) = rt {
111            let _ = rt.block_on(async {
112                let client = reqwest::Client::builder()
113                    .timeout(std::time::Duration::from_secs(8))
114                    .connect_timeout(std::time::Duration::from_secs(5))
115                    .build()
116                    .unwrap_or_default();
117                client
118                    .post(format!("{endpoint}/sync/signals"))
119                    .bearer_auth(&token)
120                    .json(&serde_json::json!({
121                        "tokens_sent": tokens_sent,
122                        "tokens_saved": tokens_saved,
123                        "skill": skill,
124                    }))
125                    .send()
126                    .await
127            });
128        }
129    })
130}
131
132#[cfg(not(feature = "cloud-server"))]
133pub fn push_signals_bg(
134    _endpoint: String,
135    _token: String,
136    _tokens_sent: i64,
137    _tokens_saved: i64,
138    _skill: String,
139) -> std::thread::JoinHandle<()> {
140    std::thread::spawn(|| {})
141}