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 home.join(".gemini/GEMINI.md"),
87 home.join(".codeium/windsurf/rules/lean-ctx.md"),
88 ];
89 for p in &check_paths {
90 if let Ok(content) = std::fs::read_to_string(p) {
91 if content.contains(marker) {
92 return true;
93 }
94 }
95 }
96 false
97 }
98}
99
100impl Default for SecretDetectionConfig {
101 fn default() -> Self {
102 Self {
103 enabled: true,
104 redact: true,
105 custom_patterns: Vec::new(),
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(default)]
113pub struct ArchiveConfig {
114 pub enabled: bool,
115 pub threshold_chars: usize,
116 pub max_age_hours: u64,
117 pub max_disk_mb: u64,
118 pub ephemeral: bool,
119 pub ephemeral_min_tokens: usize,
122}
123
124impl Default for ArchiveConfig {
125 fn default() -> Self {
126 Self {
127 enabled: true,
128 threshold_chars: 800,
129 max_age_hours: 48,
130 max_disk_mb: 500,
131 ephemeral: true,
132 ephemeral_min_tokens: 2000,
133 }
134 }
135}
136
137impl ArchiveConfig {
138 pub fn ephemeral_effective(&self) -> bool {
139 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL") {
140 return !matches!(v.trim(), "0" | "false" | "off");
141 }
142 self.ephemeral && self.enabled
143 }
144
145 pub fn ephemeral_min_tokens_effective(&self) -> usize {
146 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL_MIN_TOKENS") {
147 if let Ok(n) = v.trim().parse::<usize>() {
148 return n;
149 }
150 }
151 self.ephemeral_min_tokens
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(default)]
160pub struct ProvidersConfig {
161 pub enabled: bool,
163 pub github: ProviderEntryConfig,
165 pub gitlab: ProviderEntryConfig,
167 pub auto_index: bool,
169 pub cache_ttl_secs: u64,
171 #[serde(default)]
173 pub mcp_bridges: std::collections::HashMap<String, McpBridgeEntry>,
174}
175
176impl Default for ProvidersConfig {
177 fn default() -> Self {
178 Self {
179 enabled: true,
180 github: ProviderEntryConfig::default(),
181 gitlab: ProviderEntryConfig::default(),
182 auto_index: true,
183 cache_ttl_secs: 120,
184 mcp_bridges: std::collections::HashMap::new(),
185 }
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct McpBridgeEntry {
191 #[serde(default)]
193 pub url: Option<String>,
194 #[serde(default)]
196 pub command: Option<String>,
197 #[serde(default)]
199 pub args: Vec<String>,
200 #[serde(default)]
202 pub description: Option<String>,
203 #[serde(default)]
205 pub auth_env: Option<String>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210#[serde(default)]
211pub struct ProviderEntryConfig {
212 pub enabled: bool,
214 pub token: Option<String>,
216 pub api_url: Option<String>,
218 pub project: Option<String>,
220}
221
222impl Default for ProviderEntryConfig {
223 fn default() -> Self {
224 Self {
225 enabled: true,
226 token: None,
227 api_url: None,
228 project: None,
229 }
230 }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(default)]
236pub struct AutonomyConfig {
237 pub enabled: bool,
238 pub auto_preload: bool,
239 pub auto_dedup: bool,
240 pub auto_related: bool,
241 pub auto_consolidate: bool,
242 pub silent_preload: bool,
243 pub dedup_threshold: usize,
244 pub consolidate_every_calls: u32,
245 pub consolidate_cooldown_secs: u64,
246 #[serde(default = "serde_defaults::default_true")]
247 pub cognition_loop_enabled: bool,
248 #[serde(default = "serde_defaults::default_cognition_loop_interval")]
249 pub cognition_loop_interval_secs: u64,
250 #[serde(default = "serde_defaults::default_cognition_loop_max_steps")]
251 pub cognition_loop_max_steps: u8,
252}
253
254impl Default for AutonomyConfig {
255 fn default() -> Self {
256 Self {
257 enabled: true,
258 auto_preload: true,
259 auto_dedup: true,
260 auto_related: true,
261 auto_consolidate: true,
262 silent_preload: true,
263 dedup_threshold: 8,
264 consolidate_every_calls: 25,
265 consolidate_cooldown_secs: 120,
266 cognition_loop_enabled: true,
267 cognition_loop_interval_secs: 3600,
268 cognition_loop_max_steps: 8,
269 }
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(default)]
277pub struct UpdatesConfig {
278 pub auto_update: bool,
279 pub check_interval_hours: u64,
280 pub notify_only: bool,
281}
282
283impl Default for UpdatesConfig {
284 fn default() -> Self {
285 Self {
286 auto_update: false,
287 check_interval_hours: 6,
288 notify_only: false,
289 }
290 }
291}
292
293impl UpdatesConfig {
294 pub fn from_env() -> Self {
295 let mut cfg = Self::default();
296 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_UPDATE") {
297 cfg.auto_update = v == "1" || v.eq_ignore_ascii_case("true");
298 }
299 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_INTERVAL_HOURS") {
300 if let Ok(h) = v.parse::<u64>() {
301 cfg.check_interval_hours = h.clamp(1, 168);
302 }
303 }
304 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_NOTIFY_ONLY") {
305 cfg.notify_only = v == "1" || v.eq_ignore_ascii_case("true");
306 }
307 cfg
308 }
309}
310
311impl AutonomyConfig {
312 pub fn from_env() -> Self {
314 let mut cfg = Self::default();
315 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
316 if v == "false" || v == "0" {
317 cfg.enabled = false;
318 }
319 }
320 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
321 cfg.auto_preload = v != "false" && v != "0";
322 }
323 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
324 cfg.auto_dedup = v != "false" && v != "0";
325 }
326 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
327 cfg.auto_related = v != "false" && v != "0";
328 }
329 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_CONSOLIDATE") {
330 cfg.auto_consolidate = v != "false" && v != "0";
331 }
332 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
333 cfg.silent_preload = v != "false" && v != "0";
334 }
335 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
336 if let Ok(n) = v.parse() {
337 cfg.dedup_threshold = n;
338 }
339 }
340 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_EVERY_CALLS") {
341 if let Ok(n) = v.parse() {
342 cfg.consolidate_every_calls = n;
343 }
344 }
345 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_COOLDOWN_SECS") {
346 if let Ok(n) = v.parse() {
347 cfg.consolidate_cooldown_secs = n;
348 }
349 }
350 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
351 cfg.cognition_loop_enabled = v != "false" && v != "0";
352 }
353 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
354 if let Ok(n) = v.parse() {
355 cfg.cognition_loop_interval_secs = n;
356 }
357 }
358 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
359 if let Ok(n) = v.parse() {
360 cfg.cognition_loop_max_steps = n;
361 }
362 }
363 cfg
364 }
365
366 pub fn load() -> Self {
368 let file_cfg = Config::load().autonomy;
369 let mut cfg = file_cfg;
370 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
371 if v == "false" || v == "0" {
372 cfg.enabled = false;
373 }
374 }
375 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
376 cfg.auto_preload = v != "false" && v != "0";
377 }
378 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
379 cfg.auto_dedup = v != "false" && v != "0";
380 }
381 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
382 cfg.auto_related = v != "false" && v != "0";
383 }
384 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
385 cfg.silent_preload = v != "false" && v != "0";
386 }
387 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
388 if let Ok(n) = v.parse() {
389 cfg.dedup_threshold = n;
390 }
391 }
392 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
393 cfg.cognition_loop_enabled = v != "false" && v != "0";
394 }
395 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
396 if let Ok(n) = v.parse() {
397 cfg.cognition_loop_interval_secs = n;
398 }
399 }
400 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
401 if let Ok(n) = v.parse() {
402 cfg.cognition_loop_max_steps = n;
403 }
404 }
405 cfg
406 }
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize, Default)]
411#[serde(default)]
412pub struct CloudConfig {
413 pub contribute_enabled: bool,
414 pub last_contribute: Option<String>,
415 pub last_sync: Option<String>,
416 pub last_gain_sync: Option<String>,
417 pub last_model_pull: Option<String>,
418 pub auto_sync: bool,
422 pub last_auto_sync: Option<String>,
423 pub auto_index: bool,
428 pub last_index_push: std::collections::HashMap<String, String>,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(default)]
441pub struct GainConfig {
442 pub auto_publish: bool,
445 pub leaderboard: bool,
447 pub display_name: Option<String>,
449 pub auto_publish_interval_hours: u64,
451 pub last_auto_publish: Option<String>,
454}
455
456impl Default for GainConfig {
457 fn default() -> Self {
458 Self {
459 auto_publish: false,
460 leaderboard: true,
461 display_name: None,
462 auto_publish_interval_hours: 24,
463 last_auto_publish: None,
464 }
465 }
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
478#[serde(default)]
479pub struct GraphConfig {
480 pub traversal_edges: bool,
484}
485
486impl Default for GraphConfig {
487 fn default() -> Self {
488 Self {
489 traversal_edges: true,
490 }
491 }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
502#[serde(default)]
503pub struct SkillifyConfig {
504 pub enabled: bool,
507 pub scope: String,
510 pub min_confidence: f32,
513 pub min_recurrence: u32,
516}
517
518impl Default for SkillifyConfig {
519 fn default() -> Self {
520 Self {
521 enabled: true,
522 scope: "project".to_string(),
523 min_confidence: 0.7,
524 min_recurrence: 2,
525 }
526 }
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
534#[serde(default)]
535pub struct SummariesConfig {
536 pub enabled: bool,
539 pub every_n_turns: u32,
542 pub max_kept: u32,
544}
545
546impl Default for SummariesConfig {
547 fn default() -> Self {
548 Self {
549 enabled: true,
550 every_n_turns: 25,
551 max_kept: 100,
552 }
553 }
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct AliasEntry {
559 pub command: String,
560 pub alias: String,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize)]
565#[serde(default)]
566pub struct LoopDetectionConfig {
567 pub normal_threshold: u32,
568 pub reduced_threshold: u32,
569 pub blocked_threshold: u32,
570 pub window_secs: u64,
571 pub search_group_limit: u32,
572 pub tool_total_limits: HashMap<String, u32>,
573}
574
575impl Default for LoopDetectionConfig {
576 fn default() -> Self {
577 let mut tool_total_limits = HashMap::new();
578 tool_total_limits.insert("ctx_read".to_string(), 100);
579 tool_total_limits.insert("ctx_search".to_string(), 80);
580 tool_total_limits.insert("ctx_shell".to_string(), 50);
581 tool_total_limits.insert("ctx_semantic_search".to_string(), 60);
582 Self {
583 normal_threshold: 2,
584 reduced_threshold: 4,
585 blocked_threshold: 0,
586 window_secs: 300,
587 search_group_limit: 10,
588 tool_total_limits,
589 }
590 }
591}
592
593#[derive(Debug, Clone, Default, Serialize, Deserialize)]
606#[serde(default)]
607pub struct EmbeddingConfig {
608 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub model: Option<String>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub dimensions: Option<usize>,
612 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub auto_download: Option<bool>,
619}