Skip to main content

lean_ctx/
cloud_client.rs

1use std::path::PathBuf;
2
3fn config_dir() -> PathBuf {
4    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
5    home.join(".lean-ctx").join("cloud")
6}
7
8fn credentials_path() -> PathBuf {
9    config_dir().join("credentials.json")
10}
11
12pub fn api_url() -> String {
13    std::env::var("LEAN_CTX_API_URL").unwrap_or_else(|_| "https://api.leanctx.com".to_string())
14}
15
16#[derive(serde::Serialize, serde::Deserialize)]
17struct Credentials {
18    api_key: String,
19    user_id: String,
20    email: String,
21}
22
23pub fn save_credentials(api_key: &str, user_id: &str, email: &str) -> std::io::Result<()> {
24    let dir = config_dir();
25    std::fs::create_dir_all(&dir)?;
26    let creds = Credentials {
27        api_key: api_key.to_string(),
28        user_id: user_id.to_string(),
29        email: email.to_string(),
30    };
31    let json = serde_json::to_string_pretty(&creds).map_err(std::io::Error::other)?;
32    std::fs::write(credentials_path(), json)
33}
34
35pub fn load_api_key() -> Option<String> {
36    let data = std::fs::read_to_string(credentials_path()).ok()?;
37    let creds: Credentials = serde_json::from_str(&data).ok()?;
38    Some(creds.api_key)
39}
40
41pub fn is_logged_in() -> bool {
42    load_api_key().is_some()
43}
44
45pub fn register(email: &str) -> Result<(String, String), String> {
46    let url = format!("{}/api/auth/register", api_url());
47    let body = serde_json::json!({ "email": email });
48
49    let resp = ureq::post(&url)
50        .header("Content-Type", "application/json")
51        .send(serde_json::to_vec(&body).unwrap().as_slice())
52        .map_err(|e| format!("Request failed: {e}"))?;
53
54    let resp_body = resp
55        .into_body()
56        .read_to_string()
57        .map_err(|e| format!("Failed to read response: {e}"))?;
58
59    let json: serde_json::Value =
60        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
61
62    let api_key = json["api_key"]
63        .as_str()
64        .ok_or("Missing api_key in response")?
65        .to_string();
66    let user_id = json["user_id"]
67        .as_str()
68        .ok_or("Missing user_id in response")?
69        .to_string();
70
71    Ok((api_key, user_id))
72}
73
74pub fn sync_stats(stats: &[serde_json::Value]) -> Result<String, String> {
75    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
76    let url = format!("{}/api/stats", api_url());
77
78    let body = serde_json::json!({ "stats": stats });
79
80    let resp = ureq::post(&url)
81        .header("Authorization", &format!("Bearer {api_key}"))
82        .header("Content-Type", "application/json")
83        .send(serde_json::to_vec(&body).unwrap().as_slice())
84        .map_err(|e| format!("Sync failed: {e}"))?;
85
86    let resp_body = resp
87        .into_body()
88        .read_to_string()
89        .map_err(|e| format!("Failed to read response: {e}"))?;
90
91    let json: serde_json::Value =
92        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
93
94    Ok(json["message"].as_str().unwrap_or("Synced").to_string())
95}
96
97pub fn contribute(entries: &[serde_json::Value]) -> Result<String, String> {
98    let url = format!("{}/api/contribute", api_url());
99
100    let body = serde_json::json!({ "entries": entries });
101
102    let resp = ureq::post(&url)
103        .header("Content-Type", "application/json")
104        .send(serde_json::to_vec(&body).unwrap().as_slice())
105        .map_err(|e| format!("Contribute failed: {e}"))?;
106
107    let resp_body = resp
108        .into_body()
109        .read_to_string()
110        .map_err(|e| format!("Failed to read response: {e}"))?;
111
112    let json: serde_json::Value =
113        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
114
115    Ok(json["message"]
116        .as_str()
117        .unwrap_or("Contributed")
118        .to_string())
119}
120
121pub fn push_knowledge(entries: &[serde_json::Value]) -> Result<String, String> {
122    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
123    let url = format!("{}/api/sync/knowledge", api_url());
124
125    let body = serde_json::json!({ "entries": entries });
126
127    let resp = ureq::post(&url)
128        .header("Authorization", &format!("Bearer {api_key}"))
129        .header("Content-Type", "application/json")
130        .send(serde_json::to_vec(&body).unwrap().as_slice())
131        .map_err(|e| format!("Push failed: {e}"))?;
132
133    let resp_body = resp
134        .into_body()
135        .read_to_string()
136        .map_err(|e| format!("Failed to read response: {e}"))?;
137
138    let json: serde_json::Value =
139        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
140
141    Ok(format!(
142        "{} entries synced",
143        json["synced"].as_i64().unwrap_or(0)
144    ))
145}
146
147pub fn pull_pro_models() -> Result<serde_json::Value, String> {
148    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login <email>")?;
149    let url = format!("{}/api/pro/models", api_url());
150
151    let resp = ureq::get(&url)
152        .header("Authorization", &format!("Bearer {api_key}"))
153        .call()
154        .map_err(|e| {
155            let msg = e.to_string();
156            if msg.contains("403") {
157                "This feature is not available for your account.".to_string()
158            } else {
159                format!("Connection failed. Check your internet connection. ({e})")
160            }
161        })?;
162
163    let resp_body = resp
164        .into_body()
165        .read_to_string()
166        .map_err(|e| format!("Failed to read response: {e}"))?;
167
168    serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))
169}
170
171pub fn save_pro_models(data: &serde_json::Value) -> std::io::Result<()> {
172    let dir = config_dir();
173    std::fs::create_dir_all(&dir)?;
174    let json = serde_json::to_string_pretty(data).map_err(std::io::Error::other)?;
175    std::fs::write(dir.join("pro_models.json"), json)
176}
177
178pub fn load_pro_models() -> Option<serde_json::Value> {
179    let path = config_dir().join("pro_models.json");
180    let data = std::fs::read_to_string(path).ok()?;
181    serde_json::from_str(&data).ok()
182}
183
184pub fn check_pro() -> bool {
185    let path = config_dir().join("plan.txt");
186    std::fs::read_to_string(path)
187        .map(|p| p.trim() == "pro")
188        .unwrap_or(false)
189}
190
191pub fn save_plan(plan: &str) -> std::io::Result<()> {
192    let dir = config_dir();
193    std::fs::create_dir_all(&dir)?;
194    std::fs::write(dir.join("plan.txt"), plan)
195}
196
197pub fn fetch_plan() -> Result<String, String> {
198    let api_key = load_api_key().ok_or("Not logged in")?;
199    let url = format!("{}/api/auth/me", api_url());
200
201    let resp = ureq::get(&url)
202        .header("Authorization", &format!("Bearer {api_key}"))
203        .call()
204        .map_err(|e| format!("Failed to check plan: {e}"))?;
205
206    let resp_body = resp
207        .into_body()
208        .read_to_string()
209        .map_err(|e| format!("Failed to read response: {e}"))?;
210
211    let json: serde_json::Value =
212        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))?;
213
214    Ok(json["plan"].as_str().unwrap_or("free").to_string())
215}
216
217pub fn pull_knowledge() -> Result<Vec<serde_json::Value>, String> {
218    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
219    let url = format!("{}/api/sync/knowledge", api_url());
220
221    let resp = ureq::get(&url)
222        .header("Authorization", &format!("Bearer {api_key}"))
223        .call()
224        .map_err(|e| format!("Pull failed: {e}"))?;
225
226    let resp_body = resp
227        .into_body()
228        .read_to_string()
229        .map_err(|e| format!("Failed to read response: {e}"))?;
230
231    let entries: Vec<serde_json::Value> =
232        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
233
234    Ok(entries)
235}