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 struct RegisterResult {
46    pub api_key: String,
47    pub user_id: String,
48    pub email_verified: bool,
49    pub verification_sent: bool,
50}
51
52pub fn register(email: &str, password: Option<&str>) -> Result<RegisterResult, String> {
53    let url = format!("{}/api/auth/register", api_url());
54    let mut body = serde_json::json!({ "email": email });
55    if let Some(pw) = password {
56        body["password"] = serde_json::Value::String(pw.to_string());
57    }
58
59    let resp = ureq::post(&url)
60        .header("Content-Type", "application/json")
61        .send(serde_json::to_vec(&body).unwrap().as_slice())
62        .map_err(|e| format!("Request failed: {e}"))?;
63
64    let resp_body = resp
65        .into_body()
66        .read_to_string()
67        .map_err(|e| format!("Failed to read response: {e}"))?;
68
69    let json: serde_json::Value =
70        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
71
72    Ok(RegisterResult {
73        api_key: json["api_key"]
74            .as_str()
75            .ok_or("Missing api_key in response")?
76            .to_string(),
77        user_id: json["user_id"]
78            .as_str()
79            .ok_or("Missing user_id in response")?
80            .to_string(),
81        email_verified: json["email_verified"].as_bool().unwrap_or(false),
82        verification_sent: json["verification_sent"].as_bool().unwrap_or(false),
83    })
84}
85
86pub fn login(email: &str, password: &str) -> Result<RegisterResult, String> {
87    let url = format!("{}/api/auth/login", api_url());
88    let body = serde_json::json!({ "email": email, "password": password });
89
90    let resp = ureq::post(&url)
91        .header("Content-Type", "application/json")
92        .send(serde_json::to_vec(&body).unwrap().as_slice())
93        .map_err(|e| {
94            let msg = e.to_string();
95            if msg.contains("401") {
96                "Invalid email or password".to_string()
97            } else {
98                format!("Request failed: {e}")
99            }
100        })?;
101
102    let resp_body = resp
103        .into_body()
104        .read_to_string()
105        .map_err(|e| format!("Failed to read response: {e}"))?;
106
107    let json: serde_json::Value =
108        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
109
110    Ok(RegisterResult {
111        api_key: json["api_key"]
112            .as_str()
113            .ok_or("Missing api_key in response")?
114            .to_string(),
115        user_id: json["user_id"]
116            .as_str()
117            .ok_or("Missing user_id in response")?
118            .to_string(),
119        email_verified: json["email_verified"].as_bool().unwrap_or(false),
120        verification_sent: false,
121    })
122}
123
124pub fn sync_stats(stats: &[serde_json::Value]) -> Result<String, String> {
125    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
126    let url = format!("{}/api/stats", api_url());
127
128    let body = serde_json::json!({ "stats": stats });
129
130    let resp = ureq::post(&url)
131        .header("Authorization", &format!("Bearer {api_key}"))
132        .header("Content-Type", "application/json")
133        .send(serde_json::to_vec(&body).unwrap().as_slice())
134        .map_err(|e| format!("Sync failed: {e}"))?;
135
136    let resp_body = resp
137        .into_body()
138        .read_to_string()
139        .map_err(|e| format!("Failed to read response: {e}"))?;
140
141    let json: serde_json::Value =
142        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
143
144    Ok(json["message"].as_str().unwrap_or("Synced").to_string())
145}
146
147pub fn contribute(entries: &[serde_json::Value]) -> Result<String, String> {
148    let url = format!("{}/api/contribute", api_url());
149
150    let body = serde_json::json!({ "entries": entries });
151
152    let resp = ureq::post(&url)
153        .header("Content-Type", "application/json")
154        .send(serde_json::to_vec(&body).unwrap().as_slice())
155        .map_err(|e| format!("Contribute failed: {e}"))?;
156
157    let resp_body = resp
158        .into_body()
159        .read_to_string()
160        .map_err(|e| format!("Failed to read response: {e}"))?;
161
162    let json: serde_json::Value =
163        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
164
165    Ok(json["message"]
166        .as_str()
167        .unwrap_or("Contributed")
168        .to_string())
169}
170
171pub fn push_knowledge(entries: &[serde_json::Value]) -> Result<String, String> {
172    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
173    let url = format!("{}/api/sync/knowledge", api_url());
174
175    let body = serde_json::json!({ "entries": entries });
176
177    let resp = ureq::post(&url)
178        .header("Authorization", &format!("Bearer {api_key}"))
179        .header("Content-Type", "application/json")
180        .send(serde_json::to_vec(&body).unwrap().as_slice())
181        .map_err(|e| format!("Push 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(format!(
192        "{} entries synced",
193        json["synced"].as_i64().unwrap_or(0)
194    ))
195}
196
197pub fn pull_cloud_models() -> Result<serde_json::Value, String> {
198    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login <email>")?;
199    let url = format!("{}/api/cloud/models", api_url());
200
201    let resp = ureq::get(&url)
202        .header("Authorization", &format!("Bearer {api_key}"))
203        .call()
204        .map_err(|e| {
205            let msg = e.to_string();
206            if msg.contains("403") {
207                "This feature is not available for your account.".to_string()
208            } else {
209                format!("Connection failed. Check your internet connection. ({e})")
210            }
211        })?;
212
213    let resp_body = resp
214        .into_body()
215        .read_to_string()
216        .map_err(|e| format!("Failed to read response: {e}"))?;
217
218    serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))
219}
220
221pub fn save_cloud_models(data: &serde_json::Value) -> std::io::Result<()> {
222    let dir = config_dir();
223    std::fs::create_dir_all(&dir)?;
224    let json = serde_json::to_string_pretty(data).map_err(std::io::Error::other)?;
225    std::fs::write(dir.join("cloud_models.json"), json)
226}
227
228pub fn load_cloud_models() -> Option<serde_json::Value> {
229    let path = config_dir().join("cloud_models.json");
230    let data = std::fs::read_to_string(path).ok()?;
231    serde_json::from_str(&data).ok()
232}
233
234pub fn is_cloud_user() -> bool {
235    let path = config_dir().join("plan.txt");
236    std::fs::read_to_string(path)
237        .map(|p| matches!(p.trim(), "cloud" | "pro"))
238        .unwrap_or(false)
239}
240
241pub fn save_plan(plan: &str) -> std::io::Result<()> {
242    let dir = config_dir();
243    std::fs::create_dir_all(&dir)?;
244    std::fs::write(dir.join("plan.txt"), plan)
245}
246
247pub fn fetch_plan() -> Result<String, String> {
248    let api_key = load_api_key().ok_or("Not logged in")?;
249    let url = format!("{}/api/auth/me", api_url());
250
251    let resp = ureq::get(&url)
252        .header("Authorization", &format!("Bearer {api_key}"))
253        .call()
254        .map_err(|e| format!("Failed to check plan: {e}"))?;
255
256    let resp_body = resp
257        .into_body()
258        .read_to_string()
259        .map_err(|e| format!("Failed to read response: {e}"))?;
260
261    let json: serde_json::Value =
262        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))?;
263
264    Ok(json["plan"].as_str().unwrap_or("free").to_string())
265}
266
267pub fn push_commands(entries: &[serde_json::Value]) -> Result<String, String> {
268    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
269    let url = format!("{}/api/sync/commands", api_url());
270    let body = serde_json::json!({ "commands": entries });
271    let resp = ureq::post(&url)
272        .header("Authorization", &format!("Bearer {api_key}"))
273        .header("Content-Type", "application/json")
274        .send(serde_json::to_vec(&body).unwrap().as_slice())
275        .map_err(|e| format!("Push failed: {e}"))?;
276    let resp_body = resp
277        .into_body()
278        .read_to_string()
279        .map_err(|e| format!("Failed to read response: {e}"))?;
280    let json: serde_json::Value =
281        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
282    Ok(format!(
283        "{} commands synced",
284        json["synced"].as_i64().unwrap_or(0)
285    ))
286}
287
288pub fn push_cep(entries: &[serde_json::Value]) -> Result<String, String> {
289    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
290    let url = format!("{}/api/sync/cep", api_url());
291    let body = serde_json::json!({ "scores": entries });
292    let resp = ureq::post(&url)
293        .header("Authorization", &format!("Bearer {api_key}"))
294        .header("Content-Type", "application/json")
295        .send(serde_json::to_vec(&body).unwrap().as_slice())
296        .map_err(|e| format!("Push failed: {e}"))?;
297    let resp_body = resp
298        .into_body()
299        .read_to_string()
300        .map_err(|e| format!("Failed to read response: {e}"))?;
301    let json: serde_json::Value =
302        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
303    Ok(format!(
304        "{} sessions synced",
305        json["synced"].as_i64().unwrap_or(0)
306    ))
307}
308
309pub fn push_gotchas(entries: &[serde_json::Value]) -> Result<String, String> {
310    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
311    let url = format!("{}/api/sync/gotchas", api_url());
312    let body = serde_json::json!({ "gotchas": entries });
313    let resp = ureq::post(&url)
314        .header("Authorization", &format!("Bearer {api_key}"))
315        .header("Content-Type", "application/json")
316        .send(serde_json::to_vec(&body).unwrap().as_slice())
317        .map_err(|e| format!("Push failed: {e}"))?;
318    let resp_body = resp
319        .into_body()
320        .read_to_string()
321        .map_err(|e| format!("Failed to read response: {e}"))?;
322    let json: serde_json::Value =
323        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
324    Ok(format!(
325        "{} gotchas synced",
326        json["synced"].as_i64().unwrap_or(0)
327    ))
328}
329
330pub fn push_buddy(data: &serde_json::Value) -> Result<String, String> {
331    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
332    let url = format!("{}/api/sync/buddy", api_url());
333    let resp = ureq::post(&url)
334        .header("Authorization", &format!("Bearer {api_key}"))
335        .header("Content-Type", "application/json")
336        .send(serde_json::to_vec(data).unwrap().as_slice())
337        .map_err(|e| format!("Push failed: {e}"))?;
338    let resp_body = resp
339        .into_body()
340        .read_to_string()
341        .map_err(|e| format!("Failed to read response: {e}"))?;
342    let _json: serde_json::Value =
343        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
344    Ok("Buddy synced".to_string())
345}
346
347pub fn push_feedback(entries: &[serde_json::Value]) -> Result<String, String> {
348    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
349    let url = format!("{}/api/sync/feedback", api_url());
350    let resp = ureq::post(&url)
351        .header("Authorization", &format!("Bearer {api_key}"))
352        .header("Content-Type", "application/json")
353        .send(serde_json::to_vec(entries).unwrap().as_slice())
354        .map_err(|e| format!("Push failed: {e}"))?;
355    let resp_body = resp
356        .into_body()
357        .read_to_string()
358        .map_err(|e| format!("Failed to read response: {e}"))?;
359    let json: serde_json::Value =
360        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
361    Ok(format!(
362        "{} thresholds synced",
363        json["synced"].as_i64().unwrap_or(0)
364    ))
365}
366
367pub fn pull_knowledge() -> Result<Vec<serde_json::Value>, String> {
368    let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
369    let url = format!("{}/api/sync/knowledge", api_url());
370
371    let resp = ureq::get(&url)
372        .header("Authorization", &format!("Bearer {api_key}"))
373        .call()
374        .map_err(|e| format!("Pull failed: {e}"))?;
375
376    let resp_body = resp
377        .into_body()
378        .read_to_string()
379        .map_err(|e| format!("Failed to read response: {e}"))?;
380
381    let entries: Vec<serde_json::Value> =
382        serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
383
384    Ok(entries)
385}