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 {
85 let Some(home) = dirs::home_dir() else {
86 return false;
87 };
88 if crate::rules_inject::any_rules_marker_present(&home) {
89 return true;
90 }
91 let legacy_paths = [
92 crate::core::editor_registry::claude_rules_dir(&home).join("lean-ctx.md"),
93 crate::core::editor_registry::codebuddy_rules_dir(&home).join("lean-ctx.md"),
94 ];
95 legacy_paths.iter().any(|p| {
96 std::fs::read_to_string(p)
97 .is_ok_and(|c| c.contains(crate::core::rules_canonical::START_MARK))
98 })
99 }
100}
101
102impl Default for SecretDetectionConfig {
103 fn default() -> Self {
104 Self {
105 enabled: true,
106 redact: true,
107 custom_patterns: Vec::new(),
108 }
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(default)]
115pub struct ArchiveConfig {
116 pub enabled: bool,
117 pub threshold_chars: usize,
118 pub max_age_hours: u64,
119 pub max_disk_mb: u64,
120 pub ephemeral: bool,
121 pub ephemeral_min_tokens: usize,
124}
125
126impl Default for ArchiveConfig {
127 fn default() -> Self {
128 Self {
129 enabled: true,
130 threshold_chars: 800,
131 max_age_hours: 48,
132 max_disk_mb: 500,
133 ephemeral: true,
134 ephemeral_min_tokens: 2000,
135 }
136 }
137}
138
139impl ArchiveConfig {
140 pub fn ephemeral_effective(&self) -> bool {
141 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL") {
142 return !matches!(v.trim(), "0" | "false" | "off");
143 }
144 self.ephemeral && self.enabled
145 }
146
147 pub fn ephemeral_min_tokens_effective(&self) -> usize {
148 if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL_MIN_TOKENS")
149 && let Ok(n) = v.trim().parse::<usize>()
150 {
151 return n;
152 }
153 self.ephemeral_min_tokens
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(default)]
162pub struct ProvidersConfig {
163 pub enabled: bool,
165 pub github: ProviderEntryConfig,
167 pub gitlab: ProviderEntryConfig,
169 pub auto_index: bool,
171 pub cache_ttl_secs: u64,
173 #[serde(default)]
175 pub mcp_bridges: std::collections::HashMap<String, McpBridgeEntry>,
176}
177
178impl Default for ProvidersConfig {
179 fn default() -> Self {
180 Self {
181 enabled: true,
182 github: ProviderEntryConfig::default(),
183 gitlab: ProviderEntryConfig::default(),
184 auto_index: true,
185 cache_ttl_secs: 120,
186 mcp_bridges: std::collections::HashMap::new(),
187 }
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct McpBridgeEntry {
193 #[serde(default)]
195 pub url: Option<String>,
196 #[serde(default)]
198 pub command: Option<String>,
199 #[serde(default)]
201 pub args: Vec<String>,
202 #[serde(default)]
204 pub description: Option<String>,
205 #[serde(default)]
207 pub auth_env: Option<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(default)]
213pub struct ProviderEntryConfig {
214 pub enabled: bool,
216 pub token: Option<String>,
218 pub api_url: Option<String>,
220 pub project: Option<String>,
222}
223
224impl Default for ProviderEntryConfig {
225 fn default() -> Self {
226 Self {
227 enabled: true,
228 token: None,
229 api_url: None,
230 project: None,
231 }
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(default)]
238pub struct AutonomyConfig {
239 pub enabled: bool,
240 pub auto_preload: bool,
241 pub auto_dedup: bool,
242 pub auto_related: bool,
243 pub auto_consolidate: bool,
244 pub silent_preload: bool,
245 pub dedup_threshold: usize,
246 pub consolidate_every_calls: u32,
247 pub consolidate_cooldown_secs: u64,
248 #[serde(default = "serde_defaults::default_true")]
249 pub cognition_loop_enabled: bool,
250 #[serde(default = "serde_defaults::default_cognition_loop_interval")]
251 pub cognition_loop_interval_secs: u64,
252 #[serde(default = "serde_defaults::default_cognition_loop_max_steps")]
253 pub cognition_loop_max_steps: u8,
254 #[serde(default = "serde_defaults::default_cognition_synthesis_min_cluster")]
257 pub cognition_synthesis_min_cluster: usize,
258}
259
260impl Default for AutonomyConfig {
261 fn default() -> Self {
262 Self {
263 enabled: true,
264 auto_preload: true,
265 auto_dedup: true,
266 auto_related: true,
267 auto_consolidate: true,
268 silent_preload: true,
269 dedup_threshold: 8,
270 consolidate_every_calls: 25,
271 consolidate_cooldown_secs: 120,
272 cognition_loop_enabled: true,
273 cognition_loop_interval_secs: 3600,
274 cognition_loop_max_steps: 9,
275 cognition_synthesis_min_cluster: 3,
276 }
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(default)]
284pub struct UpdatesConfig {
285 pub auto_update: bool,
286 pub check_interval_hours: u64,
287 pub notify_only: bool,
288}
289
290impl Default for UpdatesConfig {
291 fn default() -> Self {
292 Self {
293 auto_update: false,
294 check_interval_hours: 6,
295 notify_only: false,
296 }
297 }
298}
299
300impl UpdatesConfig {
301 pub fn from_env() -> Self {
302 let mut cfg = Self::default();
303 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_UPDATE") {
304 cfg.auto_update = v == "1" || v.eq_ignore_ascii_case("true");
305 }
306 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_INTERVAL_HOURS")
307 && let Ok(h) = v.parse::<u64>()
308 {
309 cfg.check_interval_hours = h.clamp(1, 168);
310 }
311 if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_NOTIFY_ONLY") {
312 cfg.notify_only = v == "1" || v.eq_ignore_ascii_case("true");
313 }
314 cfg
315 }
316}
317
318impl AutonomyConfig {
319 pub fn from_env() -> Self {
321 let mut cfg = Self::default();
322 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY")
323 && (v == "false" || v == "0")
324 {
325 cfg.enabled = false;
326 }
327 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
328 cfg.auto_preload = v != "false" && v != "0";
329 }
330 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
331 cfg.auto_dedup = v != "false" && v != "0";
332 }
333 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
334 cfg.auto_related = v != "false" && v != "0";
335 }
336 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_CONSOLIDATE") {
337 cfg.auto_consolidate = v != "false" && v != "0";
338 }
339 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
340 cfg.silent_preload = v != "false" && v != "0";
341 }
342 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD")
343 && let Ok(n) = v.parse()
344 {
345 cfg.dedup_threshold = n;
346 }
347 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_EVERY_CALLS")
348 && let Ok(n) = v.parse()
349 {
350 cfg.consolidate_every_calls = n;
351 }
352 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_COOLDOWN_SECS")
353 && let Ok(n) = v.parse()
354 {
355 cfg.consolidate_cooldown_secs = n;
356 }
357 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
358 cfg.cognition_loop_enabled = v != "false" && v != "0";
359 }
360 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS")
361 && let Ok(n) = v.parse()
362 {
363 cfg.cognition_loop_interval_secs = n;
364 }
365 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS")
366 && let Ok(n) = v.parse()
367 {
368 cfg.cognition_loop_max_steps = n;
369 }
370 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_SYNTHESIS_MIN_CLUSTER")
371 && let Ok(n) = v.parse()
372 {
373 cfg.cognition_synthesis_min_cluster = n;
374 }
375 cfg
376 }
377
378 pub fn load() -> Self {
380 let file_cfg = Config::load().autonomy;
381 let mut cfg = file_cfg;
382 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY")
383 && (v == "false" || v == "0")
384 {
385 cfg.enabled = false;
386 }
387 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
388 cfg.auto_preload = v != "false" && v != "0";
389 }
390 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
391 cfg.auto_dedup = v != "false" && v != "0";
392 }
393 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
394 cfg.auto_related = v != "false" && v != "0";
395 }
396 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
397 cfg.silent_preload = v != "false" && v != "0";
398 }
399 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD")
400 && let Ok(n) = v.parse()
401 {
402 cfg.dedup_threshold = n;
403 }
404 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
405 cfg.cognition_loop_enabled = v != "false" && v != "0";
406 }
407 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS")
408 && let Ok(n) = v.parse()
409 {
410 cfg.cognition_loop_interval_secs = n;
411 }
412 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS")
413 && let Ok(n) = v.parse()
414 {
415 cfg.cognition_loop_max_steps = n;
416 }
417 if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_SYNTHESIS_MIN_CLUSTER")
418 && let Ok(n) = v.parse()
419 {
420 cfg.cognition_synthesis_min_cluster = n;
421 }
422 cfg
423 }
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, Default)]
428#[serde(default)]
429pub struct CloudConfig {
430 pub contribute_enabled: bool,
431 pub last_contribute: Option<String>,
432 pub last_sync: Option<String>,
433 pub last_gain_sync: Option<String>,
434 pub last_model_pull: Option<String>,
435 pub auto_sync: bool,
439 pub last_auto_sync: Option<String>,
440 pub auto_index: bool,
445 pub last_index_push: std::collections::HashMap<String, String>,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
457#[serde(default)]
458pub struct GainConfig {
459 pub auto_publish: bool,
462 pub leaderboard: bool,
464 pub display_name: Option<String>,
466 pub auto_publish_interval_hours: u64,
468 pub last_auto_publish: Option<String>,
471}
472
473impl Default for GainConfig {
474 fn default() -> Self {
475 Self {
476 auto_publish: false,
477 leaderboard: true,
478 display_name: None,
479 auto_publish_interval_hours: 24,
480 last_auto_publish: None,
481 }
482 }
483}
484
485#[derive(Debug, Clone, Default, Serialize, Deserialize)]
494#[serde(default)]
495pub struct CostConfig {
496 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub default_model: Option<String>,
500 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
505 pub models: HashMap<String, String>,
506}
507
508impl CostConfig {
509 pub fn model_for_client(&self, client: &str) -> Option<String> {
513 self.models
514 .get(client)
515 .or(self.default_model.as_ref())
516 .map(|s| s.trim().to_string())
517 .filter(|s| !s.is_empty())
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
531#[serde(default)]
532pub struct GraphConfig {
533 pub traversal_edges: bool,
537}
538
539impl Default for GraphConfig {
540 fn default() -> Self {
541 Self {
542 traversal_edges: true,
543 }
544 }
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
555#[serde(default)]
556pub struct SkillifyConfig {
557 pub enabled: bool,
560 pub scope: String,
563 pub min_confidence: f32,
566 pub min_recurrence: u32,
569}
570
571impl Default for SkillifyConfig {
572 fn default() -> Self {
573 Self {
574 enabled: true,
575 scope: "project".to_string(),
576 min_confidence: 0.7,
577 min_recurrence: 2,
578 }
579 }
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
587#[serde(default)]
588pub struct SummariesConfig {
589 pub enabled: bool,
592 pub every_n_turns: u32,
595 pub max_kept: u32,
597}
598
599impl Default for SummariesConfig {
600 fn default() -> Self {
601 Self {
602 enabled: true,
603 every_n_turns: 25,
604 max_kept: 100,
605 }
606 }
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize)]
611pub struct AliasEntry {
612 pub command: String,
613 pub alias: String,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
618#[serde(default)]
619pub struct LoopDetectionConfig {
620 pub normal_threshold: u32,
621 pub reduced_threshold: u32,
622 pub blocked_threshold: u32,
623 pub window_secs: u64,
624 pub search_group_limit: u32,
625 pub tool_total_limits: HashMap<String, u32>,
626}
627
628impl Default for LoopDetectionConfig {
629 fn default() -> Self {
630 let mut tool_total_limits = HashMap::new();
631 tool_total_limits.insert("ctx_read".to_string(), 100);
632 tool_total_limits.insert("ctx_search".to_string(), 80);
633 tool_total_limits.insert("ctx_shell".to_string(), 50);
634 tool_total_limits.insert("ctx_semantic_search".to_string(), 60);
635 Self {
636 normal_threshold: 2,
637 reduced_threshold: 4,
638 blocked_threshold: 0,
639 window_secs: 300,
640 search_group_limit: 10,
641 tool_total_limits,
642 }
643 }
644}
645
646#[derive(Debug, Clone, Default, Serialize, Deserialize)]
660#[serde(default)]
661pub struct EmbeddingConfig {
662 #[serde(default, skip_serializing_if = "Option::is_none")]
663 pub model: Option<String>,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub dimensions: Option<usize>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
672 pub auto_download: Option<bool>,
673}