1use super::serde_defaults;
9#[allow(clippy::wildcard_imports)]
10use super::*;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(default)]
15pub struct SecretDetectionConfig {
16 pub enabled: bool,
17 pub redact: bool,
18 pub custom_patterns: Vec<String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(default)]
27pub struct SetupConfig {
28 pub auto_inject_rules: Option<bool>,
32 pub auto_inject_skills: Option<bool>,
35 #[serde(default = "serde_defaults::default_true")]
37 pub auto_update_mcp: bool,
38}
39
40impl Default for SetupConfig {
41 fn default() -> Self {
42 Self {
43 auto_inject_rules: None,
44 auto_inject_skills: None,
45 auto_update_mcp: true,
46 }
47 }
48}
49
50impl SetupConfig {
51 pub fn should_inject_rules(&self) -> bool {
55 match self.auto_inject_rules {
56 Some(v) => v,
57 None => Self::rules_already_present(),
58 }
59 }
60
61 pub fn should_inject_skills(&self) -> bool {
63 match self.auto_inject_skills {
64 Some(v) => v,
65 None => Self::rules_already_present(),
66 }
67 }
68
69 pub fn should_update_mcp(&self) -> bool {
74 self.auto_update_mcp
75 }
76
77 fn rules_already_present() -> bool {
79 let Some(home) = dirs::home_dir() else {
80 return false;
81 };
82 let marker = crate::rules_inject::RULES_MARKER;
83 let check_paths = [
84 home.join(".cursor/rules/lean-ctx.mdc"),
85 crate::core::editor_registry::claude_rules_dir(&home).join("lean-ctx.md"),
86 crate::core::editor_registry::codebuddy_rules_dir(&home).join("lean-ctx.md"),
87 home.join(".gemini/GEMINI.md"),
88 home.join(".codeium/windsurf/rules/lean-ctx.md"),
89 ];
90 for p in &check_paths {
91 if let Ok(content) = std::fs::read_to_string(p) {
92 if content.contains(marker) {
93 return true;
94 }
95 }
96 }
97 false
98 }
99}
100
101impl Default for SecretDetectionConfig {
102 fn default() -> Self {
103 Self {
104 enabled: true,
105 redact: true,
106 custom_patterns: Vec::new(),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(default)]
114pub struct ArchiveConfig {
115 pub enabled: bool,
116 pub threshold_chars: usize,
117 pub max_age_hours: u64,
118 pub max_disk_mb: u64,
119 pub ephemeral: bool,
120 pub ephemeral_min_tokens: usize,
123}
124
125impl Default for ArchiveConfig {
126 fn default() -> Self {
127 Self {
128 enabled: true,
129 threshold_chars: 800,
130 max_age_hours: 48,
131 max_disk_mb: 500,
132 ephemeral: true,
133 ephemeral_min_tokens: 2000,
134 }
135 }
136}
137
138impl ArchiveConfig {
139 pub fn ephemeral_effective(&self) -> bool {
140 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL") {
141 return !matches!(v.trim(), "0" | "false" | "off");
142 }
143 self.ephemeral && self.enabled
144 }
145
146 pub fn ephemeral_min_tokens_effective(&self) -> usize {
147 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL_MIN_TOKENS") {
148 if let Ok(n) = v.trim().parse::<usize>() {
149 return n;
150 }
151 }
152 self.ephemeral_min_tokens
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(default)]
161pub struct ProvidersConfig {
162 pub enabled: bool,
164 pub github: ProviderEntryConfig,
166 pub gitlab: ProviderEntryConfig,
168 pub auto_index: bool,
170 pub cache_ttl_secs: u64,
172 #[serde(default)]
174 pub mcp_bridges: std::collections::HashMap<String, McpBridgeEntry>,
175}
176
177impl Default for ProvidersConfig {
178 fn default() -> Self {
179 Self {
180 enabled: true,
181 github: ProviderEntryConfig::default(),
182 gitlab: ProviderEntryConfig::default(),
183 auto_index: true,
184 cache_ttl_secs: 120,
185 mcp_bridges: std::collections::HashMap::new(),
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct McpBridgeEntry {
192 #[serde(default)]
194 pub url: Option<String>,
195 #[serde(default)]
197 pub command: Option<String>,
198 #[serde(default)]
200 pub args: Vec<String>,
201 #[serde(default)]
203 pub description: Option<String>,
204 #[serde(default)]
206 pub auth_env: Option<String>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(default)]
212pub struct ProviderEntryConfig {
213 pub enabled: bool,
215 pub token: Option<String>,
217 pub api_url: Option<String>,
219 pub project: Option<String>,
221}
222
223impl Default for ProviderEntryConfig {
224 fn default() -> Self {
225 Self {
226 enabled: true,
227 token: None,
228 api_url: None,
229 project: None,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236#[serde(default)]
237pub struct AutonomyConfig {
238 pub enabled: bool,
239 pub auto_preload: bool,
240 pub auto_dedup: bool,
241 pub auto_related: bool,
242 pub auto_consolidate: bool,
243 pub silent_preload: bool,
244 pub dedup_threshold: usize,
245 pub consolidate_every_calls: u32,
246 pub consolidate_cooldown_secs: u64,
247 #[serde(default = "serde_defaults::default_true")]
248 pub cognition_loop_enabled: bool,
249 #[serde(default = "serde_defaults::default_cognition_loop_interval")]
250 pub cognition_loop_interval_secs: u64,
251 #[serde(default = "serde_defaults::default_cognition_loop_max_steps")]
252 pub cognition_loop_max_steps: u8,
253}
254
255impl Default for AutonomyConfig {
256 fn default() -> Self {
257 Self {
258 enabled: true,
259 auto_preload: true,
260 auto_dedup: true,
261 auto_related: true,
262 auto_consolidate: true,
263 silent_preload: true,
264 dedup_threshold: 8,
265 consolidate_every_calls: 25,
266 consolidate_cooldown_secs: 120,
267 cognition_loop_enabled: true,
268 cognition_loop_interval_secs: 3600,
269 cognition_loop_max_steps: 8,
270 }
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(default)]
278pub struct UpdatesConfig {
279 pub auto_update: bool,
280 pub check_interval_hours: u64,
281 pub notify_only: bool,
282}
283
284impl Default for UpdatesConfig {
285 fn default() -> Self {
286 Self {
287 auto_update: false,
288 check_interval_hours: 6,
289 notify_only: false,
290 }
291 }
292}
293
294impl UpdatesConfig {
295 pub fn from_env() -> Self {
296 let mut cfg = Self::default();
297 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_UPDATE") {
298 cfg.auto_update = v == "1" || v.eq_ignore_ascii_case("true");
299 }
300 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_INTERVAL_HOURS") {
301 if let Ok(h) = v.parse::<u64>() {
302 cfg.check_interval_hours = h.clamp(1, 168);
303 }
304 }
305 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_NOTIFY_ONLY") {
306 cfg.notify_only = v == "1" || v.eq_ignore_ascii_case("true");
307 }
308 cfg
309 }
310}
311
312impl AutonomyConfig {
313 pub fn from_env() -> Self {
315 let mut cfg = Self::default();
316 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
317 if v == "false" || v == "0" {
318 cfg.enabled = false;
319 }
320 }
321 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
322 cfg.auto_preload = v != "false" && v != "0";
323 }
324 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
325 cfg.auto_dedup = v != "false" && v != "0";
326 }
327 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
328 cfg.auto_related = v != "false" && v != "0";
329 }
330 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_CONSOLIDATE") {
331 cfg.auto_consolidate = v != "false" && v != "0";
332 }
333 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
334 cfg.silent_preload = v != "false" && v != "0";
335 }
336 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
337 if let Ok(n) = v.parse() {
338 cfg.dedup_threshold = n;
339 }
340 }
341 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_EVERY_CALLS") {
342 if let Ok(n) = v.parse() {
343 cfg.consolidate_every_calls = n;
344 }
345 }
346 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_COOLDOWN_SECS") {
347 if let Ok(n) = v.parse() {
348 cfg.consolidate_cooldown_secs = n;
349 }
350 }
351 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
352 cfg.cognition_loop_enabled = v != "false" && v != "0";
353 }
354 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
355 if let Ok(n) = v.parse() {
356 cfg.cognition_loop_interval_secs = n;
357 }
358 }
359 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
360 if let Ok(n) = v.parse() {
361 cfg.cognition_loop_max_steps = n;
362 }
363 }
364 cfg
365 }
366
367 pub fn load() -> Self {
369 let file_cfg = Config::load().autonomy;
370 let mut cfg = file_cfg;
371 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
372 if v == "false" || v == "0" {
373 cfg.enabled = false;
374 }
375 }
376 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
377 cfg.auto_preload = v != "false" && v != "0";
378 }
379 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
380 cfg.auto_dedup = v != "false" && v != "0";
381 }
382 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
383 cfg.auto_related = v != "false" && v != "0";
384 }
385 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
386 cfg.silent_preload = v != "false" && v != "0";
387 }
388 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
389 if let Ok(n) = v.parse() {
390 cfg.dedup_threshold = n;
391 }
392 }
393 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
394 cfg.cognition_loop_enabled = v != "false" && v != "0";
395 }
396 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
397 if let Ok(n) = v.parse() {
398 cfg.cognition_loop_interval_secs = n;
399 }
400 }
401 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
402 if let Ok(n) = v.parse() {
403 cfg.cognition_loop_max_steps = n;
404 }
405 }
406 cfg
407 }
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, Default)]
412#[serde(default)]
413pub struct CloudConfig {
414 pub contribute_enabled: bool,
415 pub last_contribute: Option<String>,
416 pub last_sync: Option<String>,
417 pub last_gain_sync: Option<String>,
418 pub last_model_pull: Option<String>,
419 pub auto_sync: bool,
423 pub last_auto_sync: Option<String>,
424 pub auto_index: bool,
429 pub last_index_push: std::collections::HashMap<String, String>,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
441#[serde(default)]
442pub struct GainConfig {
443 pub auto_publish: bool,
446 pub leaderboard: bool,
448 pub display_name: Option<String>,
450 pub auto_publish_interval_hours: u64,
452 pub last_auto_publish: Option<String>,
455}
456
457impl Default for GainConfig {
458 fn default() -> Self {
459 Self {
460 auto_publish: false,
461 leaderboard: true,
462 display_name: None,
463 auto_publish_interval_hours: 24,
464 last_auto_publish: None,
465 }
466 }
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
479#[serde(default)]
480pub struct GraphConfig {
481 pub traversal_edges: bool,
485}
486
487impl Default for GraphConfig {
488 fn default() -> Self {
489 Self {
490 traversal_edges: true,
491 }
492 }
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
503#[serde(default)]
504pub struct SkillifyConfig {
505 pub enabled: bool,
508 pub scope: String,
511 pub min_confidence: f32,
514 pub min_recurrence: u32,
517}
518
519impl Default for SkillifyConfig {
520 fn default() -> Self {
521 Self {
522 enabled: true,
523 scope: "project".to_string(),
524 min_confidence: 0.7,
525 min_recurrence: 2,
526 }
527 }
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
535#[serde(default)]
536pub struct SummariesConfig {
537 pub enabled: bool,
540 pub every_n_turns: u32,
543 pub max_kept: u32,
545}
546
547impl Default for SummariesConfig {
548 fn default() -> Self {
549 Self {
550 enabled: true,
551 every_n_turns: 25,
552 max_kept: 100,
553 }
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct AliasEntry {
560 pub command: String,
561 pub alias: String,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
566#[serde(default)]
567pub struct LoopDetectionConfig {
568 pub normal_threshold: u32,
569 pub reduced_threshold: u32,
570 pub blocked_threshold: u32,
571 pub window_secs: u64,
572 pub search_group_limit: u32,
573 pub tool_total_limits: HashMap<String, u32>,
574}
575
576impl Default for LoopDetectionConfig {
577 fn default() -> Self {
578 let mut tool_total_limits = HashMap::new();
579 tool_total_limits.insert("ctx_read".to_string(), 100);
580 tool_total_limits.insert("ctx_search".to_string(), 80);
581 tool_total_limits.insert("ctx_shell".to_string(), 50);
582 tool_total_limits.insert("ctx_semantic_search".to_string(), 60);
583 Self {
584 normal_threshold: 2,
585 reduced_threshold: 4,
586 blocked_threshold: 0,
587 window_secs: 300,
588 search_group_limit: 10,
589 tool_total_limits,
590 }
591 }
592}
593
594#[derive(Debug, Clone, Default, Serialize, Deserialize)]
607#[serde(default)]
608pub struct EmbeddingConfig {
609 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub model: Option<String>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub dimensions: Option<usize>,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub auto_download: Option<bool>,
620}