Skip to main content

lean_ctx/core/config/
sections.rs

1//! Auxiliary configuration section structs.
2//!
3//! Nested config structs (secret-detection, setup, archive, providers,
4//! autonomy, updates, cloud, gain, loop-detection, embedding, …) split out of
5//! `config/mod.rs` to keep the top-level module focused on `Config` itself.
6//! Re-exported via `pub use sections::*`, so external paths stay stable.
7
8use 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/// Controls what lean-ctx injects during `setup` and `update --rewire`.
22/// Fresh installs default to non-invasive (rules/skills off, MCP on).
23/// Users who ran setup interactively get explicit true/false.
24/// `None` = undecided (legacy: check if rules already exist and preserve behavior).
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(default)]
27pub struct SetupConfig {
28    /// Inject agent rule files (CLAUDE.md, .cursor/rules/, etc.).
29    /// None = undecided (legacy compat: inject if rules already present).
30    /// Some(true) = always inject. Some(false) = never inject.
31    pub auto_inject_rules: Option<bool>,
32    /// Install SKILL.md files for supported agents.
33    /// None = undecided. Some(true) = install. Some(false) = skip.
34    pub auto_inject_skills: Option<bool>,
35    /// Register lean-ctx as an MCP server in editor configs.
36    #[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    /// Returns whether rules should be injected, considering legacy installs.
52    /// If undecided (None), checks if lean-ctx rules markers already exist
53    /// in any agent config — if so, keeps injecting for backward compat.
54    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    /// Returns whether skills should be installed.
62    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    /// Check if lean-ctx rules markers exist in any known agent config location.
70    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/// Settings for the zero-loss compression archive (large tool outputs saved to disk).
103#[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}
112
113impl Default for ArchiveConfig {
114    fn default() -> Self {
115        Self {
116            enabled: true,
117            threshold_chars: 800,
118            max_age_hours: 48,
119            max_disk_mb: 500,
120            ephemeral: true,
121        }
122    }
123}
124
125impl ArchiveConfig {
126    pub fn ephemeral_effective(&self) -> bool {
127        if let Ok(v) = std::env::var("LEAN_CTX_EPHEMERAL") {
128            return !matches!(v.trim(), "0" | "false" | "off");
129        }
130        self.ephemeral && self.enabled
131    }
132}
133
134/// Configuration for external context providers (GitHub, GitLab, Jira, etc.).
135/// Each provider can be enabled/disabled and configured with auth tokens.
136/// Override individual tokens via env vars (GITHUB_TOKEN, GITLAB_TOKEN, etc.).
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(default)]
139pub struct ProvidersConfig {
140    /// Master switch for the provider subsystem.
141    pub enabled: bool,
142    /// GitHub provider configuration.
143    pub github: ProviderEntryConfig,
144    /// GitLab provider configuration.
145    pub gitlab: ProviderEntryConfig,
146    /// Auto-ingest provider results into BM25/embedding indexes.
147    pub auto_index: bool,
148    /// Default cache TTL for provider results (seconds).
149    pub cache_ttl_secs: u64,
150    /// MCP Bridge providers: `{ "name" = { url = "...", description = "..." } }`.
151    #[serde(default)]
152    pub mcp_bridges: std::collections::HashMap<String, McpBridgeEntry>,
153}
154
155impl Default for ProvidersConfig {
156    fn default() -> Self {
157        Self {
158            enabled: true,
159            github: ProviderEntryConfig::default(),
160            gitlab: ProviderEntryConfig::default(),
161            auto_index: true,
162            cache_ttl_secs: 120,
163            mcp_bridges: std::collections::HashMap::new(),
164        }
165    }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct McpBridgeEntry {
170    /// HTTP/SSE URL for remote MCP servers.
171    #[serde(default)]
172    pub url: Option<String>,
173    /// Command to spawn a local MCP server (stdio transport).
174    #[serde(default)]
175    pub command: Option<String>,
176    /// Arguments for the command.
177    #[serde(default)]
178    pub args: Vec<String>,
179    /// Human-readable description.
180    #[serde(default)]
181    pub description: Option<String>,
182    /// Environment variable name containing an auth token.
183    #[serde(default)]
184    pub auth_env: Option<String>,
185}
186
187/// Per-provider configuration entry.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(default)]
190pub struct ProviderEntryConfig {
191    /// Whether this specific provider is enabled.
192    pub enabled: bool,
193    /// Auth token (prefer env var; only use this for project-local overrides).
194    pub token: Option<String>,
195    /// API base URL override (for GitHub Enterprise, self-hosted GitLab, etc.).
196    pub api_url: Option<String>,
197    /// Default project/repo for this provider (auto-detected from git remote if empty).
198    pub project: Option<String>,
199}
200
201impl Default for ProviderEntryConfig {
202    fn default() -> Self {
203        Self {
204            enabled: true,
205            token: None,
206            api_url: None,
207            project: None,
208        }
209    }
210}
211
212/// Controls autonomous background behaviors (preload, dedup, consolidation).
213#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(default)]
215pub struct AutonomyConfig {
216    pub enabled: bool,
217    pub auto_preload: bool,
218    pub auto_dedup: bool,
219    pub auto_related: bool,
220    pub auto_consolidate: bool,
221    pub silent_preload: bool,
222    pub dedup_threshold: usize,
223    pub consolidate_every_calls: u32,
224    pub consolidate_cooldown_secs: u64,
225    #[serde(default = "serde_defaults::default_true")]
226    pub cognition_loop_enabled: bool,
227    #[serde(default = "serde_defaults::default_cognition_loop_interval")]
228    pub cognition_loop_interval_secs: u64,
229    #[serde(default = "serde_defaults::default_cognition_loop_max_steps")]
230    pub cognition_loop_max_steps: u8,
231}
232
233impl Default for AutonomyConfig {
234    fn default() -> Self {
235        Self {
236            enabled: true,
237            auto_preload: true,
238            auto_dedup: true,
239            auto_related: true,
240            auto_consolidate: true,
241            silent_preload: true,
242            dedup_threshold: 8,
243            consolidate_every_calls: 25,
244            consolidate_cooldown_secs: 120,
245            cognition_loop_enabled: true,
246            cognition_loop_interval_secs: 3600,
247            cognition_loop_max_steps: 8,
248        }
249    }
250}
251
252/// Controls automatic update behavior. All defaults are OFF — auto-updates
253/// require explicit opt-in via `lean-ctx setup` or `lean-ctx update --schedule`.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(default)]
256pub struct UpdatesConfig {
257    pub auto_update: bool,
258    pub check_interval_hours: u64,
259    pub notify_only: bool,
260}
261
262impl Default for UpdatesConfig {
263    fn default() -> Self {
264        Self {
265            auto_update: false,
266            check_interval_hours: 6,
267            notify_only: false,
268        }
269    }
270}
271
272impl UpdatesConfig {
273    pub fn from_env() -> Self {
274        let mut cfg = Self::default();
275        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_UPDATE") {
276            cfg.auto_update = v == "1" || v.eq_ignore_ascii_case("true");
277        }
278        if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_INTERVAL_HOURS") {
279            if let Ok(h) = v.parse::<u64>() {
280                cfg.check_interval_hours = h.clamp(1, 168);
281            }
282        }
283        if let Ok(v) = std::env::var("LEAN_CTX_UPDATE_NOTIFY_ONLY") {
284            cfg.notify_only = v == "1" || v.eq_ignore_ascii_case("true");
285        }
286        cfg
287    }
288}
289
290impl AutonomyConfig {
291    /// Creates an autonomy config from env vars, falling back to defaults.
292    pub fn from_env() -> Self {
293        let mut cfg = Self::default();
294        if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
295            if v == "false" || v == "0" {
296                cfg.enabled = false;
297            }
298        }
299        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
300            cfg.auto_preload = v != "false" && v != "0";
301        }
302        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
303            cfg.auto_dedup = v != "false" && v != "0";
304        }
305        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
306            cfg.auto_related = v != "false" && v != "0";
307        }
308        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_CONSOLIDATE") {
309            cfg.auto_consolidate = v != "false" && v != "0";
310        }
311        if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
312            cfg.silent_preload = v != "false" && v != "0";
313        }
314        if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
315            if let Ok(n) = v.parse() {
316                cfg.dedup_threshold = n;
317            }
318        }
319        if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_EVERY_CALLS") {
320            if let Ok(n) = v.parse() {
321                cfg.consolidate_every_calls = n;
322            }
323        }
324        if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_COOLDOWN_SECS") {
325            if let Ok(n) = v.parse() {
326                cfg.consolidate_cooldown_secs = n;
327            }
328        }
329        if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
330            cfg.cognition_loop_enabled = v != "false" && v != "0";
331        }
332        if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
333            if let Ok(n) = v.parse() {
334                cfg.cognition_loop_interval_secs = n;
335            }
336        }
337        if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
338            if let Ok(n) = v.parse() {
339                cfg.cognition_loop_max_steps = n;
340            }
341        }
342        cfg
343    }
344
345    /// Loads autonomy config from disk, with env var overrides applied.
346    pub fn load() -> Self {
347        let file_cfg = Config::load().autonomy;
348        let mut cfg = file_cfg;
349        if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
350            if v == "false" || v == "0" {
351                cfg.enabled = false;
352            }
353        }
354        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
355            cfg.auto_preload = v != "false" && v != "0";
356        }
357        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
358            cfg.auto_dedup = v != "false" && v != "0";
359        }
360        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
361            cfg.auto_related = v != "false" && v != "0";
362        }
363        if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
364            cfg.silent_preload = v != "false" && v != "0";
365        }
366        if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
367            if let Ok(n) = v.parse() {
368                cfg.dedup_threshold = n;
369            }
370        }
371        if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_ENABLED") {
372            cfg.cognition_loop_enabled = v != "false" && v != "0";
373        }
374        if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_INTERVAL_SECS") {
375            if let Ok(n) = v.parse() {
376                cfg.cognition_loop_interval_secs = n;
377            }
378        }
379        if let Ok(v) = std::env::var("LEAN_CTX_COGNITION_LOOP_MAX_STEPS") {
380            if let Ok(n) = v.parse() {
381                cfg.cognition_loop_max_steps = n;
382            }
383        }
384        cfg
385    }
386}
387
388/// Cloud sync and contribution settings (pattern sharing, model pulls).
389#[derive(Debug, Clone, Serialize, Deserialize, Default)]
390#[serde(default)]
391pub struct CloudConfig {
392    pub contribute_enabled: bool,
393    pub last_contribute: Option<String>,
394    pub last_sync: Option<String>,
395    pub last_gain_sync: Option<String>,
396    pub last_model_pull: Option<String>,
397}
398
399/// Settings for publishing your token-savings recap (`gain --publish` / auto-publish).
400///
401/// Publishing is always opt-in: it sends a small, whitelisted *aggregate* payload (tokens
402/// saved, $ avoided, compression % — never code, paths or counts) to the cloud.
403/// `auto_publish` simply removes the need to re-run `gain --publish` by hand; it stays off
404/// until the user explicitly enables it.
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[serde(default)]
407pub struct GainConfig {
408    /// When true, `lean-ctx gain` automatically (re)publishes the recap, throttled to
409    /// `auto_publish_interval_hours`. Off by default.
410    pub auto_publish: bool,
411    /// When auto-publishing, also opt into the public leaderboard.
412    pub leaderboard: bool,
413    /// Optional display name for the published card / leaderboard entry.
414    pub display_name: Option<String>,
415    /// Minimum hours between automatic publishes (throttle).
416    pub auto_publish_interval_hours: u64,
417    /// Runtime state — RFC3339 timestamp of the last automatic publish. Managed by the
418    /// tool, not meant to be set by hand.
419    pub last_auto_publish: Option<String>,
420}
421
422impl Default for GainConfig {
423    fn default() -> Self {
424        Self {
425            auto_publish: false,
426            leaderboard: true,
427            display_name: None,
428            auto_publish_interval_hours: 24,
429            last_auto_publish: None,
430        }
431    }
432}
433
434/// A user-defined command alias mapping for shell compression patterns.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct AliasEntry {
437    pub command: String,
438    pub alias: String,
439}
440
441/// Thresholds for detecting and throttling repetitive agent tool call loops.
442#[derive(Debug, Clone, Serialize, Deserialize)]
443#[serde(default)]
444pub struct LoopDetectionConfig {
445    pub normal_threshold: u32,
446    pub reduced_threshold: u32,
447    pub blocked_threshold: u32,
448    pub window_secs: u64,
449    pub search_group_limit: u32,
450    pub tool_total_limits: HashMap<String, u32>,
451}
452
453impl Default for LoopDetectionConfig {
454    fn default() -> Self {
455        let mut tool_total_limits = HashMap::new();
456        tool_total_limits.insert("ctx_read".to_string(), 100);
457        tool_total_limits.insert("ctx_search".to_string(), 80);
458        tool_total_limits.insert("ctx_shell".to_string(), 50);
459        tool_total_limits.insert("ctx_semantic_search".to_string(), 60);
460        Self {
461            normal_threshold: 2,
462            reduced_threshold: 4,
463            blocked_threshold: 0,
464            window_secs: 300,
465            search_group_limit: 10,
466            tool_total_limits,
467        }
468    }
469}
470
471/// Semantic-embedding engine settings.
472///
473/// `model` selects which local ONNX embedding model lean-ctx downloads and uses for
474/// `ctx_semantic_search`. Accepts the same aliases as the `LEAN_CTX_EMBEDDING_MODEL` env
475/// var: `minilm` (all-MiniLM-L6-v2, 384d — the default), `jina-code-v2` (768d,
476/// code-optimized) or `nomic` (768d). When the env var is set it takes precedence; an
477/// unset/`None` value uses the default model. Switching models triggers a one-time
478/// re-index on the next semantic search (vector dimensions follow from the model).
479#[derive(Debug, Clone, Default, Serialize, Deserialize)]
480#[serde(default)]
481pub struct EmbeddingConfig {
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub model: Option<String>,
484}