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