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 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}