Skip to main content

lean_ctx/
cloud_client.rs

1use std::path::PathBuf;
2
3fn config_dir() -> PathBuf {
4    if let Ok(dir) = std::env::var("LEAN_CTX_DATA_DIR") {
5        return PathBuf::from(dir).join("cloud");
6    }
7    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
8    home.join(".lean-ctx").join("cloud")
9}
10
11fn credentials_path() -> PathBuf {
12    config_dir().join("credentials.json")
13}
14
15pub fn api_url() -> String {
16    std::env::var("LEAN_CTX_API_URL").unwrap_or_else(|_| "https://api.leanctx.com".to_string())
17}
18
19#[derive(serde::Serialize, serde::Deserialize)]
20struct Credentials {
21    api_key: String,
22    user_id: String,
23    email: String,
24}
25
26pub fn save_credentials(api_key: &str, user_id: &str, email: &str) -> std::io::Result<()> {
27    let dir = config_dir();
28    std::fs::create_dir_all(&dir)?;
29    let creds = Credentials {
30        api_key: api_key.to_string(),
31        user_id: user_id.to_string(),
32        email: email.to_string(),
33    };
34    let json = serde_json::to_string_pretty(&creds).map_err(std::io::Error::other)?;
35    std::fs::write(credentials_path(), json)
36}
37
38pub fn load_api_key() -> Option<String> {
39    let data = std::fs::read_to_string(credentials_path()).ok()?;
40    let creds: Credentials = serde_json::from_str(&data).ok()?;
41    Some(creds.api_key)
42}
43
44pub fn is_logged_in() -> bool {
45    load_api_key().is_some()
46}
47
48pub struct RegisterResult {
49    pub api_key: String,
50    pub user_id: String,
51    pub email_verified: bool,
52    pub verification_sent: bool,
53}
54
55pub fn register(email: &str, password: Option<&str>) -> Result<RegisterResult, String> {
56    let url = format!("{}/api/auth/register", api_url());
57    let mut body = serde_json::json!({ "email": email });
58    if let Some(pw) = password {
59        body["password"] = serde_json::Value::String(pw.to_string());
60    }
61
62    let resp = ureq::post(&url)
63        .header("Content-Type", "application/json")
64        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
65        .map_err(|e| format!("Request failed: {e}"))?;
66
67    let resp_body = resp
68        .into_body()
69        .read_to_string()
70        .map_err(|e| format!("Failed to read response: {e}"))?;
71
72    let json: serde_json::Value =
73        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
74
75    Ok(RegisterResult {
76        api_key: json["api_key"]
77            .as_str()
78            .ok_or("Missing api_key in response")?
79            .to_string(),
80        user_id: json["user_id"]
81            .as_str()
82            .ok_or("Missing user_id in response")?
83            .to_string(),
84        email_verified: json["email_verified"].as_bool().unwrap_or(false),
85        verification_sent: json["verification_sent"].as_bool().unwrap_or(false),
86    })
87}
88
89pub fn forgot_password(email: &str) -> Result<String, String> {
90    let url = format!("{}/api/auth/forgot-password", api_url());
91    let body = serde_json::json!({ "email": email });
92
93    let resp = ureq::post(&url)
94        .header("Content-Type", "application/json")
95        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
96        .map_err(|e| format!("Request failed: {e}"))?;
97
98    let resp_body = resp
99        .into_body()
100        .read_to_string()
101        .map_err(|e| format!("Failed to read response: {e}"))?;
102
103    let json: serde_json::Value =
104        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
105
106    Ok(json["message"]
107        .as_str()
108        .unwrap_or("If an account exists, a reset email has been sent.")
109        .to_string())
110}
111
112pub fn login(email: &str, password: &str) -> Result<RegisterResult, String> {
113    let url = format!("{}/api/auth/login", api_url());
114    let body = serde_json::json!({ "email": email, "password": password });
115
116    let resp = ureq::post(&url)
117        .header("Content-Type", "application/json")
118        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
119        .map_err(|e| {
120            let msg = e.to_string();
121            if msg.contains("401") {
122                "Invalid email or password".to_string()
123            } else {
124                format!("Request failed: {e}")
125            }
126        })?;
127
128    let resp_body = resp
129        .into_body()
130        .read_to_string()
131        .map_err(|e| format!("Failed to read response: {e}"))?;
132
133    let json: serde_json::Value =
134        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
135
136    Ok(RegisterResult {
137        api_key: json["api_key"]
138            .as_str()
139            .ok_or("Missing api_key in response")?
140            .to_string(),
141        user_id: json["user_id"]
142            .as_str()
143            .ok_or("Missing user_id in response")?
144            .to_string(),
145        email_verified: json["email_verified"].as_bool().unwrap_or(false),
146        verification_sent: false,
147    })
148}
149
150pub fn sync_stats(stats: &[serde_json::Value]) -> Result<String, String> {
151    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
152    let url = format!("{}/api/stats", api_url());
153
154    let body = serde_json::json!({ "stats": stats });
155
156    let resp = ureq::post(&url)
157        .header("Authorization", &format!("Bearer {api_key}"))
158        .header("Content-Type", "application/json")
159        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
160        .map_err(|e| format!("Sync failed: {e}"))?;
161
162    let resp_body = resp
163        .into_body()
164        .read_to_string()
165        .map_err(|e| format!("Failed to read response: {e}"))?;
166
167    let json: serde_json::Value =
168        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
169
170    Ok(json["message"].as_str().unwrap_or("Synced").to_string())
171}
172
173pub fn contribute(entries: &[serde_json::Value]) -> Result<String, String> {
174    let url = format!("{}/api/contribute", api_url());
175
176    let body = serde_json::json!({ "entries": entries });
177
178    let resp = ureq::post(&url)
179        .header("Content-Type", "application/json")
180        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
181        .map_err(|e| format!("Contribute failed: {e}"))?;
182
183    let resp_body = resp
184        .into_body()
185        .read_to_string()
186        .map_err(|e| format!("Failed to read response: {e}"))?;
187
188    let json: serde_json::Value =
189        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
190
191    Ok(json["message"]
192        .as_str()
193        .unwrap_or("Contributed")
194        .to_string())
195}
196
197pub fn push_knowledge(entries: &[serde_json::Value]) -> Result<String, String> {
198    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
199    let url = format!("{}/api/sync/knowledge", api_url());
200
201    let body = serde_json::json!({ "entries": entries });
202
203    let resp = ureq::post(&url)
204        .header("Authorization", &format!("Bearer {api_key}"))
205        .header("Content-Type", "application/json")
206        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
207        .map_err(|e| format!("Push failed: {e}"))?;
208
209    let resp_body = resp
210        .into_body()
211        .read_to_string()
212        .map_err(|e| format!("Failed to read response: {e}"))?;
213
214    let json: serde_json::Value =
215        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
216
217    Ok(format!(
218        "{} entries synced",
219        json["synced"].as_i64().unwrap_or(0)
220    ))
221}
222
223pub fn pull_cloud_models() -> Result<serde_json::Value, String> {
224    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login <email>")?;
225    let url = format!("{}/api/cloud/models", api_url());
226
227    let resp = ureq::get(&url)
228        .header("Authorization", &format!("Bearer {api_key}"))
229        .call()
230        .map_err(|e| {
231            let msg = e.to_string();
232            if msg.contains("403") {
233                "This feature is not available for your account.".to_string()
234            } else {
235                format!("Connection failed. Check your internet connection. ({e})")
236            }
237        })?;
238
239    let resp_body = resp
240        .into_body()
241        .read_to_string()
242        .map_err(|e| format!("Failed to read response: {e}"))?;
243
244    serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))
245}
246
247pub fn save_cloud_models(data: &serde_json::Value) -> std::io::Result<()> {
248    let dir = config_dir();
249    std::fs::create_dir_all(&dir)?;
250    let json = serde_json::to_string_pretty(data).map_err(std::io::Error::other)?;
251    std::fs::write(dir.join("cloud_models.json"), json)
252}
253
254pub fn load_cloud_models() -> Option<serde_json::Value> {
255    let path = config_dir().join("cloud_models.json");
256    let data = std::fs::read_to_string(path).ok()?;
257    serde_json::from_str(&data).ok()
258}
259
260pub fn is_cloud_user() -> bool {
261    let path = config_dir().join("plan.txt");
262    std::fs::read_to_string(path).is_ok_and(|p| matches!(p.trim(), "cloud" | "pro"))
263}
264
265pub fn save_plan(plan: &str) -> std::io::Result<()> {
266    let dir = config_dir();
267    std::fs::create_dir_all(&dir)?;
268    std::fs::write(dir.join("plan.txt"), plan)
269}
270
271pub fn fetch_plan() -> Result<String, String> {
272    let api_key = load_api_key().ok_or("Not logged in")?;
273    let url = format!("{}/api/auth/me", api_url());
274
275    let resp = ureq::get(&url)
276        .header("Authorization", &format!("Bearer {api_key}"))
277        .call()
278        .map_err(|e| format!("Failed to check plan: {e}"))?;
279
280    let resp_body = resp
281        .into_body()
282        .read_to_string()
283        .map_err(|e| format!("Failed to read response: {e}"))?;
284
285    let json: serde_json::Value =
286        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))?;
287
288    Ok(json["plan"].as_str().unwrap_or("free").to_string())
289}
290
291pub fn push_commands(entries: &[serde_json::Value]) -> Result<String, String> {
292    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
293    let url = format!("{}/api/sync/commands", api_url());
294    let body = serde_json::json!({ "commands": entries });
295    let resp = ureq::post(&url)
296        .header("Authorization", &format!("Bearer {api_key}"))
297        .header("Content-Type", "application/json")
298        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
299        .map_err(|e| format!("Push failed: {e}"))?;
300    let resp_body = resp
301        .into_body()
302        .read_to_string()
303        .map_err(|e| format!("Failed to read response: {e}"))?;
304    let json: serde_json::Value =
305        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
306    Ok(format!(
307        "{} commands synced",
308        json["synced"].as_i64().unwrap_or(0)
309    ))
310}
311
312pub fn push_cep(entries: &[serde_json::Value]) -> Result<String, String> {
313    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
314    let url = format!("{}/api/sync/cep", api_url());
315    let body = serde_json::json!({ "scores": entries });
316    let resp = ureq::post(&url)
317        .header("Authorization", &format!("Bearer {api_key}"))
318        .header("Content-Type", "application/json")
319        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
320        .map_err(|e| format!("Push failed: {e}"))?;
321    let resp_body = resp
322        .into_body()
323        .read_to_string()
324        .map_err(|e| format!("Failed to read response: {e}"))?;
325    let json: serde_json::Value =
326        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
327    Ok(format!(
328        "{} sessions synced",
329        json["synced"].as_i64().unwrap_or(0)
330    ))
331}
332
333pub fn push_gain(entries: &[serde_json::Value]) -> Result<String, String> {
334    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
335    let url = format!("{}/api/sync/gain", api_url());
336    let body = serde_json::json!({ "scores": entries });
337    let resp = ureq::post(&url)
338        .header("Authorization", &format!("Bearer {api_key}"))
339        .header("Content-Type", "application/json")
340        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
341        .map_err(|e| format!("Push failed: {e}"))?;
342    let resp_body = resp
343        .into_body()
344        .read_to_string()
345        .map_err(|e| format!("Failed to read response: {e}"))?;
346    let json: serde_json::Value =
347        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
348    Ok(format!(
349        "{} gain scores synced",
350        json["synced"].as_i64().unwrap_or(0)
351    ))
352}
353
354pub fn push_gotchas(entries: &[serde_json::Value]) -> Result<String, String> {
355    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
356    let url = format!("{}/api/sync/gotchas", api_url());
357    let body = serde_json::json!({ "gotchas": entries });
358    let resp = ureq::post(&url)
359        .header("Authorization", &format!("Bearer {api_key}"))
360        .header("Content-Type", "application/json")
361        .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
362        .map_err(|e| format!("Push failed: {e}"))?;
363    let resp_body = resp
364        .into_body()
365        .read_to_string()
366        .map_err(|e| format!("Failed to read response: {e}"))?;
367    let json: serde_json::Value =
368        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
369    Ok(format!(
370        "{} gotchas synced",
371        json["synced"].as_i64().unwrap_or(0)
372    ))
373}
374
375pub fn push_buddy(data: &serde_json::Value) -> Result<String, String> {
376    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
377    let url = format!("{}/api/sync/buddy", api_url());
378    let resp = ureq::post(&url)
379        .header("Authorization", &format!("Bearer {api_key}"))
380        .header("Content-Type", "application/json")
381        .send(&serde_json::to_vec(data).map_err(|e| format!("JSON error: {e}"))?)
382        .map_err(|e| format!("Push failed: {e}"))?;
383    let resp_body = resp
384        .into_body()
385        .read_to_string()
386        .map_err(|e| format!("Failed to read response: {e}"))?;
387    let _json: serde_json::Value =
388        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
389    Ok("Buddy synced".to_string())
390}
391
392pub fn push_feedback(entries: &[serde_json::Value]) -> Result<String, String> {
393    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
394    let url = format!("{}/api/sync/feedback", api_url());
395    let resp = ureq::post(&url)
396        .header("Authorization", &format!("Bearer {api_key}"))
397        .header("Content-Type", "application/json")
398        .send(&serde_json::to_vec(entries).map_err(|e| format!("JSON error: {e}"))?)
399        .map_err(|e| format!("Push failed: {e}"))?;
400    let resp_body = resp
401        .into_body()
402        .read_to_string()
403        .map_err(|e| format!("Failed to read response: {e}"))?;
404    let json: serde_json::Value =
405        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
406    Ok(format!(
407        "{} thresholds synced",
408        json["synced"].as_i64().unwrap_or(0)
409    ))
410}
411
412pub fn pull_knowledge() -> Result<Vec<serde_json::Value>, String> {
413    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
414    let url = format!("{}/api/sync/knowledge", api_url());
415
416    let resp = ureq::get(&url)
417        .header("Authorization", &format!("Bearer {api_key}"))
418        .call()
419        .map_err(|e| format!("Pull failed: {e}"))?;
420
421    let resp_body = resp
422        .into_body()
423        .read_to_string()
424        .map_err(|e| format!("Failed to read response: {e}"))?;
425
426    let entries: Vec<serde_json::Value> =
427        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
428
429    Ok(entries)
430}