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}