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