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::new()
41        .post(format!("{endpoint}/sync/vault"))
42        .bearer_auth(token)
43        .json(&payload)
44        .send()
45        .await?
46        .error_for_status()?
47        .json::<SyncPushResponse>()
48        .await?;
49    Ok(resp)
50}
51
52#[cfg(not(feature = "cloud-server"))]
53pub async fn push_vault(
54    _endpoint: &str,
55    _token: &str,
56    _project_hash: &str,
57    _facts: Vec<MemoFact>,
58) -> Result<SyncPushResponse> {
59    bail!("cloud-server feature not enabled")
60}
61
62/// Pull CrystallizedKnowledge facts from the cloud for the given project.
63#[cfg(feature = "cloud-server")]
64pub async fn pull_vault(
65    endpoint: &str,
66    token: &str,
67    project_hash: &str,
68) -> Result<SyncPullResponse> {
69    let resp = reqwest::Client::new()
70        .get(format!("{endpoint}/sync/vault/{project_hash}"))
71        .bearer_auth(token)
72        .send()
73        .await?
74        .error_for_status()?
75        .json::<SyncPullResponse>()
76        .await?;
77    Ok(resp)
78}
79
80#[cfg(not(feature = "cloud-server"))]
81pub async fn pull_vault(
82    _endpoint: &str,
83    _token: &str,
84    _project_hash: &str,
85) -> Result<SyncPullResponse> {
86    bail!("cloud-server feature not enabled")
87}
88
89/// Record token savings on the cloud server.
90/// Returns a thread handle — caller must join before process::exit.
91/// Errors are silently ignored so they never interrupt the CLI.
92#[cfg(feature = "cloud-server")]
93pub fn push_signals_bg(
94    endpoint: String,
95    token: String,
96    tokens_sent: i64,
97    tokens_saved: i64,
98    skill: String,
99) -> std::thread::JoinHandle<()> {
100    std::thread::spawn(move || {
101        let rt = tokio::runtime::Builder::new_current_thread()
102            .enable_all()
103            .build();
104        if let Ok(rt) = rt {
105            let _ = rt.block_on(async {
106                reqwest::Client::new()
107                    .post(format!("{endpoint}/sync/signals"))
108                    .bearer_auth(&token)
109                    .json(&serde_json::json!({
110                        "tokens_sent": tokens_sent,
111                        "tokens_saved": tokens_saved,
112                        "skill": skill,
113                    }))
114                    .send()
115                    .await
116            });
117        }
118    })
119}
120
121#[cfg(not(feature = "cloud-server"))]
122pub fn push_signals_bg(
123    _endpoint: String,
124    _token: String,
125    _tokens_sent: i64,
126    _tokens_saved: i64,
127    _skill: String,
128) -> std::thread::JoinHandle<()> {
129    std::thread::spawn(|| {})
130}