Skip to main content

lean_ctx/
setup.rs

1use std::path::PathBuf;
2
3struct EditorTarget {
4    name: &'static str,
5    agent_key: &'static str,
6    config_path: PathBuf,
7    detect_path: PathBuf,
8    config_type: ConfigType,
9}
10
11enum ConfigType {
12    McpJson,
13    Zed,
14    Codex,
15    VsCodeMcp,
16    OpenCode,
17}
18
19pub fn run_setup() {
20    use crate::terminal_ui;
21
22    let home = match dirs::home_dir() {
23        Some(h) => h,
24        None => {
25            eprintln!("Cannot determine home directory");
26            std::process::exit(1);
27        }
28    };
29
30    let binary = resolve_portable_binary();
31
32    let home_str = home.to_string_lossy().to_string();
33
34    terminal_ui::print_setup_header();
35
36    // Step 1: Shell hook
37    terminal_ui::print_step_header(1, 5, "Shell Hook");
38    crate::cli::cmd_init(&["--global".to_string()]);
39
40    // Step 2: Editor auto-detection + configuration
41    terminal_ui::print_step_header(2, 5, "AI Tool Detection");
42
43    let targets = build_targets(&home, &binary);
44    let mut newly_configured: Vec<&str> = Vec::new();
45    let mut already_configured: Vec<&str> = Vec::new();
46    let mut not_installed: Vec<&str> = Vec::new();
47    let mut errors: Vec<&str> = Vec::new();
48
49    for target in &targets {
50        let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
51
52        if !target.detect_path.exists() {
53            not_installed.push(target.name);
54            continue;
55        }
56
57        let has_config = target.config_path.exists()
58            && std::fs::read_to_string(&target.config_path)
59                .map(|c| c.contains("lean-ctx"))
60                .unwrap_or(false);
61
62        if has_config {
63            terminal_ui::print_status_ok(&format!(
64                "{:<20} \x1b[2m{short_path}\x1b[0m",
65                target.name
66            ));
67            already_configured.push(target.name);
68            continue;
69        }
70
71        match write_config(target, &binary) {
72            Ok(()) => {
73                terminal_ui::print_status_new(&format!(
74                    "{:<20} \x1b[2m{short_path}\x1b[0m",
75                    target.name
76                ));
77                newly_configured.push(target.name);
78            }
79            Err(e) => {
80                terminal_ui::print_status_warn(&format!("{}: {e}", target.name));
81                errors.push(target.name);
82            }
83        }
84    }
85
86    let total_ok = newly_configured.len() + already_configured.len();
87    if total_ok == 0 && errors.is_empty() {
88        terminal_ui::print_status_warn(
89            "No AI tools detected. Install one and re-run: lean-ctx setup",
90        );
91    }
92
93    if !not_installed.is_empty() {
94        println!(
95            "  \x1b[2m○ {} not detected: {}\x1b[0m",
96            not_installed.len(),
97            not_installed.join(", ")
98        );
99    }
100
101    // Step 3: Agent rules injection
102    terminal_ui::print_step_header(3, 5, "Agent Rules");
103    let rules_result = crate::rules_inject::inject_all_rules(&home);
104    for name in &rules_result.injected {
105        terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules injected\x1b[0m"));
106    }
107    for name in &rules_result.updated {
108        terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules updated\x1b[0m"));
109    }
110    for name in &rules_result.already {
111        terminal_ui::print_status_ok(&format!("{name:<20} \x1b[2mrules up-to-date\x1b[0m"));
112    }
113    for err in &rules_result.errors {
114        terminal_ui::print_status_warn(err);
115    }
116    if rules_result.injected.is_empty()
117        && rules_result.updated.is_empty()
118        && rules_result.already.is_empty()
119        && rules_result.errors.is_empty()
120    {
121        terminal_ui::print_status_skip("No agent rules needed");
122    }
123
124    // Legacy agent hooks
125    for target in &targets {
126        if !target.detect_path.exists() || target.agent_key.is_empty() {
127            continue;
128        }
129        crate::hooks::install_agent_hook(target.agent_key, true);
130    }
131
132    // Step 4: Data directory + diagnostics
133    terminal_ui::print_step_header(4, 5, "Environment Check");
134    let lean_dir = home.join(".lean-ctx");
135    if !lean_dir.exists() {
136        let _ = std::fs::create_dir_all(&lean_dir);
137        terminal_ui::print_status_new("Created ~/.lean-ctx/");
138    } else {
139        terminal_ui::print_status_ok("~/.lean-ctx/ ready");
140    }
141    crate::doctor::run_compact();
142
143    // Step 5: Data sharing
144    terminal_ui::print_step_header(5, 5, "Help Improve lean-ctx");
145    println!("  Share anonymous compression stats to make lean-ctx better.");
146    println!("  \x1b[1mNo code, no file names, no personal data — ever.\x1b[0m");
147    println!();
148    print!("  Enable anonymous data sharing? \x1b[1m[Y/n]\x1b[0m ");
149    use std::io::Write;
150    std::io::stdout().flush().ok();
151
152    let mut input = String::new();
153    let contribute = if std::io::stdin().read_line(&mut input).is_ok() {
154        let answer = input.trim().to_lowercase();
155        answer.is_empty() || answer == "y" || answer == "yes"
156    } else {
157        false
158    };
159
160    if contribute {
161        let config_dir = home.join(".lean-ctx");
162        let _ = std::fs::create_dir_all(&config_dir);
163        let config_path = config_dir.join("config.toml");
164        let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
165        if !config_content.contains("[cloud]") {
166            if !config_content.is_empty() && !config_content.ends_with('\n') {
167                config_content.push('\n');
168            }
169            config_content.push_str("\n[cloud]\ncontribute_enabled = true\n");
170            let _ = std::fs::write(&config_path, config_content);
171        }
172        terminal_ui::print_status_ok("Enabled — thank you!");
173    } else {
174        terminal_ui::print_status_skip("Skipped — enable later with: lean-ctx config");
175    }
176
177    // Summary
178    println!();
179    println!(
180        "  \x1b[1;32m✓ Setup complete!\x1b[0m  \x1b[1m{}\x1b[0m configured, \x1b[2m{} already set, {} skipped\x1b[0m",
181        newly_configured.len(),
182        already_configured.len(),
183        not_installed.len()
184    );
185
186    if !errors.is_empty() {
187        println!(
188            "  \x1b[33m⚠ {} error{}: {}\x1b[0m",
189            errors.len(),
190            if errors.len() != 1 { "s" } else { "" },
191            errors.join(", ")
192        );
193    }
194
195    // Next steps
196    let shell = std::env::var("SHELL").unwrap_or_default();
197    let source_cmd = if shell.contains("zsh") {
198        "source ~/.zshrc"
199    } else if shell.contains("fish") {
200        "source ~/.config/fish/config.fish"
201    } else if shell.contains("bash") {
202        "source ~/.bashrc"
203    } else {
204        "Restart your shell"
205    };
206
207    let dim = "\x1b[2m";
208    let bold = "\x1b[1m";
209    let cyan = "\x1b[36m";
210    let yellow = "\x1b[33m";
211    let rst = "\x1b[0m";
212
213    println!();
214    println!("  {bold}Next steps:{rst}");
215    println!();
216    println!("  {cyan}1.{rst} Reload your shell:");
217    println!("     {bold}{source_cmd}{rst}");
218    println!();
219
220    let mut tools_to_restart: Vec<String> =
221        newly_configured.iter().map(|s| s.to_string()).collect();
222    for name in rules_result
223        .injected
224        .iter()
225        .chain(rules_result.updated.iter())
226    {
227        if !tools_to_restart.iter().any(|t| t == name) {
228            tools_to_restart.push(name.clone());
229        }
230    }
231
232    if !tools_to_restart.is_empty() {
233        println!("  {cyan}2.{rst} {yellow}{bold}Restart your IDE / AI tool:{rst}");
234        println!("     {bold}{}{rst}", tools_to_restart.join(", "));
235        println!(
236            "     {dim}The MCP connection must be re-established for changes to take effect.{rst}"
237        );
238        println!("     {dim}Close and re-open the application completely.{rst}");
239    } else if !already_configured.is_empty() {
240        println!(
241            "  {cyan}2.{rst} {dim}Your tools are already configured — no restart needed.{rst}"
242        );
243    }
244
245    println!();
246    println!(
247        "  {dim}After restart, lean-ctx will automatically optimize every AI interaction.{rst}"
248    );
249    println!("  {dim}Verify with:{rst} {bold}lean-ctx gain{rst}");
250
251    // Logo + commands
252    println!();
253    terminal_ui::print_logo_animated();
254    terminal_ui::print_command_box();
255}
256
257fn shorten_path(path: &str, home: &str) -> String {
258    if let Some(stripped) = path.strip_prefix(home) {
259        format!("~{stripped}")
260    } else {
261        path.to_string()
262    }
263}
264
265fn build_targets(home: &std::path::Path, _binary: &str) -> Vec<EditorTarget> {
266    vec![
267        EditorTarget {
268            name: "Cursor",
269            agent_key: "cursor",
270            config_path: home.join(".cursor/mcp.json"),
271            detect_path: home.join(".cursor"),
272            config_type: ConfigType::McpJson,
273        },
274        EditorTarget {
275            name: "Claude Code",
276            agent_key: "claude",
277            config_path: home.join(".claude.json"),
278            detect_path: detect_claude_path(),
279            config_type: ConfigType::McpJson,
280        },
281        EditorTarget {
282            name: "Windsurf",
283            agent_key: "windsurf",
284            config_path: home.join(".codeium/windsurf/mcp_config.json"),
285            detect_path: home.join(".codeium/windsurf"),
286            config_type: ConfigType::McpJson,
287        },
288        EditorTarget {
289            name: "Codex CLI",
290            agent_key: "codex",
291            config_path: home.join(".codex/config.toml"),
292            detect_path: detect_codex_path(home),
293            config_type: ConfigType::Codex,
294        },
295        EditorTarget {
296            name: "Gemini CLI",
297            agent_key: "gemini",
298            config_path: home.join(".gemini/settings/mcp.json"),
299            detect_path: home.join(".gemini"),
300            config_type: ConfigType::McpJson,
301        },
302        EditorTarget {
303            name: "Antigravity",
304            agent_key: "gemini",
305            config_path: home.join(".gemini/antigravity/mcp_config.json"),
306            detect_path: home.join(".gemini/antigravity"),
307            config_type: ConfigType::McpJson,
308        },
309        EditorTarget {
310            name: "Zed",
311            agent_key: "",
312            config_path: zed_settings_path(home),
313            detect_path: zed_config_dir(home),
314            config_type: ConfigType::Zed,
315        },
316        EditorTarget {
317            name: "VS Code / Copilot",
318            agent_key: "copilot",
319            config_path: vscode_mcp_path(),
320            detect_path: detect_vscode_path(),
321            config_type: ConfigType::VsCodeMcp,
322        },
323        EditorTarget {
324            name: "OpenCode",
325            agent_key: "",
326            config_path: home.join(".config/opencode/opencode.json"),
327            detect_path: home.join(".config/opencode"),
328            config_type: ConfigType::OpenCode,
329        },
330        EditorTarget {
331            name: "Qwen Code",
332            agent_key: "qwen",
333            config_path: home.join(".qwen/mcp.json"),
334            detect_path: home.join(".qwen"),
335            config_type: ConfigType::McpJson,
336        },
337        EditorTarget {
338            name: "Trae",
339            agent_key: "trae",
340            config_path: home.join(".trae/mcp.json"),
341            detect_path: home.join(".trae"),
342            config_type: ConfigType::McpJson,
343        },
344        EditorTarget {
345            name: "Amazon Q Developer",
346            agent_key: "amazonq",
347            config_path: home.join(".aws/amazonq/mcp.json"),
348            detect_path: home.join(".aws/amazonq"),
349            config_type: ConfigType::McpJson,
350        },
351        EditorTarget {
352            name: "JetBrains IDEs",
353            agent_key: "jetbrains",
354            config_path: home.join(".jb-mcp.json"),
355            detect_path: detect_jetbrains_path(home),
356            config_type: ConfigType::McpJson,
357        },
358        EditorTarget {
359            name: "Cline",
360            agent_key: "cline",
361            config_path: cline_mcp_path(),
362            detect_path: detect_cline_path(),
363            config_type: ConfigType::McpJson,
364        },
365        EditorTarget {
366            name: "Roo Code",
367            agent_key: "roo",
368            config_path: roo_mcp_path(),
369            detect_path: detect_roo_path(),
370            config_type: ConfigType::McpJson,
371        },
372        EditorTarget {
373            name: "AWS Kiro",
374            agent_key: "kiro",
375            config_path: home.join(".kiro/settings/mcp.json"),
376            detect_path: home.join(".kiro"),
377            config_type: ConfigType::McpJson,
378        },
379        EditorTarget {
380            name: "Verdent",
381            agent_key: "verdent",
382            config_path: home.join(".verdent/mcp.json"),
383            detect_path: home.join(".verdent"),
384            config_type: ConfigType::McpJson,
385        },
386    ]
387}
388
389fn detect_claude_path() -> PathBuf {
390    if let Ok(output) = std::process::Command::new("which").arg("claude").output() {
391        if output.status.success() {
392            return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
393        }
394    }
395    if let Some(home) = dirs::home_dir() {
396        let claude_json = home.join(".claude.json");
397        if claude_json.exists() {
398            return claude_json;
399        }
400    }
401    PathBuf::from("/nonexistent")
402}
403
404fn detect_codex_path(home: &std::path::Path) -> PathBuf {
405    let codex_dir = home.join(".codex");
406    if codex_dir.exists() {
407        return codex_dir;
408    }
409    if let Ok(output) = std::process::Command::new("which").arg("codex").output() {
410        if output.status.success() {
411            return codex_dir;
412        }
413    }
414    PathBuf::from("/nonexistent")
415}
416
417fn zed_settings_path(home: &std::path::Path) -> PathBuf {
418    if cfg!(target_os = "macos") {
419        home.join("Library/Application Support/Zed/settings.json")
420    } else {
421        home.join(".config/zed/settings.json")
422    }
423}
424
425fn zed_config_dir(home: &std::path::Path) -> PathBuf {
426    if cfg!(target_os = "macos") {
427        home.join("Library/Application Support/Zed")
428    } else {
429        home.join(".config/zed")
430    }
431}
432
433fn write_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
434    if let Some(parent) = target.config_path.parent() {
435        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
436    }
437
438    match target.config_type {
439        ConfigType::McpJson => write_mcp_json(target, binary),
440        ConfigType::Zed => write_zed_config(target, binary),
441        ConfigType::Codex => write_codex_config(target, binary),
442        ConfigType::VsCodeMcp => write_vscode_mcp(target, binary),
443        ConfigType::OpenCode => write_opencode_config(target, binary),
444    }
445}
446
447fn lean_ctx_server_entry(binary: &str) -> serde_json::Value {
448    serde_json::json!({
449        "command": binary,
450        "autoApprove": [
451            "ctx_read", "ctx_shell", "ctx_search", "ctx_tree",
452            "ctx_overview", "ctx_compress", "ctx_metrics", "ctx_session",
453            "ctx_knowledge", "ctx_agent", "ctx_analyze", "ctx_benchmark",
454            "ctx_cache", "ctx_discover", "ctx_smart_read", "ctx_delta",
455            "ctx_edit", "ctx_dedup", "ctx_fill", "ctx_intent", "ctx_response",
456            "ctx_context", "ctx_graph", "ctx_wrapped", "ctx_multi_read",
457            "ctx_semantic_search", "ctx"
458        ]
459    })
460}
461
462fn write_mcp_json(target: &EditorTarget, binary: &str) -> Result<(), String> {
463    if target.config_path.exists() {
464        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
465
466        if content.contains("lean-ctx") {
467            return Ok(());
468        }
469
470        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
471            if let Some(obj) = json.as_object_mut() {
472                let servers = obj
473                    .entry("mcpServers")
474                    .or_insert_with(|| serde_json::json!({}));
475                if let Some(servers_obj) = servers.as_object_mut() {
476                    servers_obj.insert("lean-ctx".to_string(), lean_ctx_server_entry(binary));
477                }
478                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
479                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
480                return Ok(());
481            }
482        }
483        return Err(format!(
484            "Could not parse existing config at {}. Please add lean-ctx manually:\n\
485             Add to \"mcpServers\": \"lean-ctx\": {{ \"command\": \"{}\" }}",
486            target.config_path.display(),
487            binary
488        ));
489    }
490
491    let content = serde_json::to_string_pretty(&serde_json::json!({
492        "mcpServers": {
493            "lean-ctx": lean_ctx_server_entry(binary)
494        }
495    }))
496    .map_err(|e| e.to_string())?;
497
498    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
499}
500
501fn write_zed_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
502    if target.config_path.exists() {
503        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
504
505        if content.contains("lean-ctx") {
506            return Ok(());
507        }
508
509        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
510            if let Some(obj) = json.as_object_mut() {
511                let servers = obj
512                    .entry("context_servers")
513                    .or_insert_with(|| serde_json::json!({}));
514                if let Some(servers_obj) = servers.as_object_mut() {
515                    servers_obj.insert(
516                        "lean-ctx".to_string(),
517                        serde_json::json!({
518                            "source": "custom",
519                            "command": binary,
520                            "args": [],
521                            "env": {}
522                        }),
523                    );
524                }
525                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
526                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
527                return Ok(());
528            }
529        }
530        return Err(format!(
531            "Could not parse existing config at {}. Please add lean-ctx manually to \"context_servers\".",
532            target.config_path.display()
533        ));
534    }
535
536    let content = serde_json::to_string_pretty(&serde_json::json!({
537        "context_servers": {
538            "lean-ctx": {
539                "source": "custom",
540                "command": binary,
541                "args": [],
542                "env": {}
543            }
544        }
545    }))
546    .map_err(|e| e.to_string())?;
547
548    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
549}
550
551fn write_codex_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
552    if target.config_path.exists() {
553        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
554
555        if content.contains("lean-ctx") {
556            return Ok(());
557        }
558
559        let mut new_content = content.clone();
560        if !new_content.ends_with('\n') {
561            new_content.push('\n');
562        }
563        new_content.push_str(&format!(
564            "\n[mcp_servers.lean-ctx]\ncommand = \"{}\"\nargs = []\n",
565            binary
566        ));
567        std::fs::write(&target.config_path, new_content).map_err(|e| e.to_string())?;
568        return Ok(());
569    }
570
571    let content = format!(
572        "[mcp_servers.lean-ctx]\ncommand = \"{}\"\nargs = []\n",
573        binary
574    );
575    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
576}
577
578fn write_vscode_mcp(target: &EditorTarget, binary: &str) -> Result<(), String> {
579    if target.config_path.exists() {
580        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
581        if content.contains("lean-ctx") {
582            return Ok(());
583        }
584        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
585            if let Some(obj) = json.as_object_mut() {
586                let servers = obj
587                    .entry("servers")
588                    .or_insert_with(|| serde_json::json!({}));
589                if let Some(servers_obj) = servers.as_object_mut() {
590                    servers_obj.insert(
591                        "lean-ctx".to_string(),
592                        serde_json::json!({ "command": binary, "args": [] }),
593                    );
594                }
595                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
596                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
597                return Ok(());
598            }
599        }
600        return Err(format!(
601            "Could not parse existing config at {}. Please add lean-ctx manually to \"servers\".",
602            target.config_path.display()
603        ));
604    }
605
606    if let Some(parent) = target.config_path.parent() {
607        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
608    }
609
610    let content = serde_json::to_string_pretty(&serde_json::json!({
611        "servers": {
612            "lean-ctx": {
613                "command": binary,
614                "args": []
615            }
616        }
617    }))
618    .map_err(|e| e.to_string())?;
619
620    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
621}
622
623fn write_opencode_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
624    if target.config_path.exists() {
625        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
626        if content.contains("lean-ctx") {
627            return Ok(());
628        }
629        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
630            if let Some(obj) = json.as_object_mut() {
631                let mcp = obj.entry("mcp").or_insert_with(|| serde_json::json!({}));
632                if let Some(mcp_obj) = mcp.as_object_mut() {
633                    mcp_obj.insert(
634                        "lean-ctx".to_string(),
635                        serde_json::json!({
636                            "type": "local",
637                            "command": [binary],
638                            "enabled": true
639                        }),
640                    );
641                }
642                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
643                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
644                return Ok(());
645            }
646        }
647        return Err(format!(
648            "Could not parse existing config at {}. Please add lean-ctx manually:\n\
649             Add to the \"mcp\" section: \"lean-ctx\": {{ \"type\": \"local\", \"command\": [\"{}\"], \"enabled\": true }}",
650            target.config_path.display(),
651            binary
652        ));
653    }
654
655    if let Some(parent) = target.config_path.parent() {
656        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
657    }
658
659    let content = serde_json::to_string_pretty(&serde_json::json!({
660        "$schema": "https://opencode.ai/config.json",
661        "mcp": {
662            "lean-ctx": {
663                "type": "local",
664                "command": [binary],
665                "enabled": true
666            }
667        }
668    }))
669    .map_err(|e| e.to_string())?;
670
671    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
672}
673
674fn detect_vscode_path() -> PathBuf {
675    #[cfg(target_os = "macos")]
676    {
677        if let Some(home) = dirs::home_dir() {
678            let vscode = home.join("Library/Application Support/Code/User/settings.json");
679            if vscode.exists() {
680                return vscode;
681            }
682        }
683    }
684    #[cfg(target_os = "linux")]
685    {
686        if let Some(home) = dirs::home_dir() {
687            let vscode = home.join(".config/Code/User/settings.json");
688            if vscode.exists() {
689                return vscode;
690            }
691        }
692    }
693    #[cfg(target_os = "windows")]
694    {
695        if let Ok(appdata) = std::env::var("APPDATA") {
696            let vscode = PathBuf::from(appdata).join("Code/User/settings.json");
697            if vscode.exists() {
698                return vscode;
699            }
700        }
701    }
702    if let Ok(output) = std::process::Command::new("which").arg("code").output() {
703        if output.status.success() {
704            return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
705        }
706    }
707    PathBuf::from("/nonexistent")
708}
709
710fn vscode_mcp_path() -> PathBuf {
711    if let Some(home) = dirs::home_dir() {
712        #[cfg(target_os = "macos")]
713        {
714            return home.join("Library/Application Support/Code/User/mcp.json");
715        }
716        #[cfg(target_os = "linux")]
717        {
718            return home.join(".config/Code/User/mcp.json");
719        }
720        #[cfg(target_os = "windows")]
721        {
722            if let Ok(appdata) = std::env::var("APPDATA") {
723                return PathBuf::from(appdata).join("Code/User/mcp.json");
724            }
725        }
726        #[allow(unreachable_code)]
727        home.join(".config/Code/User/mcp.json")
728    } else {
729        PathBuf::from("/nonexistent")
730    }
731}
732
733fn detect_jetbrains_path(home: &std::path::Path) -> PathBuf {
734    #[cfg(target_os = "macos")]
735    {
736        let lib = home.join("Library/Application Support/JetBrains");
737        if lib.exists() {
738            return lib;
739        }
740    }
741    #[cfg(target_os = "linux")]
742    {
743        let cfg = home.join(".config/JetBrains");
744        if cfg.exists() {
745            return cfg;
746        }
747    }
748    if home.join(".jb-mcp.json").exists() {
749        return home.join(".jb-mcp.json");
750    }
751    PathBuf::from("/nonexistent")
752}
753
754fn cline_mcp_path() -> PathBuf {
755    if let Some(home) = dirs::home_dir() {
756        #[cfg(target_os = "macos")]
757        {
758            return home.join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
759        }
760        #[cfg(target_os = "linux")]
761        {
762            return home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
763        }
764        #[cfg(target_os = "windows")]
765        {
766            if let Ok(appdata) = std::env::var("APPDATA") {
767                return PathBuf::from(appdata).join("Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
768            }
769        }
770    }
771    PathBuf::from("/nonexistent")
772}
773
774fn detect_cline_path() -> PathBuf {
775    if let Some(home) = dirs::home_dir() {
776        #[cfg(target_os = "macos")]
777        {
778            let p = home
779                .join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev");
780            if p.exists() {
781                return p;
782            }
783        }
784        #[cfg(target_os = "linux")]
785        {
786            let p = home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev");
787            if p.exists() {
788                return p;
789            }
790        }
791    }
792    PathBuf::from("/nonexistent")
793}
794
795fn roo_mcp_path() -> PathBuf {
796    if let Some(home) = dirs::home_dir() {
797        #[cfg(target_os = "macos")]
798        {
799            return home.join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
800        }
801        #[cfg(target_os = "linux")]
802        {
803            return home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
804        }
805        #[cfg(target_os = "windows")]
806        {
807            if let Ok(appdata) = std::env::var("APPDATA") {
808                return PathBuf::from(appdata).join("Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
809            }
810        }
811    }
812    PathBuf::from("/nonexistent")
813}
814
815fn detect_roo_path() -> PathBuf {
816    if let Some(home) = dirs::home_dir() {
817        #[cfg(target_os = "macos")]
818        {
819            let p = home.join(
820                "Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline",
821            );
822            if p.exists() {
823                return p;
824            }
825        }
826        #[cfg(target_os = "linux")]
827        {
828            let p = home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline");
829            if p.exists() {
830                return p;
831            }
832        }
833    }
834    PathBuf::from("/nonexistent")
835}
836
837fn resolve_portable_binary() -> String {
838    let which_cmd = if cfg!(windows) { "where" } else { "which" };
839    if let Ok(status) = std::process::Command::new(which_cmd)
840        .arg("lean-ctx")
841        .stdout(std::process::Stdio::null())
842        .stderr(std::process::Stdio::null())
843        .status()
844    {
845        if status.success() {
846            return "lean-ctx".to_string();
847        }
848    }
849    std::env::current_exe()
850        .map(|p| p.to_string_lossy().to_string())
851        .unwrap_or_else(|_| "lean-ctx".to_string())
852}