Skip to main content

lean_ctx/core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::sync::Mutex;
4use std::time::SystemTime;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
7#[serde(rename_all = "lowercase")]
8pub enum TeeMode {
9    Never,
10    #[default]
11    Failures,
12    Always,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(default)]
17pub struct Config {
18    pub ultra_compact: bool,
19    #[serde(default, deserialize_with = "deserialize_tee_mode")]
20    pub tee_mode: TeeMode,
21    pub checkpoint_interval: u32,
22    pub excluded_commands: Vec<String>,
23    pub passthrough_urls: Vec<String>,
24    pub custom_aliases: Vec<AliasEntry>,
25    /// Commands taking longer than this threshold (ms) are recorded in the slow log.
26    /// Set to 0 to disable slow logging.
27    pub slow_command_threshold_ms: u64,
28    #[serde(default = "default_theme")]
29    pub theme: String,
30    #[serde(default)]
31    pub cloud: CloudConfig,
32    #[serde(default)]
33    pub autonomy: AutonomyConfig,
34}
35
36fn deserialize_tee_mode<'de, D>(deserializer: D) -> Result<TeeMode, D::Error>
37where
38    D: serde::Deserializer<'de>,
39{
40    use serde::de::Error;
41    let v = serde_json::Value::deserialize(deserializer)?;
42    match &v {
43        serde_json::Value::Bool(true) => Ok(TeeMode::Failures),
44        serde_json::Value::Bool(false) => Ok(TeeMode::Never),
45        serde_json::Value::String(s) => match s.as_str() {
46            "never" => Ok(TeeMode::Never),
47            "failures" => Ok(TeeMode::Failures),
48            "always" => Ok(TeeMode::Always),
49            other => Err(D::Error::custom(format!("unknown tee_mode: {other}"))),
50        },
51        _ => Err(D::Error::custom("tee_mode must be string or bool")),
52    }
53}
54
55fn default_theme() -> String {
56    "default".to_string()
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(default)]
61pub struct AutonomyConfig {
62    pub enabled: bool,
63    pub auto_preload: bool,
64    pub auto_dedup: bool,
65    pub auto_related: bool,
66    pub silent_preload: bool,
67    pub dedup_threshold: usize,
68}
69
70impl Default for AutonomyConfig {
71    fn default() -> Self {
72        Self {
73            enabled: true,
74            auto_preload: true,
75            auto_dedup: true,
76            auto_related: true,
77            silent_preload: true,
78            dedup_threshold: 8,
79        }
80    }
81}
82
83impl AutonomyConfig {
84    pub fn from_env() -> Self {
85        let mut cfg = Self::default();
86        if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
87            if v == "false" || v == "0" {
88                cfg.enabled = false;
89            }
90        }
91        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
92            cfg.auto_preload = v != "false" && v != "0";
93        }
94        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
95            cfg.auto_dedup = v != "false" && v != "0";
96        }
97        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
98            cfg.auto_related = v != "false" && v != "0";
99        }
100        if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
101            cfg.silent_preload = v != "false" && v != "0";
102        }
103        if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
104            if let Ok(n) = v.parse() {
105                cfg.dedup_threshold = n;
106            }
107        }
108        cfg
109    }
110
111    pub fn load() -> Self {
112        let file_cfg = Config::load().autonomy;
113        let mut cfg = file_cfg;
114        if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
115            if v == "false" || v == "0" {
116                cfg.enabled = false;
117            }
118        }
119        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
120            cfg.auto_preload = v != "false" && v != "0";
121        }
122        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
123            cfg.auto_dedup = v != "false" && v != "0";
124        }
125        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
126            cfg.auto_related = v != "false" && v != "0";
127        }
128        if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
129            cfg.silent_preload = v != "false" && v != "0";
130        }
131        if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
132            if let Ok(n) = v.parse() {
133                cfg.dedup_threshold = n;
134            }
135        }
136        cfg
137    }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, Default)]
141#[serde(default)]
142pub struct CloudConfig {
143    pub contribute_enabled: bool,
144    pub last_contribute: Option<String>,
145    pub last_sync: Option<String>,
146    pub last_model_pull: Option<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct AliasEntry {
151    pub command: String,
152    pub alias: String,
153}
154
155impl Default for Config {
156    fn default() -> Self {
157        Self {
158            ultra_compact: false,
159            tee_mode: TeeMode::default(),
160            checkpoint_interval: 15,
161            excluded_commands: Vec::new(),
162            passthrough_urls: Vec::new(),
163            custom_aliases: Vec::new(),
164            slow_command_threshold_ms: 5000,
165            theme: default_theme(),
166            cloud: CloudConfig::default(),
167            autonomy: AutonomyConfig::default(),
168        }
169    }
170}
171
172impl Config {
173    pub fn path() -> Option<PathBuf> {
174        dirs::home_dir().map(|h| h.join(".lean-ctx").join("config.toml"))
175    }
176
177    pub fn load() -> Self {
178        static CACHE: Mutex<Option<(Config, SystemTime)>> = Mutex::new(None);
179
180        let path = match Self::path() {
181            Some(p) => p,
182            None => return Self::default(),
183        };
184
185        let mtime = std::fs::metadata(&path)
186            .and_then(|m| m.modified())
187            .unwrap_or(SystemTime::UNIX_EPOCH);
188
189        if let Ok(guard) = CACHE.lock() {
190            if let Some((ref cfg, ref cached_mtime)) = *guard {
191                if *cached_mtime == mtime {
192                    return cfg.clone();
193                }
194            }
195        }
196
197        let cfg = match std::fs::read_to_string(&path) {
198            Ok(content) => toml::from_str(&content).unwrap_or_default(),
199            Err(_) => Self::default(),
200        };
201
202        if let Ok(mut guard) = CACHE.lock() {
203            *guard = Some((cfg.clone(), mtime));
204        }
205
206        cfg
207    }
208
209    pub fn save(&self) -> Result<(), String> {
210        let path = Self::path().ok_or("cannot determine home directory")?;
211        if let Some(parent) = path.parent() {
212            std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
213        }
214        let content = toml::to_string_pretty(self).map_err(|e| e.to_string())?;
215        std::fs::write(&path, content).map_err(|e| e.to_string())
216    }
217
218    pub fn show(&self) -> String {
219        let path = Self::path()
220            .map(|p| p.to_string_lossy().to_string())
221            .unwrap_or_else(|| "~/.lean-ctx/config.toml".to_string());
222        let content = toml::to_string_pretty(self).unwrap_or_default();
223        format!("Config: {path}\n\n{content}")
224    }
225}