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 fn rules_already_present() -> bool {
71 let Some(home) = dirs::home_dir() else {
72 return false;
73 };
74 let marker = crate::rules_inject::RULES_MARKER;
75 let check_paths = [
76 home.join(".cursor/rules/lean-ctx.mdc"),
77 crate::core::editor_registry::claude_rules_dir(&home).join("lean-ctx.md"),
78 home.join(".gemini/GEMINI.md"),
79 home.join(".codeium/windsurf/rules/lean-ctx.md"),
80 ];
81 for p in &check_paths {
82 if let Ok(content) = std::fs::read_to_string(p) {
83 if content.contains(marker) {
84 return true;
85 }
86 }
87 }
88 false
89 }
90}
91
92impl Default for SecretDetectionConfig {
93 fn default() -> Self {
94 Self {
95 enabled: true,
96 redact: true,
97 custom_patterns: Vec::new(),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(default)]
105pub struct ArchiveConfig {
106 pub enabled: bool,
107 pub threshold_chars: usize,
108 pub max_age_hours: u64,
109 pub max_disk_mb: u64,
110 pub ephemeral: bool,
111 pub ephemeral_min_tokens: usize,
114}
115
116impl Default for ArchiveConfig {
117 fn default() -> Self {
118 Self {
119 enabled: true,
120 threshold_chars: 800,
121 max_age_hours: 48,
122 max_disk_mb: 500,
123 ephemeral: true,
124 ephemeral_min_tokens: 2000,
125 }
126 }
127}
128
129impl ArchiveConfig {
130 pub fn ephemeral_effective(&self) -> bool {
131 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL") {
132 return !matches!(v.trim(), "0" | "false" | "off");
133 }
134 self.ephemeral && self.enabled
135 }
136
137 pub fn ephemeral_min_tokens_effective(&self) -> usize {
138 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL_MIN_TOKENS") {
139 if let Ok(n) = v.trim().parse::<usize>() {
140 return n;
141 }
142 }
143 self.ephemeral_min_tokens
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(default)]
152pub struct ProvidersConfig {
153 pub enabled: bool,
155 pub github: ProviderEntryConfig,
157 pub gitlab: ProviderEntryConfig,
159 pub auto_index: bool,
161 pub cache_ttl_secs: u64,
163 #[serde(default)]
165 pub mcp_bridges: std::collections::HashMap<String, McpBridgeEntry>,
166}
167
168impl Default for ProvidersConfig {
169 fn default() -> Self {
170 Self {
171 enabled: true,
172 github: ProviderEntryConfig::default(),
173 gitlab: ProviderEntryConfig::default(),
174 auto_index: true,
175 cache_ttl_secs: 120,
176 mcp_bridges: std::collections::HashMap::new(),
177 }
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct McpBridgeEntry {
183 #[serde(default)]
185 pub url: Option<String>,
186 #[serde(default)]
188 pub command: Option<String>,
189 #[serde(default)]
191 pub args: Vec<String>,
192 #[serde(default)]
194 pub description: Option<String>,
195 #[serde(default)]
197 pub auth_env: Option<String>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(default)]
203pub struct ProviderEntryConfig {
204 pub enabled: bool,
206 pub token: Option<String>,
208 pub api_url: Option<String>,
210 pub project: Option<String>,
212}
213
214impl Default for ProviderEntryConfig {
215 fn default() -> Self {
216 Self {
217 enabled: true,
218 token: None,
219 api_url: None,
220 project: None,
221 }
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(default)]
228pub struct AutonomyConfig {
229 pub enabled: bool,
230 pub auto_preload: bool,
231 pub auto_dedup: bool,
232 pub auto_related: bool,
233 pub auto_consolidate: bool,
234 pub silent_preload: bool,
235 pub dedup_threshold: usize,
236 pub consolidate_every_calls: u32,
237 pub consolidate_cooldown_secs: u64,
238 #[serde(default = "serde_defaults::default_true")]
239 pub cognition_loop_enabled: bool,
240 #[serde(default = "serde_defaults::default_cognition_loop_interval")]
241 pub cognition_loop_interval_secs: u64,
242 #[serde(default = "serde_defaults::default_cognition_loop_max_steps")]
243 pub cognition_loop_max_steps: u8,
244}
245
246impl Default for AutonomyConfig {
247 fn default() -> Self {
248 Self {
249 enabled: true,
250 auto_preload: true,
251 auto_dedup: true,
252 auto_related: true,
253 auto_consolidate: true,
254 silent_preload: true,
255 dedup_threshold: 8,
256 consolidate_every_calls: 25,
257 consolidate_cooldown_secs: 120,
258 cognition_loop_enabled: true,
259 cognition_loop_interval_secs: 3600,
260 cognition_loop_max_steps: 8,
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
268#[serde(default)]
269pub struct UpdatesConfig {
270 pub auto_update: bool,
271 pub check_interval_hours: u64,
272 pub notify_only: bool,
273}
274
275impl Default for UpdatesConfig {
276 fn default() -> Self {
277 Self {
278 auto_update: false,
279 check_interval_hours: 6,
280 notify_only: false,
281 }
282 }
283}
284
285impl UpdatesConfig {
286 pub fn from_env() -> Self {
287 let mut cfg = Self::default();
288 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_UPDATE") {
289 cfg.auto_update = v == "1" || v.eq_ignore_ascii_case("true");
290 }
291 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_INTERVAL_HOURS") {
292 if let Ok(h) = v.parse::<u64>() {
293 cfg.check_interval_hours = h.clamp(1, 168);
294 }
295 }
296 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_NOTIFY_ONLY") {
297 cfg.notify_only = v == "1" || v.eq_ignore_ascii_case("true");
298 }
299 cfg
300 }
301}
302
303impl AutonomyConfig {
304 pub fn from_env() -> Self {
306 let mut cfg = Self::default();
307 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
308 if v == "false" || v == "0" {
309 cfg.enabled = false;
310 }
311 }
312 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
313 cfg.auto_preload = v != "false" && v != "0";
314 }
315 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
316 cfg.auto_dedup = v != "false" && v != "0";
317 }
318 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
319 cfg.auto_related = v != "false" && v != "0";
320 }
321 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_CONSOLIDATE") {
322 cfg.auto_consolidate = v != "false" && v != "0";
323 }
324 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
325 cfg.silent_preload = v != "false" && v != "0";
326 }
327 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
328 if let Ok(n) = v.parse() {
329 cfg.dedup_threshold = n;
330 }
331 }
332 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_EVERY_CALLS") {
333 if let Ok(n) = v.parse() {
334 cfg.consolidate_every_calls = n;
335 }
336 }
337 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_COOLDOWN_SECS") {
338 if let Ok(n) = v.parse() {
339 cfg.consolidate_cooldown_secs = n;
340 }
341 }
342 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
343 cfg.cognition_loop_enabled = v != "false" && v != "0";
344 }
345 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
346 if let Ok(n) = v.parse() {
347 cfg.cognition_loop_interval_secs = n;
348 }
349 }
350 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
351 if let Ok(n) = v.parse() {
352 cfg.cognition_loop_max_steps = n;
353 }
354 }
355 cfg
356 }
357
358 pub fn load() -> Self {
360 let file_cfg = Config::load().autonomy;
361 let mut cfg = file_cfg;
362 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
363 if v == "false" || v == "0" {
364 cfg.enabled = false;
365 }
366 }
367 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
368 cfg.auto_preload = v != "false" && v != "0";
369 }
370 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
371 cfg.auto_dedup = v != "false" && v != "0";
372 }
373 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
374 cfg.auto_related = v != "false" && v != "0";
375 }
376 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
377 cfg.silent_preload = v != "false" && v != "0";
378 }
379 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
380 if let Ok(n) = v.parse() {
381 cfg.dedup_threshold = n;
382 }
383 }
384 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
385 cfg.cognition_loop_enabled = v != "false" && v != "0";
386 }
387 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
388 if let Ok(n) = v.parse() {
389 cfg.cognition_loop_interval_secs = n;
390 }
391 }
392 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
393 if let Ok(n) = v.parse() {
394 cfg.cognition_loop_max_steps = n;
395 }
396 }
397 cfg
398 }
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize, Default)]
403#[serde(default)]
404pub struct CloudConfig {
405 pub contribute_enabled: bool,
406 pub last_contribute: Option<String>,
407 pub last_sync: Option<String>,
408 pub last_gain_sync: Option<String>,
409 pub last_model_pull: Option<String>,
410 pub auto_sync: bool,
414 pub last_auto_sync: Option<String>,
415 pub auto_index: bool,
420 pub last_index_push: std::collections::HashMap<String, String>,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
432#[serde(default)]
433pub struct GainConfig {
434 pub auto_publish: bool,
437 pub leaderboard: bool,
439 pub display_name: Option<String>,
441 pub auto_publish_interval_hours: u64,
443 pub last_auto_publish: Option<String>,
446}
447
448impl Default for GainConfig {
449 fn default() -> Self {
450 Self {
451 auto_publish: false,
452 leaderboard: true,
453 display_name: None,
454 auto_publish_interval_hours: 24,
455 last_auto_publish: None,
456 }
457 }
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
470#[serde(default)]
471pub struct GraphConfig {
472 pub traversal_edges: bool,
476}
477
478impl Default for GraphConfig {
479 fn default() -> Self {
480 Self {
481 traversal_edges: true,
482 }
483 }
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
494#[serde(default)]
495pub struct SkillifyConfig {
496 pub enabled: bool,
499 pub scope: String,
502 pub min_confidence: f32,
505 pub min_recurrence: u32,
508}
509
510impl Default for SkillifyConfig {
511 fn default() -> Self {
512 Self {
513 enabled: true,
514 scope: "project".to_string(),
515 min_confidence: 0.7,
516 min_recurrence: 2,
517 }
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
526#[serde(default)]
527pub struct SummariesConfig {
528 pub enabled: bool,
531 pub every_n_turns: u32,
534 pub max_kept: u32,
536}
537
538impl Default for SummariesConfig {
539 fn default() -> Self {
540 Self {
541 enabled: true,
542 every_n_turns: 25,
543 max_kept: 100,
544 }
545 }
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct AliasEntry {
551 pub command: String,
552 pub alias: String,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
557#[serde(default)]
558pub struct LoopDetectionConfig {
559 pub normal_threshold: u32,
560 pub reduced_threshold: u32,
561 pub blocked_threshold: u32,
562 pub window_secs: u64,
563 pub search_group_limit: u32,
564 pub tool_total_limits: HashMap<String, u32>,
565}
566
567impl Default for LoopDetectionConfig {
568 fn default() -> Self {
569 let mut tool_total_limits = HashMap::new();
570 tool_total_limits.insert("ctx_read".to_string(), 100);
571 tool_total_limits.insert("ctx_search".to_string(), 80);
572 tool_total_limits.insert("ctx_shell".to_string(), 50);
573 tool_total_limits.insert("ctx_semantic_search".to_string(), 60);
574 Self {
575 normal_threshold: 2,
576 reduced_threshold: 4,
577 blocked_threshold: 0,
578 window_secs: 300,
579 search_group_limit: 10,
580 tool_total_limits,
581 }
582 }
583}
584
585#[derive(Debug, Clone, Default, Serialize, Deserialize)]
598#[serde(default)]
599pub struct EmbeddingConfig {
600 #[serde(default, skip_serializing_if = "Option::is_none")]
601 pub model: Option<String>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub dimensions: Option<usize>,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub auto_download: Option<bool>,
611}