Skip to main content

lean_ctx/
cloud_sync.rs

1use crate::core::config::Config;
2
3pub fn cloud_background_tasks() {
4    let mut config = Config::load();
5    let today = chrono::Local::now().format("%Y-%m-%d").to_string();
6
7    let already_contributed = config
8        .cloud
9        .last_contribute
10        .as_deref()
11        .map(|d| d == today)
12        .unwrap_or(false);
13    let already_synced = config
14        .cloud
15        .last_sync
16        .as_deref()
17        .map(|d| d == today)
18        .unwrap_or(false);
19    let already_pulled = config
20        .cloud
21        .last_model_pull
22        .as_deref()
23        .map(|d| d == today)
24        .unwrap_or(false);
25
26    if config.cloud.contribute_enabled && !already_contributed {
27        let entries = collect_contribute_entries();
28        if !entries.is_empty() && crate::cloud_client::contribute(&entries).is_ok() {
29            config.cloud.last_contribute = Some(today.clone());
30        }
31    }
32
33    if crate::cloud_client::check_pro() {
34        if !already_synced {
35            let stats_data = crate::core::stats::format_gain_json();
36            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stats_data) {
37                let entry = serde_json::json!({
38                    "date": &today,
39                    "tokens_original": parsed["total_original_tokens"].as_i64().unwrap_or(0),
40                    "tokens_compressed": parsed["total_compressed_tokens"].as_i64().unwrap_or(0),
41                    "tokens_saved": parsed["total_saved_tokens"].as_i64().unwrap_or(0),
42                    "tool_calls": parsed["total_calls"].as_i64().unwrap_or(0),
43                    "cache_hits": parsed["cache_hits"].as_i64().unwrap_or(0),
44                    "cache_misses": parsed["cache_misses"].as_i64().unwrap_or(0),
45                });
46                if crate::cloud_client::sync_stats(&[entry]).is_ok() {
47                    config.cloud.last_sync = Some(today.clone());
48                }
49            }
50        }
51
52        if !already_pulled {
53            if let Ok(data) = crate::cloud_client::pull_pro_models() {
54                let _ = crate::cloud_client::save_pro_models(&data);
55                config.cloud.last_model_pull = Some(today.clone());
56            }
57        }
58    }
59
60    let _ = config.save();
61}
62
63pub fn collect_contribute_entries() -> Vec<serde_json::Value> {
64    let mut entries = Vec::new();
65
66    if let Some(home) = dirs::home_dir() {
67        let mode_stats_path = home.join(".lean-ctx").join("mode_stats.json");
68        if let Ok(data) = std::fs::read_to_string(&mode_stats_path) {
69            if let Ok(predictor) = serde_json::from_str::<serde_json::Value>(&data) {
70                if let Some(history) = predictor["history"].as_object() {
71                    for (_key, outcomes) in history {
72                        if let Some(arr) = outcomes.as_array() {
73                            for outcome in arr.iter().rev().take(3) {
74                                let ext = outcome["ext"].as_str().unwrap_or("unknown");
75                                let mode = outcome["mode"].as_str().unwrap_or("full");
76                                let t_in = outcome["tokens_in"].as_u64().unwrap_or(0);
77                                let t_out = outcome["tokens_out"].as_u64().unwrap_or(0);
78                                let ratio = if t_in > 0 {
79                                    1.0 - t_out as f64 / t_in as f64
80                                } else {
81                                    0.0
82                                };
83                                let bucket = match t_in {
84                                    0..=500 => "0-500",
85                                    501..=2000 => "500-2k",
86                                    2001..=10000 => "2k-10k",
87                                    _ => "10k+",
88                                };
89                                entries.push(serde_json::json!({
90                                    "file_ext": format!(".{ext}"),
91                                    "size_bucket": bucket,
92                                    "best_mode": mode,
93                                    "compression_ratio": (ratio * 100.0).round() / 100.0,
94                                }));
95                                if entries.len() >= 200 {
96                                    return entries;
97                                }
98                            }
99                        }
100                    }
101                }
102            }
103        }
104    }
105
106    if entries.is_empty() {
107        let stats_data = crate::core::stats::format_gain_json();
108        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stats_data) {
109            let original = parsed["cep"]["total_tokens_original"].as_u64().unwrap_or(0);
110            let compressed = parsed["cep"]["total_tokens_compressed"]
111                .as_u64()
112                .unwrap_or(0);
113            let ratio = if original > 0 {
114                1.0 - compressed as f64 / original as f64
115            } else {
116                0.0
117            };
118            if let Some(modes) = parsed["cep"]["modes"].as_object() {
119                let read_modes = ["full", "map", "signatures", "auto", "aggressive", "entropy"];
120                for (mode, count) in modes {
121                    if !read_modes.contains(&mode.as_str()) || count.as_u64().unwrap_or(0) == 0 {
122                        continue;
123                    }
124                    entries.push(serde_json::json!({
125                        "file_ext": "mixed",
126                        "size_bucket": "mixed",
127                        "best_mode": mode,
128                        "compression_ratio": (ratio * 100.0).round() / 100.0,
129                    }));
130                }
131            }
132        }
133    }
134
135    entries
136}