Skip to main content

lean_ctx/cli/
config_cmd.rs

1use crate::core::config;
2use crate::core::theme;
3
4pub fn cmd_config(args: &[String]) {
5    let cfg = config::Config::load();
6
7    if args.is_empty() {
8        println!("{}", cfg.show());
9        return;
10    }
11
12    match args[0].as_str() {
13        "init" | "create" => {
14            let full = args.iter().any(|a| a == "--full");
15            if full {
16                let default = config::Config::default();
17                match default.save() {
18                    Ok(()) => {
19                        let path = config::Config::path().map_or_else(
20                            || "~/.lean-ctx/config.toml".to_string(),
21                            |p| p.to_string_lossy().to_string(),
22                        );
23                        println!("Created full config at {path}");
24                    }
25                    Err(e) => eprintln!("Error: {e}"),
26                }
27            } else {
28                match write_simplified_config() {
29                    Ok(path) => println!("Created simplified config at {path}"),
30                    Err(e) => eprintln!("Error: {e}"),
31                }
32            }
33        }
34        "set" => {
35            if args.len() < 3 {
36                eprintln!("Usage: lean-ctx config set <key> <value>");
37                std::process::exit(1);
38            }
39            let mut cfg = cfg;
40            let key = &args[1];
41            let val = &args[2];
42            match key.as_str() {
43                "ultra_compact" => cfg.ultra_compact = val == "true",
44                "tee_on_error" | "tee_mode" => {
45                    cfg.tee_mode = match val.as_str() {
46                        "true" | "failures" => config::TeeMode::Failures,
47                        "highcompression" | "high_compression" => config::TeeMode::HighCompression,
48                        "always" => config::TeeMode::Always,
49                        "false" | "never" => config::TeeMode::Never,
50                        _ => {
51                            eprintln!(
52                                "Valid tee_mode values: always, highcompression, failures, never"
53                            );
54                            std::process::exit(1);
55                        }
56                    };
57                }
58                "checkpoint_interval" => {
59                    cfg.checkpoint_interval = val.parse().unwrap_or(15);
60                }
61                "theme" => {
62                    if theme::from_preset(val).is_some() || val == "custom" {
63                        cfg.theme.clone_from(val);
64                    } else {
65                        eprintln!(
66                            "Unknown theme '{val}'. Available: {}",
67                            theme::PRESET_NAMES.join(", ")
68                        );
69                        std::process::exit(1);
70                    }
71                }
72                "slow_command_threshold_ms" => {
73                    cfg.slow_command_threshold_ms = val.parse().unwrap_or(5000);
74                }
75                "passthrough_urls" => {
76                    cfg.passthrough_urls = val.split(',').map(|s| s.trim().to_string()).collect();
77                }
78                "excluded_commands" => {
79                    cfg.excluded_commands = val
80                        .split(',')
81                        .map(|s| s.trim().to_string())
82                        .filter(|s| !s.is_empty())
83                        .collect();
84                }
85                "rules_scope" => match val.as_str() {
86                    "global" | "project" | "both" => {
87                        cfg.rules_scope = Some(val.clone());
88                    }
89                    _ => {
90                        eprintln!("Valid rules_scope values: global, project, both");
91                        std::process::exit(1);
92                    }
93                },
94                "project_root" => {
95                    let path = std::path::Path::new(val.as_str());
96                    if !path.exists() || !path.is_dir() {
97                        eprintln!("Error: '{val}' is not an existing directory.");
98                        std::process::exit(1);
99                    }
100                    cfg.project_root = Some(val.clone());
101                }
102                "proxy.anthropic_upstream" => {
103                    cfg.proxy.anthropic_upstream = normalize_optional_upstream(val);
104                }
105                "proxy.openai_upstream" => {
106                    cfg.proxy.openai_upstream = normalize_optional_upstream(val);
107                }
108                "proxy.gemini_upstream" => {
109                    cfg.proxy.gemini_upstream = normalize_optional_upstream(val);
110                }
111                _ => {
112                    eprintln!("Unknown config key: {key}");
113                    std::process::exit(1);
114                }
115            }
116            match cfg.save() {
117                Ok(()) => println!("Updated {key} = {val}"),
118                Err(e) => eprintln!("Error saving config: {e}"),
119            }
120        }
121        "schema" => {
122            let schema = config::schema::ConfigSchema::generate();
123            println!(
124                "{}",
125                serde_json::to_string_pretty(&schema).unwrap_or_else(|_| "{}".to_string())
126            );
127        }
128        "validate" => {
129            cmd_validate();
130        }
131        "show" | "effective" => {
132            cmd_show_effective();
133        }
134        "apply" | "reload" => {
135            cmd_apply();
136        }
137        _ => {
138            eprintln!("Usage: lean-ctx config [init|set|show|schema|validate|apply]");
139            std::process::exit(1);
140        }
141    }
142}
143
144fn cmd_apply() {
145    use crate::daemon;
146    use crate::ipc;
147
148    println!("Applying config changes…");
149
150    // 1. Validate config first
151    println!("\n[1/4] Validating config…");
152    let schema = config::schema::ConfigSchema::generate();
153    let known = schema.known_keys();
154    let cfg = config::Config::load();
155
156    if let Some(path) = config::Config::path() {
157        if path.exists() {
158            if let Ok(raw) = std::fs::read_to_string(&path) {
159                if let Ok(table) = raw.parse::<toml::Table>() {
160                    let mut user_keys = Vec::new();
161                    fn collect_flat(table: &toml::Table, prefix: &str, out: &mut Vec<String>) {
162                        for (k, v) in table {
163                            let full = if prefix.is_empty() {
164                                k.clone()
165                            } else {
166                                format!("{prefix}.{k}")
167                            };
168                            if let toml::Value::Table(sub) = v {
169                                collect_flat(sub, &full, out);
170                            } else {
171                                out.push(full);
172                            }
173                        }
174                    }
175                    collect_flat(&table, "", &mut user_keys);
176                    let warnings: Vec<_> = user_keys
177                        .iter()
178                        .filter(|uk| {
179                            !known.contains(uk)
180                                && !known.iter().any(|k| uk.starts_with(&format!("{k}.")))
181                        })
182                        .collect();
183                    if warnings.is_empty() {
184                        println!("  ✓ All config keys valid.");
185                    } else {
186                        for w in &warnings {
187                            eprintln!("  [WARN] Unknown key: {w}");
188                        }
189                        eprintln!(
190                            "  {} unknown key(s) found. Continuing anyway…",
191                            warnings.len()
192                        );
193                    }
194                }
195            }
196        }
197    }
198
199    // 2. Restart processes
200    println!("\n[2/4] Restarting processes…");
201    crate::proxy_autostart::stop();
202
203    if let Err(e) = daemon::stop_daemon() {
204        eprintln!("  Warning: daemon stop: {e}");
205    }
206
207    let orphans = ipc::process::kill_all_by_name("lean-ctx");
208    if orphans > 0 {
209        println!("  Terminated {orphans} orphan process(es).");
210    }
211
212    std::thread::sleep(std::time::Duration::from_millis(500));
213
214    let remaining = ipc::process::find_pids_by_name("lean-ctx");
215    if !remaining.is_empty() {
216        for &pid in &remaining {
217            let _ = ipc::process::force_kill(pid);
218        }
219        std::thread::sleep(std::time::Duration::from_millis(300));
220    }
221
222    daemon::cleanup_daemon_files();
223    crate::proxy_autostart::start();
224
225    match daemon::start_daemon(&[]) {
226        Ok(()) => println!("  ✓ Daemon restarted."),
227        Err(e) => {
228            eprintln!("  ✗ Daemon start failed: {e}");
229            std::process::exit(1);
230        }
231    }
232
233    // 3. Safety checks
234    println!("\n[3/4] Running safety checks…");
235    println!("  RAM guard: max {}% system", cfg.max_ram_percent);
236
237    if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
238        let sessions_dir = data_dir.join("sessions");
239        let session_count = std::fs::read_dir(&sessions_dir)
240            .map_or(0, |rd| rd.filter_map(std::result::Result::ok).count());
241        println!("  Sessions dir: {session_count} files");
242    }
243
244    // 4. Summary
245    println!("\n[4/4] Config applied successfully.");
246    println!("  Theme:       {}", cfg.theme);
247    println!("  Ultra compact: {}", cfg.ultra_compact);
248    println!("  Checkpoint:  every {} calls", cfg.checkpoint_interval);
249    if let Some(ref root) = cfg.project_root {
250        println!("  Project root: {root}");
251    }
252}
253
254fn cmd_validate() {
255    let schema = config::schema::ConfigSchema::generate();
256    let known = schema.known_keys();
257
258    let path = match config::Config::path() {
259        Some(p) if p.exists() => p,
260        _ => {
261            println!("[OK] No config.toml found — using defaults.");
262            return;
263        }
264    };
265
266    let raw = match std::fs::read_to_string(&path) {
267        Ok(s) => s,
268        Err(e) => {
269            eprintln!("[ERROR] Cannot read {}: {e}", path.display());
270            std::process::exit(1);
271        }
272    };
273
274    let table: toml::Table = match raw.parse() {
275        Ok(t) => t,
276        Err(e) => {
277            eprintln!("[ERROR] Invalid TOML: {e}");
278            std::process::exit(1);
279        }
280    };
281
282    let mut warnings = 0u32;
283    let mut validated = 0u32;
284
285    fn collect_keys(table: &toml::Table, prefix: &str, out: &mut Vec<String>) {
286        for (k, v) in table {
287            let full = if prefix.is_empty() {
288                k.clone()
289            } else {
290                format!("{prefix}.{k}")
291            };
292            match v {
293                toml::Value::Table(sub) => collect_keys(sub, &full, out),
294                toml::Value::Array(arr) => {
295                    out.push(full.clone());
296                    for item in arr {
297                        if let toml::Value::Table(sub) = item {
298                            for sk in sub.keys() {
299                                out.push(format!("{full}[].{sk}"));
300                            }
301                        }
302                    }
303                }
304                _ => out.push(full),
305            }
306        }
307    }
308
309    let mut user_keys = Vec::new();
310    collect_keys(&table, "", &mut user_keys);
311
312    for uk in &user_keys {
313        let base = uk.split("[].").next().unwrap_or(uk);
314        let field = uk.rsplit("[].").next().unwrap_or("");
315        let check_key = if uk.contains("[].") {
316            format!("{base}.{field}")
317        } else {
318            uk.clone()
319        };
320
321        if known.contains(&check_key)
322            || known
323                .iter()
324                .any(|k| check_key.starts_with(&format!("{k}.")))
325        {
326            validated += 1;
327        } else {
328            warnings += 1;
329            let suggestion = find_closest(&check_key, &known);
330            if let Some(sug) = suggestion {
331                eprintln!("[WARN] Unknown key '{uk}' -- did you mean '{sug}'?");
332            } else {
333                eprintln!("[WARN] Unknown key '{uk}' -- this field does not exist");
334            }
335        }
336    }
337
338    let cfg = config::Config::load();
339    let budget = cfg.max_disk_mb_effective();
340    if budget > 0 {
341        let explicit_archive = cfg.archive.max_disk_mb;
342        let explicit_bm25 = cfg.bm25_max_cache_mb;
343        let sum = explicit_archive + explicit_bm25;
344        if sum > budget {
345            warnings += 1;
346            println!(
347                "  ⚠ max_disk_mb={budget} but archive.max_disk_mb({explicit_archive}) + bm25_max_cache_mb({explicit_bm25}) = {sum} exceeds budget"
348            );
349        }
350    }
351
352    let total = validated + warnings;
353    if warnings == 0 {
354        println!(
355            "[OK] All {total} keys validated successfully ({}).",
356            path.display()
357        );
358    } else {
359        println!(
360            "[RESULT] {validated} of {total} keys validated, {warnings} unknown ({}).",
361            path.display()
362        );
363        std::process::exit(1);
364    }
365}
366
367fn find_closest(needle: &str, haystack: &[String]) -> Option<String> {
368    let mut best: Option<(usize, &str)> = None;
369    for candidate in haystack {
370        let d = levenshtein(needle, candidate);
371        if d <= 3 && (best.is_none() || d < best.unwrap().0) {
372            best = Some((d, candidate));
373        }
374    }
375    if best.is_some() {
376        return best.map(|(_, s)| s.to_string());
377    }
378    let leaf = needle.rsplit('.').next().unwrap_or(needle);
379    let mut leaf_best: Option<(usize, &str)> = None;
380    for candidate in haystack {
381        let cand_leaf = candidate.rsplit('.').next().unwrap_or(candidate);
382        let d = levenshtein(leaf, cand_leaf);
383        if d <= 2 && (leaf_best.is_none() || d < leaf_best.unwrap().0) {
384            leaf_best = Some((d, candidate));
385        }
386    }
387    leaf_best.map(|(_, s)| s.to_string())
388}
389
390fn levenshtein(a: &str, b: &str) -> usize {
391    let a: Vec<char> = a.chars().collect();
392    let b: Vec<char> = b.chars().collect();
393    let (m, n) = (a.len(), b.len());
394    let mut dp = vec![vec![0usize; n + 1]; m + 1];
395    for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
396        row[0] = i;
397    }
398    for (j, val) in dp[0].iter_mut().enumerate().take(n + 1) {
399        *val = j;
400    }
401    for i in 1..=m {
402        for j in 1..=n {
403            let cost = usize::from(a[i - 1] != b[j - 1]);
404            dp[i][j] = (dp[i - 1][j] + 1)
405                .min(dp[i][j - 1] + 1)
406                .min(dp[i - 1][j - 1] + cost);
407        }
408    }
409    dp[m][n]
410}
411
412fn normalize_optional_upstream(value: &str) -> Option<String> {
413    use crate::core::config::normalize_url_opt;
414    let trimmed = value.trim();
415    if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("default") {
416        None
417    } else {
418        normalize_url_opt(trimmed)
419    }
420}
421
422pub fn cmd_benchmark(args: &[String]) {
423    use crate::core::benchmark;
424
425    let action = args.first().map_or("run", std::string::String::as_str);
426
427    match action {
428        "--help" | "-h" => {
429            println!("Usage: lean-ctx benchmark run [path] [--json]");
430            println!("       lean-ctx benchmark report [path]");
431        }
432        "run" => {
433            let path = args.get(1).map_or(".", std::string::String::as_str);
434            let is_json = args.iter().any(|a| a == "--json");
435
436            let result = benchmark::run_project_benchmark(path);
437            if is_json {
438                println!("{}", benchmark::format_json(&result));
439            } else {
440                println!("{}", benchmark::format_terminal(&result));
441            }
442        }
443        "report" => {
444            let path = args.get(1).map_or(".", std::string::String::as_str);
445            let result = benchmark::run_project_benchmark(path);
446            println!("{}", benchmark::format_markdown(&result));
447        }
448        _ => {
449            if std::path::Path::new(action).exists() {
450                let result = benchmark::run_project_benchmark(action);
451                println!("{}", benchmark::format_terminal(&result));
452            } else {
453                eprintln!("Usage: lean-ctx benchmark run [path] [--json]");
454                eprintln!("       lean-ctx benchmark report [path]");
455                std::process::exit(1);
456            }
457        }
458    }
459}
460
461pub fn cmd_stats(args: &[String]) {
462    match args.first().map(std::string::String::as_str) {
463        Some("reset-cep") => {
464            crate::core::stats::reset_cep();
465            println!("CEP stats reset. Shell hook data preserved.");
466        }
467        Some("json") => {
468            let store = crate::core::stats::load();
469            println!(
470                "{}",
471                serde_json::to_string_pretty(&store).unwrap_or_else(|_| "{}".to_string())
472            );
473        }
474        _ => {
475            let store = crate::core::stats::load();
476            let input_saved = store
477                .total_input_tokens
478                .saturating_sub(store.total_output_tokens);
479            let pct = if store.total_input_tokens > 0 {
480                input_saved as f64 / store.total_input_tokens as f64 * 100.0
481            } else {
482                0.0
483            };
484            println!("Commands:    {}", store.total_commands);
485            println!("Input:       {} tokens", store.total_input_tokens);
486            println!("Output:      {} tokens", store.total_output_tokens);
487            println!("Saved:       {input_saved} tokens ({pct:.1}%)");
488            println!();
489            println!("CEP sessions:  {}", store.cep.sessions);
490            println!(
491                "CEP tokens:    {} → {}",
492                store.cep.total_tokens_original, store.cep.total_tokens_compressed
493            );
494            println!();
495            println!("Subcommands: stats reset-cep | stats json");
496        }
497    }
498}
499
500pub fn cmd_cache(args: &[String]) {
501    use crate::core::cli_cache;
502    match args.first().map(std::string::String::as_str) {
503        Some("clear") => {
504            let count = cli_cache::clear();
505            println!("Cleared {count} cached entries.");
506        }
507        Some("reset") => {
508            let project_flag = args.get(1).map(std::string::String::as_str) == Some("--project");
509            if project_flag {
510                let root =
511                    crate::core::session::SessionState::load_latest().and_then(|s| s.project_root);
512                if let Some(root) = root {
513                    let count = cli_cache::clear_project(&root);
514                    println!("Reset {count} cache entries for project: {root}");
515                } else {
516                    eprintln!("No active project root found. Start a session first.");
517                    std::process::exit(1);
518                }
519            } else {
520                let count = cli_cache::clear();
521                println!("Reset all {count} cache entries.");
522            }
523        }
524        Some("stats") => {
525            let (hits, reads, entries) = cli_cache::stats();
526            let rate = if reads > 0 {
527                (hits as f64 / reads as f64 * 100.0).round() as u32
528            } else {
529                0
530            };
531            println!("CLI Cache Stats:");
532            println!("  Entries:   {entries}");
533            println!("  Reads:     {reads}");
534            println!("  Hits:      {hits}");
535            println!("  Hit Rate:  {rate}%");
536        }
537        Some("invalidate") => {
538            if args.len() < 2 {
539                eprintln!("Usage: lean-ctx cache invalidate <path>");
540                std::process::exit(1);
541            }
542            cli_cache::invalidate(&args[1]);
543            println!("Invalidated cache for {}", args[1]);
544        }
545        Some("prune") => {
546            let bm25 = prune_bm25_caches();
547            let graph = prune_graph_caches();
548            let removed = bm25.removed + graph.removed;
549            let freed = bm25.bytes_freed + graph.bytes_freed;
550            println!(
551                "Pruned {} entries, freed {:.1} MB (BM25: {}, graphs: {})",
552                removed,
553                freed as f64 / 1_048_576.0,
554                bm25.removed,
555                graph.removed,
556            );
557        }
558        _ => {
559            let (hits, reads, entries) = cli_cache::stats();
560            let rate = if reads > 0 {
561                (hits as f64 / reads as f64 * 100.0).round() as u32
562            } else {
563                0
564            };
565            println!("CLI File Cache: {entries} entries, {hits}/{reads} hits ({rate}%)");
566            println!();
567            println!("Subcommands:");
568            println!("  cache stats       Show detailed stats");
569            println!("  cache clear       Clear all cached entries");
570            println!("  cache reset       Reset all cache (or --project for current project only)");
571            println!("  cache invalidate  Remove specific file from cache");
572            println!(
573                "  cache prune       Remove oversized, quarantined, and orphaned indexes (BM25 + graphs)"
574            );
575        }
576    }
577}
578
579pub struct PruneResult {
580    pub scanned: u32,
581    pub removed: u32,
582    pub bytes_freed: u64,
583}
584
585pub fn prune_bm25_caches() -> PruneResult {
586    let mut result = PruneResult {
587        scanned: 0,
588        removed: 0,
589        bytes_freed: 0,
590    };
591
592    let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
593        return result;
594    };
595    let vectors_dir = data_dir.join("vectors");
596    let Ok(entries) = std::fs::read_dir(&vectors_dir) else {
597        return result;
598    };
599
600    let max_bytes = crate::core::config::Config::load().bm25_max_cache_mb_effective() * 1024 * 1024;
601
602    for entry in entries.flatten() {
603        let dir = entry.path();
604        if !dir.is_dir() {
605            continue;
606        }
607        result.scanned += 1;
608
609        for q_name in &[
610            "bm25_index.json.quarantined",
611            "bm25_index.bin.quarantined",
612            "bm25_index.bin.zst.quarantined",
613        ] {
614            let quarantined = dir.join(q_name);
615            if quarantined.exists() {
616                if let Ok(meta) = std::fs::metadata(&quarantined) {
617                    result.bytes_freed += meta.len();
618                }
619                let _ = std::fs::remove_file(&quarantined);
620                result.removed += 1;
621                println!("  Removed quarantined: {}", quarantined.display());
622            }
623        }
624
625        let index_path = if dir.join("bm25_index.bin.zst").exists() {
626            dir.join("bm25_index.bin.zst")
627        } else if dir.join("bm25_index.bin").exists() {
628            dir.join("bm25_index.bin")
629        } else {
630            dir.join("bm25_index.json")
631        };
632        if let Ok(meta) = std::fs::metadata(&index_path) {
633            if meta.len() > max_bytes {
634                result.bytes_freed += meta.len();
635                let _ = std::fs::remove_file(&index_path);
636                result.removed += 1;
637                println!(
638                    "  Removed oversized ({:.1} MB): {}",
639                    meta.len() as f64 / 1_048_576.0,
640                    index_path.display()
641                );
642            }
643        }
644
645        let marker = dir.join("project_root.txt");
646        if let Ok(root_str) = std::fs::read_to_string(&marker) {
647            let root_path = std::path::Path::new(root_str.trim());
648            if !root_path.exists() {
649                let freed = dir_size(&dir);
650                result.bytes_freed += freed;
651                let _ = std::fs::remove_dir_all(&dir);
652                result.removed += 1;
653                println!(
654                    "  Removed orphaned ({:.1} MB, project gone: {}): {}",
655                    freed as f64 / 1_048_576.0,
656                    root_str.trim(),
657                    dir.display()
658                );
659            }
660        }
661    }
662
663    result
664}
665
666pub fn prune_graph_caches() -> PruneResult {
667    let mut result = PruneResult {
668        scanned: 0,
669        removed: 0,
670        bytes_freed: 0,
671    };
672
673    let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
674        return result;
675    };
676    let graphs_dir = data_dir.join("graphs");
677    let Ok(entries) = std::fs::read_dir(&graphs_dir) else {
678        return result;
679    };
680
681    for entry in entries.flatten() {
682        let dir = entry.path();
683        if !dir.is_dir() {
684            continue;
685        }
686        result.scanned += 1;
687
688        let index_path = dir.join("index.json.zst");
689        let index_json = dir.join("index.json");
690
691        let has_index = index_path.exists() || index_json.exists();
692        if !has_index {
693            continue;
694        }
695
696        let idx_file = if index_path.exists() {
697            &index_path
698        } else {
699            &index_json
700        };
701
702        let root_from_index = try_read_project_root_from_graph(idx_file);
703        if let Some(root) = root_from_index {
704            if !root.is_empty() && !std::path::Path::new(&root).exists() {
705                let freed = dir_size(&dir);
706                result.bytes_freed += freed;
707                let _ = std::fs::remove_dir_all(&dir);
708                result.removed += 1;
709                println!(
710                    "  Removed orphaned graph ({:.1} MB, project gone: {}): {}",
711                    freed as f64 / 1_048_576.0,
712                    root,
713                    dir.display()
714                );
715                continue;
716            }
717        }
718
719        if let Ok(meta) = std::fs::metadata(idx_file) {
720            if meta.len() > 100 * 1024 * 1024 {
721                result.bytes_freed += meta.len();
722                let _ = std::fs::remove_file(idx_file);
723                result.removed += 1;
724                println!(
725                    "  Removed oversized graph ({:.1} MB): {}",
726                    meta.len() as f64 / 1_048_576.0,
727                    idx_file.display()
728                );
729            }
730        }
731    }
732
733    result
734}
735
736fn try_read_project_root_from_graph(path: &std::path::Path) -> Option<String> {
737    let data = if path.extension().and_then(|e| e.to_str()) == Some("zst") {
738        let compressed = std::fs::read(path).ok()?;
739        zstd::decode_all(compressed.as_slice()).ok()?
740    } else {
741        std::fs::read(path).ok()?
742    };
743    let content = String::from_utf8(data).ok()?;
744    let val: serde_json::Value = serde_json::from_str(&content).ok()?;
745    val.get("project_root")?.as_str().map(String::from)
746}
747
748pub const SIMPLIFIED_TEMPLATE: &str = r#"# lean-ctx — Simplified Configuration
749# Full reference: https://leanctx.com/docs/configuration
750# For all settings: lean-ctx config init --full
751
752# ── High-Level Knobs ─────────────────────────────────────────────────
753# These auto-adjust advanced settings. Override individual values below
754# only if you need fine-grained control.
755
756# Compression aggressiveness: off | lite | standard | max
757compression_level = "standard"
758
759# RAM/feature trade-off: low | balanced | performance
760memory_profile = "balanced"
761
762# Maximum % of system RAM lean-ctx may use (1-50)
763max_ram_percent = 5
764
765# Total disk budget in MB (0 = use individual limits).
766# Distributes proportionally: archive ~25%, BM25 cache ~10%.
767# max_disk_mb = 2000
768
769# Auto-purge data older than N days (0 = disabled).
770# Flows into archive.max_age_hours.
771# max_staleness_days = 30
772
773# Explicit project paths to scan/index (default: auto-detect).
774# [ide_paths]
775# cursor = ["/home/user/projects/app1"]
776
777# ── Proxy ────────────────────────────────────────────────────────────
778# proxy_enabled = false
779# proxy_port = 3128
780"#;
781
782fn write_simplified_config() -> Result<String, String> {
783    let path = config::Config::path().ok_or_else(|| "Cannot determine config path".to_string())?;
784    if let Some(dir) = path.parent() {
785        std::fs::create_dir_all(dir).map_err(|e| format!("{e}"))?;
786    }
787    std::fs::write(&path, SIMPLIFIED_TEMPLATE).map_err(|e| format!("{e}"))?;
788    Ok(path.to_string_lossy().to_string())
789}
790
791fn cmd_show_effective() {
792    let cfg = config::Config::load();
793    let compression = config::CompressionLevel::effective(&cfg);
794    let policy = cfg.memory_policy_effective().unwrap_or_default();
795
796    println!("╭─── Simplified (high-level) ───────────────────────────────╮");
797    println!(
798        "│ compression_level   = {:10}  {}",
799        format!("{compression:?}"),
800        source_hint(
801            "LEAN_CTX_COMPRESSION",
802            cfg.compression_level != config::CompressionLevel::Off
803        )
804    );
805    println!(
806        "│ max_disk_mb         = {:10}  {}",
807        cfg.max_disk_mb_effective(),
808        source_hint("LEAN_CTX_MAX_DISK_MB", cfg.max_disk_mb > 0)
809    );
810    println!(
811        "│ max_ram_percent     = {:10}  {}",
812        cfg.max_ram_percent,
813        source_hint("LEAN_CTX_MAX_RAM_PERCENT", cfg.max_ram_percent != 5)
814    );
815    println!(
816        "│ max_staleness_days  = {:10}  {}",
817        cfg.max_staleness_days_effective(),
818        source_hint("LEAN_CTX_MAX_STALENESS_DAYS", cfg.max_staleness_days > 0)
819    );
820    println!(
821        "│ memory_profile      = {:10}  {}",
822        format!("{:?}", cfg.memory_profile),
823        source_hint("LEAN_CTX_MEMORY_PROFILE", false)
824    );
825    println!("╰────────────────────────────────────────────────────────────╯");
826
827    println!();
828    println!("╭─── Derived effective limits ────────────────────────────────╮");
829    println!(
830        "│ archive_max_disk_mb    = {:>6} MB",
831        cfg.archive_max_disk_mb_effective()
832    );
833    println!(
834        "│ bm25_max_cache_mb      = {:>6} MB",
835        cfg.bm25_max_cache_mb_effective()
836    );
837    println!(
838        "│ archive_max_age_hours  = {:>6} h",
839        cfg.archive_max_age_hours_effective()
840    );
841    println!(
842        "│ graph_index_max_files  = {:>6}",
843        cfg.graph_index_max_files
844    );
845    println!("│");
846    println!(
847        "│ memory.knowledge.max_facts     = {:>6}",
848        policy.knowledge.max_facts
849    );
850    println!(
851        "│ memory.knowledge.max_patterns  = {:>6}",
852        policy.knowledge.max_patterns
853    );
854    println!(
855        "│ memory.episodic.max_episodes   = {:>6}",
856        policy.episodic.max_episodes
857    );
858    println!(
859        "│ memory.procedural.max_procedures = {:>4}",
860        policy.procedural.max_procedures
861    );
862    println!("╰────────────────────────────────────────────────────────────╯");
863
864    if cfg.max_disk_mb_effective() > 0 {
865        println!();
866        println!(
867            "  ℹ  max_disk_mb={} → limits scaled proportionally (factor: {:.1}x)",
868            cfg.max_disk_mb_effective(),
869            (cfg.max_disk_mb_effective() as f64 / 500.0).clamp(0.5, 10.0)
870        );
871    }
872}
873
874fn source_hint(env_var: &str, config_set: bool) -> &'static str {
875    if std::env::var(env_var).is_ok() {
876        "← env"
877    } else if config_set {
878        "← config"
879    } else {
880        "← default"
881    }
882}
883
884fn dir_size(path: &std::path::Path) -> u64 {
885    let mut total = 0u64;
886    if let Ok(entries) = std::fs::read_dir(path) {
887        for entry in entries.flatten() {
888            let p = entry.path();
889            if p.is_file() {
890                total += std::fs::metadata(&p).map_or(0, |m| m.len());
891            } else if p.is_dir() {
892                total += dir_size(&p);
893            }
894        }
895    }
896    total
897}