1use std::path::PathBuf;
2
3use crate::core::editor_registry::{ConfigType, EditorTarget, WriteAction, WriteOptions};
4use crate::core::portable_binary::resolve_portable_binary;
5use crate::core::setup_report::{PlatformInfo, SetupItem, SetupReport, SetupStepReport};
6use chrono::Utc;
7use std::ffi::OsString;
8
9pub fn claude_config_json_path(home: &std::path::Path) -> PathBuf {
10 crate::core::editor_registry::claude_mcp_json_path(home)
11}
12
13pub fn claude_config_dir(home: &std::path::Path) -> PathBuf {
14 crate::core::editor_registry::claude_state_dir(home)
15}
16
17pub(crate) struct EnvVarGuard {
18 key: &'static str,
19 previous: Option<OsString>,
20}
21
22impl EnvVarGuard {
23 pub(crate) fn set(key: &'static str, value: &str) -> Self {
24 let previous = std::env::var_os(key);
25 std::env::set_var(key, value);
26 Self { key, previous }
27 }
28}
29
30impl Drop for EnvVarGuard {
31 fn drop(&mut self) {
32 if let Some(previous) = &self.previous {
33 std::env::set_var(self.key, previous);
34 } else {
35 std::env::remove_var(self.key);
36 }
37 }
38}
39
40pub fn run_setup() {
41 use crate::terminal_ui;
42
43 if crate::shell::is_non_interactive() {
44 eprintln!("Non-interactive terminal detected (no TTY on stdin).");
45 eprintln!("Running in non-interactive mode (equivalent to: lean-ctx setup --non-interactive --yes)");
46 eprintln!();
47 let opts = SetupOptions {
48 non_interactive: true,
49 yes: true,
50 fix: false,
51 json: false,
52 };
53 match run_setup_with_options(opts) {
54 Ok(report) => {
55 if !report.warnings.is_empty() {
56 for w in &report.warnings {
57 tracing::warn!("{w}");
58 }
59 }
60 }
61 Err(e) => tracing::error!("Setup error: {e}"),
62 }
63 return;
64 }
65
66 let Some(home) = dirs::home_dir() else {
67 tracing::error!("Cannot determine home directory");
68 std::process::exit(1);
69 };
70
71 let binary = resolve_portable_binary();
72
73 let home_str = home.to_string_lossy().to_string();
74
75 terminal_ui::print_setup_header();
76
77 terminal_ui::print_step_header(1, 7, "Shell Hook");
79 crate::cli::cmd_init(&["--global".to_string()]);
80 crate::shell_hook::install_all(false);
81
82 terminal_ui::print_step_header(2, 7, "AI Tool Detection");
84
85 let targets = crate::core::editor_registry::build_targets(&home);
86 let mut newly_configured: Vec<&str> = Vec::new();
87 let mut already_configured: Vec<&str> = Vec::new();
88 let mut not_installed: Vec<&str> = Vec::new();
89 let mut errors: Vec<&str> = Vec::new();
90
91 for target in &targets {
92 let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
93
94 if !target.detect_path.exists() {
95 not_installed.push(target.name);
96 continue;
97 }
98
99 match crate::core::editor_registry::write_config_with_options(
100 target,
101 &binary,
102 WriteOptions {
103 overwrite_invalid: false,
104 },
105 ) {
106 Ok(res) if res.action == WriteAction::Already => {
107 terminal_ui::print_status_ok(&format!(
108 "{:<20} \x1b[2m{short_path}\x1b[0m",
109 target.name
110 ));
111 already_configured.push(target.name);
112 }
113 Ok(_) => {
114 terminal_ui::print_status_new(&format!(
115 "{:<20} \x1b[2m{short_path}\x1b[0m",
116 target.name
117 ));
118 newly_configured.push(target.name);
119 }
120 Err(e) => {
121 terminal_ui::print_status_warn(&format!("{}: {e}", target.name));
122 errors.push(target.name);
123 }
124 }
125 }
126
127 let total_ok = newly_configured.len() + already_configured.len();
128 if total_ok == 0 && errors.is_empty() {
129 terminal_ui::print_status_warn(
130 "No AI tools detected. Install one and re-run: lean-ctx setup",
131 );
132 }
133
134 if !not_installed.is_empty() {
135 println!(
136 " \x1b[2m○ {} not detected: {}\x1b[0m",
137 not_installed.len(),
138 not_installed.join(", ")
139 );
140 }
141
142 terminal_ui::print_step_header(3, 7, "Agent Rules");
144 let rules_result = crate::rules_inject::inject_all_rules(&home);
145 for name in &rules_result.injected {
146 terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules injected\x1b[0m"));
147 }
148 for name in &rules_result.updated {
149 terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules updated\x1b[0m"));
150 }
151 for name in &rules_result.already {
152 terminal_ui::print_status_ok(&format!("{name:<20} \x1b[2mrules up-to-date\x1b[0m"));
153 }
154 for err in &rules_result.errors {
155 terminal_ui::print_status_warn(err);
156 }
157 if rules_result.injected.is_empty()
158 && rules_result.updated.is_empty()
159 && rules_result.already.is_empty()
160 && rules_result.errors.is_empty()
161 {
162 terminal_ui::print_status_skip("No agent rules needed");
163 }
164
165 for target in &targets {
167 if !target.detect_path.exists() || target.agent_key.is_empty() {
168 continue;
169 }
170 crate::hooks::install_agent_hook(&target.agent_key, true);
171 }
172
173 terminal_ui::print_step_header(4, 7, "API Proxy");
175 crate::proxy_setup::install_proxy_env(&home, crate::proxy_setup::default_port(), false);
176 println!();
177 println!(" \x1b[2mStart proxy for maximum token savings:\x1b[0m");
178 println!(" \x1b[1mlean-ctx proxy start\x1b[0m");
179 println!(" \x1b[2mEnable autostart:\x1b[0m");
180 println!(" \x1b[1mlean-ctx proxy start --autostart\x1b[0m");
181
182 terminal_ui::print_step_header(5, 7, "Environment Check");
184 let lean_dir = home.join(".lean-ctx");
185 if lean_dir.exists() {
186 terminal_ui::print_status_ok("~/.lean-ctx/ ready");
187 } else {
188 let _ = std::fs::create_dir_all(&lean_dir);
189 terminal_ui::print_status_new("Created ~/.lean-ctx/");
190 }
191 crate::doctor::run_compact();
192
193 terminal_ui::print_step_header(6, 7, "Help Improve lean-ctx");
195 println!(" Share anonymous compression stats to make lean-ctx better.");
196 println!(" \x1b[1mNo code, no file names, no personal data — ever.\x1b[0m");
197 println!();
198 print!(" Enable anonymous data sharing? \x1b[1m[y/N]\x1b[0m ");
199 use std::io::Write;
200 std::io::stdout().flush().ok();
201
202 let mut input = String::new();
203 let contribute = if std::io::stdin().read_line(&mut input).is_ok() {
204 let answer = input.trim().to_lowercase();
205 answer == "y" || answer == "yes"
206 } else {
207 false
208 };
209
210 if contribute {
211 let config_dir = home.join(".lean-ctx");
212 let _ = std::fs::create_dir_all(&config_dir);
213 let config_path = config_dir.join("config.toml");
214 let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
215 if !config_content.contains("[cloud]") {
216 if !config_content.is_empty() && !config_content.ends_with('\n') {
217 config_content.push('\n');
218 }
219 config_content.push_str("\n[cloud]\ncontribute_enabled = true\n");
220 let _ = std::fs::write(&config_path, config_content);
221 }
222 terminal_ui::print_status_ok("Enabled — thank you!");
223 } else {
224 terminal_ui::print_status_skip("Skipped — enable later with: lean-ctx config");
225 }
226
227 terminal_ui::print_step_header(7, 7, "Premium Features");
229 configure_premium_features(&home);
230
231 println!();
233 println!(
234 " \x1b[1;32m✓ Setup complete!\x1b[0m \x1b[1m{}\x1b[0m configured, \x1b[2m{} already set, {} skipped\x1b[0m",
235 newly_configured.len(),
236 already_configured.len(),
237 not_installed.len()
238 );
239
240 if !errors.is_empty() {
241 println!(
242 " \x1b[33m⚠ {} error{}: {}\x1b[0m",
243 errors.len(),
244 if errors.len() == 1 { "" } else { "s" },
245 errors.join(", ")
246 );
247 }
248
249 let shell = std::env::var("SHELL").unwrap_or_default();
251 let source_cmd = if shell.contains("zsh") {
252 "source ~/.zshrc"
253 } else if shell.contains("fish") {
254 "source ~/.config/fish/config.fish"
255 } else if shell.contains("bash") {
256 "source ~/.bashrc"
257 } else {
258 "Restart your shell"
259 };
260
261 let dim = "\x1b[2m";
262 let bold = "\x1b[1m";
263 let cyan = "\x1b[36m";
264 let yellow = "\x1b[33m";
265 let rst = "\x1b[0m";
266
267 println!();
268 println!(" {bold}Next steps:{rst}");
269 println!();
270 println!(" {cyan}1.{rst} Reload your shell:");
271 println!(" {bold}{source_cmd}{rst}");
272 println!();
273
274 let mut tools_to_restart: Vec<String> = newly_configured
275 .iter()
276 .map(std::string::ToString::to_string)
277 .collect();
278 for name in rules_result
279 .injected
280 .iter()
281 .chain(rules_result.updated.iter())
282 {
283 if !tools_to_restart.iter().any(|t| t == name) {
284 tools_to_restart.push(name.clone());
285 }
286 }
287
288 if !tools_to_restart.is_empty() {
289 println!(" {cyan}2.{rst} {yellow}{bold}Restart your IDE / AI tool:{rst}");
290 println!(" {bold}{}{rst}", tools_to_restart.join(", "));
291 println!(
292 " {dim}The MCP connection must be re-established for changes to take effect.{rst}"
293 );
294 println!(" {dim}Close and re-open the application completely.{rst}");
295 } else if !already_configured.is_empty() {
296 println!(
297 " {cyan}2.{rst} {dim}Your tools are already configured — no restart needed.{rst}"
298 );
299 }
300
301 println!();
302 println!(
303 " {dim}After restart, lean-ctx will automatically optimize every AI interaction.{rst}"
304 );
305 println!(" {dim}Verify with:{rst} {bold}lean-ctx gain{rst}");
306
307 println!();
309 terminal_ui::print_logo_animated();
310 terminal_ui::print_command_box();
311}
312
313#[derive(Debug, Clone, Copy, Default)]
314pub struct SetupOptions {
315 pub non_interactive: bool,
316 pub yes: bool,
317 pub fix: bool,
318 pub json: bool,
319}
320
321pub fn run_setup_with_options(opts: SetupOptions) -> Result<SetupReport, String> {
322 let _quiet_guard = opts.json.then(|| EnvVarGuard::set("LEAN_CTX_QUIET", "1"));
323 let started_at = Utc::now();
324 let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
325 let binary = resolve_portable_binary();
326 let home_str = home.to_string_lossy().to_string();
327
328 let mut steps: Vec<SetupStepReport> = Vec::new();
329
330 let mut shell_step = SetupStepReport {
332 name: "shell_hook".to_string(),
333 ok: true,
334 items: Vec::new(),
335 warnings: Vec::new(),
336 errors: Vec::new(),
337 };
338 if !opts.non_interactive || opts.yes {
339 if opts.json {
340 crate::cli::cmd_init_quiet(&["--global".to_string()]);
341 } else {
342 crate::cli::cmd_init(&["--global".to_string()]);
343 }
344 crate::shell_hook::install_all(opts.json);
345 shell_step.items.push(SetupItem {
346 name: "init --global".to_string(),
347 status: "ran".to_string(),
348 path: None,
349 note: None,
350 });
351 shell_step.items.push(SetupItem {
352 name: "universal_shell_hook".to_string(),
353 status: "installed".to_string(),
354 path: None,
355 note: Some("~/.zshenv, ~/.bashenv, agent aliases".to_string()),
356 });
357 } else {
358 shell_step
359 .warnings
360 .push("non_interactive_without_yes: shell hook not installed (use --yes)".to_string());
361 shell_step.ok = false;
362 shell_step.items.push(SetupItem {
363 name: "init --global".to_string(),
364 status: "skipped".to_string(),
365 path: None,
366 note: Some("requires --yes in --non-interactive mode".to_string()),
367 });
368 }
369 steps.push(shell_step);
370
371 let mut editor_step = SetupStepReport {
373 name: "editors".to_string(),
374 ok: true,
375 items: Vec::new(),
376 warnings: Vec::new(),
377 errors: Vec::new(),
378 };
379
380 let targets = crate::core::editor_registry::build_targets(&home);
381 for target in &targets {
382 let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
383 if !target.detect_path.exists() {
384 editor_step.items.push(SetupItem {
385 name: target.name.to_string(),
386 status: "not_detected".to_string(),
387 path: Some(short_path),
388 note: None,
389 });
390 continue;
391 }
392
393 let res = crate::core::editor_registry::write_config_with_options(
394 target,
395 &binary,
396 WriteOptions {
397 overwrite_invalid: opts.fix,
398 },
399 );
400 match res {
401 Ok(w) => {
402 editor_step.items.push(SetupItem {
403 name: target.name.to_string(),
404 status: match w.action {
405 WriteAction::Created => "created".to_string(),
406 WriteAction::Updated => "updated".to_string(),
407 WriteAction::Already => "already".to_string(),
408 },
409 path: Some(short_path),
410 note: w.note,
411 });
412 }
413 Err(e) => {
414 editor_step.ok = false;
415 editor_step.items.push(SetupItem {
416 name: target.name.to_string(),
417 status: "error".to_string(),
418 path: Some(short_path),
419 note: Some(e),
420 });
421 }
422 }
423 }
424 steps.push(editor_step);
425
426 let mut rules_step = SetupStepReport {
428 name: "agent_rules".to_string(),
429 ok: true,
430 items: Vec::new(),
431 warnings: Vec::new(),
432 errors: Vec::new(),
433 };
434 let rules_result = crate::rules_inject::inject_all_rules(&home);
435 for n in rules_result.injected {
436 rules_step.items.push(SetupItem {
437 name: n,
438 status: "injected".to_string(),
439 path: None,
440 note: None,
441 });
442 }
443 for n in rules_result.updated {
444 rules_step.items.push(SetupItem {
445 name: n,
446 status: "updated".to_string(),
447 path: None,
448 note: None,
449 });
450 }
451 for n in rules_result.already {
452 rules_step.items.push(SetupItem {
453 name: n,
454 status: "already".to_string(),
455 path: None,
456 note: None,
457 });
458 }
459 for e in rules_result.errors {
460 rules_step.ok = false;
461 rules_step.errors.push(e);
462 }
463 steps.push(rules_step);
464
465 let mut hooks_step = SetupStepReport {
467 name: "agent_hooks".to_string(),
468 ok: true,
469 items: Vec::new(),
470 warnings: Vec::new(),
471 errors: Vec::new(),
472 };
473 for target in &targets {
474 if !target.detect_path.exists() {
475 continue;
476 }
477 match target.agent_key.as_str() {
478 "codex" => {
479 crate::hooks::agents::install_codex_hook();
480 hooks_step.items.push(SetupItem {
481 name: "Codex integration".to_string(),
482 status: "installed".to_string(),
483 path: Some("~/.codex/".to_string()),
484 note: Some(
485 "Installs AGENTS/MCP guidance and Codex-compatible SessionStart/PreToolUse hooks."
486 .to_string(),
487 ),
488 });
489 }
490 "cursor" => {
491 let hooks_path = home.join(".cursor/hooks.json");
492 if !hooks_path.exists() {
493 crate::hooks::agents::install_cursor_hook(true);
494 hooks_step.items.push(SetupItem {
495 name: "Cursor hooks".to_string(),
496 status: "installed".to_string(),
497 path: Some("~/.cursor/hooks.json".to_string()),
498 note: None,
499 });
500 }
501 }
502 _ => {}
503 }
504 }
505 if !hooks_step.items.is_empty() {
506 steps.push(hooks_step);
507 }
508
509 let mut proxy_step = SetupStepReport {
511 name: "proxy_env".to_string(),
512 ok: true,
513 items: Vec::new(),
514 warnings: Vec::new(),
515 errors: Vec::new(),
516 };
517 crate::proxy_setup::install_proxy_env(&home, crate::proxy_setup::default_port(), opts.json);
518 proxy_step.items.push(SetupItem {
519 name: "proxy_env".to_string(),
520 status: "configured".to_string(),
521 path: None,
522 note: Some("ANTHROPIC_BASE_URL, OPENAI_BASE_URL, GEMINI_API_BASE_URL".to_string()),
523 });
524 steps.push(proxy_step);
525
526 let mut env_step = SetupStepReport {
528 name: "doctor_compact".to_string(),
529 ok: true,
530 items: Vec::new(),
531 warnings: Vec::new(),
532 errors: Vec::new(),
533 };
534 let (passed, total) = crate::doctor::compact_score();
535 env_step.items.push(SetupItem {
536 name: "doctor".to_string(),
537 status: format!("{passed}/{total}"),
538 path: None,
539 note: None,
540 });
541 if passed != total {
542 env_step.warnings.push(format!(
543 "doctor compact not fully passing: {passed}/{total}"
544 ));
545 }
546 steps.push(env_step);
547
548 let finished_at = Utc::now();
549 let success = steps.iter().all(|s| s.ok);
550 let report = SetupReport {
551 schema_version: 1,
552 started_at,
553 finished_at,
554 success,
555 platform: PlatformInfo {
556 os: std::env::consts::OS.to_string(),
557 arch: std::env::consts::ARCH.to_string(),
558 },
559 steps,
560 warnings: Vec::new(),
561 errors: Vec::new(),
562 };
563
564 let path = SetupReport::default_path()?;
565 let mut content =
566 serde_json::to_string_pretty(&report).map_err(|e| format!("serialize report: {e}"))?;
567 content.push('\n');
568 crate::config_io::write_atomic(&path, &content)?;
569
570 Ok(report)
571}
572
573pub fn configure_agent_mcp(agent: &str) -> Result<(), String> {
574 let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
575 let binary = resolve_portable_binary();
576
577 let mut targets = Vec::<EditorTarget>::new();
578
579 let push = |targets: &mut Vec<EditorTarget>,
580 name: &'static str,
581 config_path: PathBuf,
582 config_type: ConfigType| {
583 targets.push(EditorTarget {
584 name,
585 agent_key: agent.to_string(),
586 detect_path: PathBuf::from("/nonexistent"), config_path,
588 config_type,
589 });
590 };
591
592 match agent {
593 "cursor" => push(
594 &mut targets,
595 "Cursor",
596 home.join(".cursor/mcp.json"),
597 ConfigType::McpJson,
598 ),
599 "claude" | "claude-code" => push(
600 &mut targets,
601 "Claude Code",
602 crate::core::editor_registry::claude_mcp_json_path(&home),
603 ConfigType::McpJson,
604 ),
605 "windsurf" => push(
606 &mut targets,
607 "Windsurf",
608 home.join(".codeium/windsurf/mcp_config.json"),
609 ConfigType::McpJson,
610 ),
611 "codex" => push(
612 &mut targets,
613 "Codex CLI",
614 home.join(".codex/config.toml"),
615 ConfigType::Codex,
616 ),
617 "gemini" => {
618 push(
619 &mut targets,
620 "Gemini CLI",
621 home.join(".gemini/settings.json"),
622 ConfigType::GeminiSettings,
623 );
624 push(
625 &mut targets,
626 "Antigravity",
627 home.join(".gemini/antigravity/mcp_config.json"),
628 ConfigType::McpJson,
629 );
630 }
631 "antigravity" => push(
632 &mut targets,
633 "Antigravity",
634 home.join(".gemini/antigravity/mcp_config.json"),
635 ConfigType::McpJson,
636 ),
637 "copilot" => push(
638 &mut targets,
639 "VS Code / Copilot",
640 crate::core::editor_registry::vscode_mcp_path(),
641 ConfigType::VsCodeMcp,
642 ),
643 "crush" => push(
644 &mut targets,
645 "Crush",
646 home.join(".config/crush/crush.json"),
647 ConfigType::Crush,
648 ),
649 "pi" => push(
650 &mut targets,
651 "Pi Coding Agent",
652 home.join(".pi/agent/mcp.json"),
653 ConfigType::McpJson,
654 ),
655 "cline" => push(
656 &mut targets,
657 "Cline",
658 crate::core::editor_registry::cline_mcp_path(),
659 ConfigType::McpJson,
660 ),
661 "roo" => push(
662 &mut targets,
663 "Roo Code",
664 crate::core::editor_registry::roo_mcp_path(),
665 ConfigType::McpJson,
666 ),
667 "kiro" => push(
668 &mut targets,
669 "AWS Kiro",
670 home.join(".kiro/settings/mcp.json"),
671 ConfigType::McpJson,
672 ),
673 "verdent" => push(
674 &mut targets,
675 "Verdent",
676 home.join(".verdent/mcp.json"),
677 ConfigType::McpJson,
678 ),
679 "jetbrains" | "amp" => {
680 }
682 "qwen" => push(
683 &mut targets,
684 "Qwen Code",
685 home.join(".qwen/mcp.json"),
686 ConfigType::McpJson,
687 ),
688 "trae" => push(
689 &mut targets,
690 "Trae",
691 home.join(".trae/mcp.json"),
692 ConfigType::McpJson,
693 ),
694 "amazonq" => push(
695 &mut targets,
696 "Amazon Q Developer",
697 home.join(".aws/amazonq/mcp.json"),
698 ConfigType::McpJson,
699 ),
700 "opencode" => {
701 #[cfg(windows)]
702 let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
703 std::path::PathBuf::from(appdata)
704 .join("opencode")
705 .join("opencode.json")
706 } else {
707 home.join(".config/opencode/opencode.json")
708 };
709 #[cfg(not(windows))]
710 let opencode_path = home.join(".config/opencode/opencode.json");
711 push(
712 &mut targets,
713 "OpenCode",
714 opencode_path,
715 ConfigType::OpenCode,
716 );
717 }
718 "aider" => push(
719 &mut targets,
720 "Aider",
721 home.join(".aider/mcp.json"),
722 ConfigType::McpJson,
723 ),
724 "hermes" => push(
725 &mut targets,
726 "Hermes Agent",
727 home.join(".hermes/config.yaml"),
728 ConfigType::HermesYaml,
729 ),
730 _ => {
731 return Err(format!("Unknown agent '{agent}'"));
732 }
733 }
734
735 for t in &targets {
736 crate::core::editor_registry::write_config_with_options(
737 t,
738 &binary,
739 WriteOptions {
740 overwrite_invalid: true,
741 },
742 )?;
743 }
744
745 if agent == "kiro" {
746 install_kiro_steering(&home);
747 }
748
749 Ok(())
750}
751
752fn install_kiro_steering(home: &std::path::Path) {
753 let cwd = std::env::current_dir().unwrap_or_else(|_| home.to_path_buf());
754 let steering_dir = cwd.join(".kiro").join("steering");
755 let steering_file = steering_dir.join("lean-ctx.md");
756
757 if steering_file.exists()
758 && std::fs::read_to_string(&steering_file)
759 .unwrap_or_default()
760 .contains("lean-ctx")
761 {
762 println!(" Kiro steering file already exists at .kiro/steering/lean-ctx.md");
763 return;
764 }
765
766 let _ = std::fs::create_dir_all(&steering_dir);
767 let _ = std::fs::write(&steering_file, crate::hooks::KIRO_STEERING_TEMPLATE);
768 println!(" \x1b[32m✓\x1b[0m Created .kiro/steering/lean-ctx.md (Kiro will now prefer lean-ctx tools)");
769}
770
771fn shorten_path(path: &str, home: &str) -> String {
772 if let Some(stripped) = path.strip_prefix(home) {
773 format!("~{stripped}")
774 } else {
775 path.to_string()
776 }
777}
778
779fn upsert_toml_key(content: &mut String, key: &str, value: &str) {
780 let pattern = format!("{key} = ");
781 if let Some(start) = content.find(&pattern) {
782 let line_end = content[start..]
783 .find('\n')
784 .map_or(content.len(), |p| start + p);
785 content.replace_range(start..line_end, &format!("{key} = \"{value}\""));
786 } else {
787 if !content.is_empty() && !content.ends_with('\n') {
788 content.push('\n');
789 }
790 content.push_str(&format!("{key} = \"{value}\"\n"));
791 }
792}
793
794fn configure_premium_features(home: &std::path::Path) {
795 use crate::terminal_ui;
796 use std::io::Write;
797
798 let config_dir = home.join(".lean-ctx");
799 let _ = std::fs::create_dir_all(&config_dir);
800 let config_path = config_dir.join("config.toml");
801 let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
802
803 let dim = "\x1b[2m";
804 let bold = "\x1b[1m";
805 let rst = "\x1b[0m";
806
807 println!(
809 "\n {bold}Agent Output Optimization{rst} {dim}(reduces output tokens by 40-70%){rst}"
810 );
811 println!(
812 " {dim}Levels: lite (concise), full (max density), ultra (expert pair-programmer){rst}"
813 );
814 print!(" Terse agent mode? {bold}[off/lite/full/ultra]{rst} {dim}(default: off){rst} ");
815 std::io::stdout().flush().ok();
816
817 let mut terse_input = String::new();
818 let terse_level = if std::io::stdin().read_line(&mut terse_input).is_ok() {
819 match terse_input.trim().to_lowercase().as_str() {
820 "lite" => "lite",
821 "full" => "full",
822 "ultra" => "ultra",
823 _ => "off",
824 }
825 } else {
826 "off"
827 };
828
829 if terse_level != "off" {
830 upsert_toml_key(&mut config_content, "terse_agent", terse_level);
831 terminal_ui::print_status_ok(&format!("Terse agent: {terse_level}"));
832 } else if config_content.contains("terse_agent") {
833 upsert_toml_key(&mut config_content, "terse_agent", "off");
834 terminal_ui::print_status_ok("Terse agent: off");
835 } else {
836 terminal_ui::print_status_skip(
837 "Terse agent: off (change later with: lean-ctx terse <level>)",
838 );
839 }
840
841 println!(
843 "\n {bold}Tool Result Archive{rst} {dim}(zero-loss: large outputs archived, retrievable via ctx_expand){rst}"
844 );
845 print!(" Enable auto-archive? {bold}[Y/n]{rst} ");
846 std::io::stdout().flush().ok();
847
848 let mut archive_input = String::new();
849 let archive_on = if std::io::stdin().read_line(&mut archive_input).is_ok() {
850 let a = archive_input.trim().to_lowercase();
851 a.is_empty() || a == "y" || a == "yes"
852 } else {
853 true
854 };
855
856 if archive_on && !config_content.contains("[archive]") {
857 if !config_content.is_empty() && !config_content.ends_with('\n') {
858 config_content.push('\n');
859 }
860 config_content.push_str("\n[archive]\nenabled = true\n");
861 terminal_ui::print_status_ok("Tool Result Archive: enabled");
862 } else if !archive_on {
863 terminal_ui::print_status_skip("Archive: off (enable later in config.toml)");
864 }
865
866 println!(
868 "\n {bold}Output Density{rst} {dim}(compresses tool output: normal, terse, ultra){rst}"
869 );
870 print!(" Output density? {bold}[normal/terse/ultra]{rst} {dim}(default: normal){rst} ");
871 std::io::stdout().flush().ok();
872
873 let mut density_input = String::new();
874 let density = if std::io::stdin().read_line(&mut density_input).is_ok() {
875 match density_input.trim().to_lowercase().as_str() {
876 "terse" => "terse",
877 "ultra" => "ultra",
878 _ => "normal",
879 }
880 } else {
881 "normal"
882 };
883
884 if density != "normal" {
885 upsert_toml_key(&mut config_content, "output_density", density);
886 terminal_ui::print_status_ok(&format!("Output density: {density}"));
887 } else if config_content.contains("output_density") {
888 upsert_toml_key(&mut config_content, "output_density", "normal");
889 terminal_ui::print_status_ok("Output density: normal");
890 } else {
891 terminal_ui::print_status_skip("Output density: normal (change later in config.toml)");
892 }
893
894 let _ = std::fs::write(&config_path, config_content);
895}