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