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    /// Returns whether `setup`/`onboard`/`init` may (re)register the lean-ctx
70    /// MCP server in editor configs. Honors `auto_update_mcp` (#281) so locked-
71    /// down environments can keep MCP out of agent settings while still getting
72    /// hooks, rules and skills.
73    pub fn should_update_mcp(&self) -> bool {
74        self.auto_update_mcp
75    }
76
77    /// Check if lean-ctx rules markers exist in any known agent config location.
78    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/// Settings for the zero-loss compression archive (large tool outputs saved to disk).
111#[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    /// Minimum output tokens before the ephemeral firewall replaces an inline tool
120    /// result with a summary + retrieval ref. Outputs below this stay fully inline.
121    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/// Configuration for external context providers (GitHub, GitLab, Jira, etc.).
156/// Each provider can be enabled/disabled and configured with auth tokens.
157/// Override individual tokens via env vars (GITHUB_TOKEN, GITLAB_TOKEN, etc.).
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(default)]
160pub struct ProvidersConfig {
161    /// Master switch for the provider subsystem.
162    pub enabled: bool,
163    /// GitHub provider configuration.
164    pub github: ProviderEntryConfig,
165    /// GitLab provider configuration.
166    pub gitlab: ProviderEntryConfig,
167    /// Auto-ingest provider results into BM25/embedding indexes.
168    pub auto_index: bool,
169    /// Default cache TTL for provider results (seconds).
170    pub cache_ttl_secs: u64,
171    /// MCP Bridge providers: `{ "name" = { url = "...", description = "..." } }`.
172    #[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    /// HTTP/SSE URL for remote MCP servers.
192    #[serde(default)]
193    pub url: Option<String>,
194    /// Command to spawn a local MCP server (stdio transport).
195    #[serde(default)]
196    pub command: Option<String>,
197    /// Arguments for the command.
198    #[serde(default)]
199    pub args: Vec<String>,
200    /// Human-readable description.
201    #[serde(default)]
202    pub description: Option<String>,
203    /// Environment variable name containing an auth token.
204    #[serde(default)]
205    pub auth_env: Option<String>,
206}
207
208/// Per-provider configuration entry.
209#[derive(Debug, Clone, Serialize, Deserialize)]
210#[serde(default)]
211pub struct ProviderEntryConfig {
212    /// Whether this specific provider is enabled.
213    pub enabled: bool,
214    /// Auth token (prefer env var; only use this for project-local overrides).
215    pub token: Option<String>,
216    /// API base URL override (for GitHub Enterprise, self-hosted GitLab, etc.).
217    pub api_url: Option<String>,
218    /// Default project/repo for this provider (auto-detected from git remote if empty).
219    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/// Controls autonomous background behaviors (preload, dedup, consolidation).
234#[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/// Controls automatic update behavior. All defaults are OFF — auto-updates
274/// require explicit opt-in via `lean-ctx setup` or `lean-ctx update --schedule`.
275#[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    /// Creates an autonomy config from env vars, falling back to defaults.
313    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    /// Loads autonomy config from disk, with env var overrides applied.
367    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/// Cloud sync and contribution settings (pattern sharing, model pulls).
410#[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    /// Auto-push the Pro Personal-Cloud surfaces (knowledge, commands, CEP,
419    /// gotchas, buddy, feedback) from the background task — opt-in, once per
420    /// day, offline-tolerant (GL #384). Toggle: `lean-ctx cloud autosync on`.
421    pub auto_sync: bool,
422    pub last_auto_sync: Option<String>,
423    /// Auto-push the project's encrypted retrieval-index bundle (hosted
424    /// Personal Index, GL #392) alongside the daily auto-sync — separate
425    /// opt-in because index bundles are orders of magnitude larger than the
426    /// other surfaces. Toggle: `lean-ctx cloud autoindex on`.
427    pub auto_index: bool,
428    /// Per-project debounce: `project_hash → YYYY-MM-DD` of the last
429    /// successful background index push.
430    pub last_index_push: std::collections::HashMap<String, String>,
431}
432
433/// Settings for publishing your token-savings recap (`gain --publish` / auto-publish).
434///
435/// Publishing is always opt-in: it sends a small, whitelisted *aggregate* payload (tokens
436/// saved, $ avoided, compression % — never code, paths or counts) to the cloud.
437/// `auto_publish` simply removes the need to re-run `gain --publish` by hand; it stays off
438/// until the user explicitly enables it.
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(default)]
441pub struct GainConfig {
442    /// When true, `lean-ctx gain` automatically (re)publishes the recap, throttled to
443    /// `auto_publish_interval_hours`. Off by default.
444    pub auto_publish: bool,
445    /// When auto-publishing, also opt into the public leaderboard.
446    pub leaderboard: bool,
447    /// Optional display name for the published card / leaderboard entry.
448    pub display_name: Option<String>,
449    /// Minimum hours between automatic publishes (throttle).
450    pub auto_publish_interval_hours: u64,
451    /// Runtime state — RFC3339 timestamp of the last automatic publish. Managed by the
452    /// tool, not meant to be set by hand.
453    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/// Settings for the code graph — in particular the *traversal* (co-access) edges
469/// learned from real agent sessions (#289).
470///
471/// The static AST/import graph captures how code is wired structurally; it cannot
472/// see which files an agent actually opens *together* while solving a task.
473/// Traversal edges add that behavioural signal: files surfaced together are
474/// associated with a decaying weight (Hebbian co-access), folded into the graph
475/// as `co_access` edges and mixed into recall. The store is bounded and decays,
476/// so stale associations fade.
477#[derive(Debug, Clone, Serialize, Deserialize)]
478#[serde(default)]
479pub struct GraphConfig {
480    /// Record co-access between files surfaced together in a session, surface them
481    /// as decaying `co_access` edges in the graph, and boost recall by them.
482    /// On by default; set to `false` for a purely static (AST-only) graph.
483    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/// Skillify (#290): mine the project's session diary + knowledge facts into
495/// versioned, git-committable `.cursor/rules/skillify-*.mdc` rule files.
496///
497/// The miner is precision-biased — it only codifies recurring or high-confidence
498/// patterns and never invents content. Runs on demand (`ctx_skillify` /
499/// `lean-ctx skillify`); re-running merges (bumps version) only when the distilled
500/// content actually changes.
501#[derive(Debug, Clone, Serialize, Deserialize)]
502#[serde(default)]
503pub struct SkillifyConfig {
504    /// Master switch for the skillify miner. On by default; the miner only ever
505    /// acts when explicitly invoked, so this never writes files unprompted.
506    pub enabled: bool,
507    /// Where generated rules are written: `project` (`<repo>/.cursor/rules`,
508    /// git-committable, default) or `global` (`~/.cursor/rules`).
509    pub scope: String,
510    /// Minimum confidence for a single curated knowledge fact to be codified even
511    /// without repetition. 0.0..=1.0.
512    pub min_confidence: f32,
513    /// Minimum number of reinforcements (confirmations / repeated mentions) before
514    /// a pattern is codified when its confidence is below `min_confidence`.
515    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/// AI session summaries (#292): periodically distil the working session into a
530/// compact, *semantically recallable* summary so a future session can answer
531/// "what did I do last time on X?". Deterministic and local-first — recall uses
532/// embeddings when the `embeddings` feature is on, else a lexical fallback.
533#[derive(Debug, Clone, Serialize, Deserialize)]
534#[serde(default)]
535pub struct SummariesConfig {
536    /// Record periodic session summaries. On by default; recording is cheap and
537    /// happens at most once per `every_n_turns` tool calls.
538    pub enabled: bool,
539    /// Tool calls between automatic summaries. The auto-checkpoint cadence still
540    /// gates the check, so the effective minimum is the checkpoint interval.
541    pub every_n_turns: u32,
542    /// Maximum summaries kept per project (oldest pruned first).
543    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/// A user-defined command alias mapping for shell compression patterns.
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct AliasEntry {
559    pub command: String,
560    pub alias: String,
561}
562
563/// Thresholds for detecting and throttling repetitive agent tool call loops.
564#[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/// Semantic-embedding engine settings.
594///
595/// `model` selects which local ONNX embedding model lean-ctx downloads and uses for
596/// `ctx_semantic_search`. Accepts the same aliases as the `LEAN_CTX_EMBEDDING_MODEL` env
597/// var: `minilm` (all-MiniLM-L6-v2, 384d — the default), `jina-code-v2` (768d,
598/// code-optimized), `nomic` (768d) — or any HuggingFace repo with an ONNX export via
599/// `hf:org/repo[@revision]` (GL #397). When the env var is set it takes precedence; an
600/// unset/`None` value uses the default model. Switching models triggers a one-time
601/// re-index on the next semantic search (vector dimensions follow from the model).
602///
603/// `dimensions` is only consulted for `hf:` custom models as the declared fallback
604/// width; the real width is probed from the ONNX graph at load time. Built-ins ignore it.
605#[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    /// Allow downloading the embedding model on first semantic need (#551).
613    /// `None` (unset) means **allowed** — the soft default that activates the
614    /// semantic features without manual setup. Set `false` for air-gapped
615    /// machines. The `LEAN_CTX_EMBEDDINGS_AUTO_DOWNLOAD` env var, when set,
616    /// overrides this in either direction.
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub auto_download: Option<bool>,
619}