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 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 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 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 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}