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