bctx-cloud-core 0.1.4

bctx-cloud-core — cloud client and server for Vault sync, dashboard API, billing
Documentation
#[cfg(not(feature = "cloud-server"))]
use anyhow::bail;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use vault::fact::MemoFact;

#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPushPayload {
    pub project_hash: String,
    pub facts: Vec<MemoFact>,
    pub client_version: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPullResponse {
    pub facts: Vec<MemoFact>,
    pub server_version: u64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPushResponse {
    pub accepted: usize,
    pub merged: usize,
    pub server_version: u64,
}

/// Push CrystallizedKnowledge facts to the cloud for the given project.
#[cfg(feature = "cloud-server")]
pub async fn push_vault(
    endpoint: &str,
    token: &str,
    project_hash: &str,
    facts: Vec<MemoFact>,
) -> Result<SyncPushResponse> {
    let payload = SyncPushPayload {
        project_hash: project_hash.to_string(),
        facts,
        client_version: env!("CARGO_PKG_VERSION").to_string(),
    };
    let resp = reqwest::Client::new()
        .post(format!("{endpoint}/sync/vault"))
        .bearer_auth(token)
        .json(&payload)
        .send()
        .await?
        .error_for_status()?
        .json::<SyncPushResponse>()
        .await?;
    Ok(resp)
}

#[cfg(not(feature = "cloud-server"))]
pub async fn push_vault(
    _endpoint: &str,
    _token: &str,
    _project_hash: &str,
    _facts: Vec<MemoFact>,
) -> Result<SyncPushResponse> {
    bail!("cloud-server feature not enabled")
}

/// Pull CrystallizedKnowledge facts from the cloud for the given project.
#[cfg(feature = "cloud-server")]
pub async fn pull_vault(
    endpoint: &str,
    token: &str,
    project_hash: &str,
) -> Result<SyncPullResponse> {
    let resp = reqwest::Client::new()
        .get(format!("{endpoint}/sync/vault/{project_hash}"))
        .bearer_auth(token)
        .send()
        .await?
        .error_for_status()?
        .json::<SyncPullResponse>()
        .await?;
    Ok(resp)
}

#[cfg(not(feature = "cloud-server"))]
pub async fn pull_vault(
    _endpoint: &str,
    _token: &str,
    _project_hash: &str,
) -> Result<SyncPullResponse> {
    bail!("cloud-server feature not enabled")
}

/// Record token savings on the cloud server.
/// Returns a thread handle — caller must join before process::exit.
/// Errors are silently ignored so they never interrupt the CLI.
#[cfg(feature = "cloud-server")]
pub fn push_signals_bg(
    endpoint: String,
    token: String,
    tokens_sent: i64,
    tokens_saved: i64,
    skill: String,
) -> std::thread::JoinHandle<()> {
    std::thread::spawn(move || {
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build();
        if let Ok(rt) = rt {
            let _ = rt.block_on(async {
                reqwest::Client::new()
                    .post(format!("{endpoint}/sync/signals"))
                    .bearer_auth(&token)
                    .json(&serde_json::json!({
                        "tokens_sent": tokens_sent,
                        "tokens_saved": tokens_saved,
                        "skill": skill,
                    }))
                    .send()
                    .await
            });
        }
    })
}

#[cfg(not(feature = "cloud-server"))]
pub fn push_signals_bg(
    _endpoint: String,
    _token: String,
    _tokens_sent: i64,
    _tokens_saved: i64,
    _skill: String,
) -> std::thread::JoinHandle<()> {
    std::thread::spawn(|| {})
}