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}