Skip to main content

lean_ctx/
uninstall.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4pub fn run() {
5    let home = match dirs::home_dir() {
6        Some(h) => h,
7        None => {
8            eprintln!("  ✗ Could not determine home directory");
9            return;
10        }
11    };
12
13    println!("\n  lean-ctx uninstall\n  ──────────────────────────────────\n");
14
15    let mut removed_any = false;
16
17    removed_any |= remove_shell_hook(&home);
18    crate::proxy_setup::uninstall_proxy_env(&home, false);
19    removed_any |= remove_mcp_configs(&home);
20    removed_any |= remove_rules_files(&home);
21    removed_any |= remove_hook_files(&home);
22    removed_any |= remove_project_agent_files();
23    removed_any |= remove_data_dir(&home);
24
25    println!();
26
27    if removed_any {
28        println!("  ──────────────────────────────────");
29        println!("  lean-ctx configuration removed.\n");
30    } else {
31        println!("  Nothing to remove — lean-ctx was not configured.\n");
32    }
33
34    print_binary_removal_instructions();
35}
36
37fn remove_project_agent_files() -> bool {
38    let cwd = std::env::current_dir().unwrap_or_default();
39    let agents = cwd.join("AGENTS.md");
40    let lean_ctx_md = cwd.join("LEAN-CTX.md");
41
42    const START: &str = "<!-- lean-ctx -->";
43    const END: &str = "<!-- /lean-ctx -->";
44    const OWNED: &str = "<!-- lean-ctx-owned: PROJECT-LEAN-CTX.md v1 -->";
45
46    let mut removed = false;
47
48    if agents.exists() {
49        if let Ok(content) = fs::read_to_string(&agents) {
50            if content.contains(START) {
51                let cleaned = remove_marked_block(&content, START, END);
52                if cleaned != content {
53                    if let Err(e) = fs::write(&agents, cleaned) {
54                        eprintln!("  ✗ Failed to update project AGENTS.md: {e}");
55                    } else {
56                        println!("  ✓ Project: removed lean-ctx block from AGENTS.md");
57                        removed = true;
58                    }
59                }
60            }
61        }
62    }
63
64    if lean_ctx_md.exists() {
65        if let Ok(content) = fs::read_to_string(&lean_ctx_md) {
66            if content.contains(OWNED) {
67                if let Err(e) = fs::remove_file(&lean_ctx_md) {
68                    eprintln!("  ✗ Failed to remove project LEAN-CTX.md: {e}");
69                } else {
70                    println!("  ✓ Project: removed LEAN-CTX.md");
71                    removed = true;
72                }
73            }
74        }
75    }
76
77    let project_files = [
78        ".windsurfrules",
79        ".clinerules",
80        ".cursorrules",
81        ".kiro/steering/lean-ctx.md",
82        ".cursor/rules/lean-ctx.mdc",
83    ];
84    for rel in &project_files {
85        let path = cwd.join(rel);
86        if path.exists() {
87            if let Ok(content) = fs::read_to_string(&path) {
88                if content.contains("lean-ctx") {
89                    let _ = fs::remove_file(&path);
90                    println!("  ✓ Project: removed {rel}");
91                    removed = true;
92                }
93            }
94        }
95    }
96
97    removed
98}
99
100fn remove_marked_block(content: &str, start: &str, end: &str) -> String {
101    let s = content.find(start);
102    let e = content.find(end);
103    match (s, e) {
104        (Some(si), Some(ei)) if ei >= si => {
105            let after_end = ei + end.len();
106            let before = &content[..si];
107            let after = &content[after_end..];
108            let mut out = String::new();
109            out.push_str(before.trim_end_matches('\n'));
110            out.push('\n');
111            if !after.trim().is_empty() {
112                out.push('\n');
113                out.push_str(after.trim_start_matches('\n'));
114            }
115            out
116        }
117        _ => content.to_string(),
118    }
119}
120
121fn remove_shell_hook(home: &Path) -> bool {
122    let shell = std::env::var("SHELL").unwrap_or_default();
123    let mut removed = false;
124
125    crate::shell_hook::uninstall_all(false);
126
127    let rc_files: Vec<PathBuf> = vec![
128        home.join(".zshrc"),
129        home.join(".bashrc"),
130        home.join(".config/fish/config.fish"),
131        #[cfg(windows)]
132        home.join("Documents/PowerShell/Microsoft.PowerShell_profile.ps1"),
133    ];
134
135    for rc in &rc_files {
136        if !rc.exists() {
137            continue;
138        }
139        let content = match fs::read_to_string(rc) {
140            Ok(c) => c,
141            Err(_) => continue,
142        };
143        if !content.contains("lean-ctx") {
144            continue;
145        }
146
147        let mut cleaned = remove_lean_ctx_block(&content);
148        cleaned = remove_source_lines(&cleaned);
149        if cleaned.trim() != content.trim() {
150            let bak = rc.with_extension("lean-ctx.bak");
151            let _ = fs::copy(rc, &bak);
152            if let Err(e) = fs::write(rc, &cleaned) {
153                eprintln!("  ✗ Failed to update {}: {}", rc.display(), e);
154            } else {
155                let short = shorten(rc, home);
156                println!("  ✓ Shell hook removed from {short}");
157                println!("    Backup: {}", shorten(&bak, home));
158                removed = true;
159            }
160        }
161    }
162
163    let hook_files = [
164        "shell-hook.zsh",
165        "shell-hook.bash",
166        "shell-hook.fish",
167        "shell-hook.ps1",
168    ];
169    let lc_dir = home.join(".lean-ctx");
170    for f in &hook_files {
171        let path = lc_dir.join(f);
172        if path.exists() {
173            let _ = fs::remove_file(&path);
174            println!("  ✓ Removed ~/.lean-ctx/{f}");
175            removed = true;
176        }
177    }
178
179    if !removed && !shell.is_empty() {
180        println!("  · No shell hook found");
181    }
182
183    removed
184}
185
186fn remove_source_lines(content: &str) -> String {
187    content
188        .lines()
189        .filter(|line| !line.contains(".lean-ctx/shell-hook."))
190        .collect::<Vec<_>>()
191        .join("\n")
192        + "\n"
193}
194
195fn remove_mcp_configs(home: &Path) -> bool {
196    let claude_cfg_dir_json = std::env::var("CLAUDE_CONFIG_DIR")
197        .ok()
198        .map(|d| PathBuf::from(d).join(".claude.json"))
199        .unwrap_or_else(|| PathBuf::from("/nonexistent"));
200    let configs: Vec<(&str, PathBuf)> = vec![
201        ("Cursor", home.join(".cursor/mcp.json")),
202        ("Claude Code (config dir)", claude_cfg_dir_json),
203        ("Claude Code (home)", home.join(".claude.json")),
204        ("Windsurf", home.join(".codeium/windsurf/mcp_config.json")),
205        ("Gemini CLI", home.join(".gemini/settings.json")),
206        (
207            "Gemini CLI (legacy)",
208            home.join(".gemini/settings/mcp.json"),
209        ),
210        (
211            "Antigravity",
212            home.join(".gemini/antigravity/mcp_config.json"),
213        ),
214        ("Codex CLI", home.join(".codex/config.toml")),
215        ("OpenCode", home.join(".config/opencode/opencode.json")),
216        ("Qwen Code", home.join(".qwen/mcp.json")),
217        ("Trae", home.join(".trae/mcp.json")),
218        ("Amazon Q Developer", home.join(".aws/amazonq/mcp.json")),
219        ("JetBrains IDEs", home.join(".jb-mcp.json")),
220        ("AWS Kiro", home.join(".kiro/settings/mcp.json")),
221        ("Verdent", home.join(".verdent/mcp.json")),
222        ("Aider", home.join(".aider/mcp.json")),
223        ("Amp", home.join(".config/amp/settings.json")),
224        ("Crush", home.join(".config/crush/crush.json")),
225        ("Pi Coding Agent", home.join(".pi/agent/mcp.json")),
226        ("Cline", crate::core::editor_registry::cline_mcp_path()),
227        ("Roo Code", crate::core::editor_registry::roo_mcp_path()),
228        ("Hermes Agent", home.join(".hermes/config.yaml")),
229    ];
230
231    let mut removed = false;
232
233    for (name, path) in &configs {
234        if !path.exists() {
235            continue;
236        }
237        let content = match fs::read_to_string(path) {
238            Ok(c) => c,
239            Err(_) => continue,
240        };
241        if !content.contains("lean-ctx") {
242            continue;
243        }
244
245        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
246        let is_yaml = ext == "yaml" || ext == "yml";
247        let is_toml = ext == "toml";
248
249        let cleaned = if is_yaml {
250            Some(remove_lean_ctx_from_yaml(&content))
251        } else if is_toml {
252            Some(remove_lean_ctx_from_toml(&content))
253        } else {
254            remove_lean_ctx_from_json(&content)
255        };
256
257        if let Some(cleaned) = cleaned {
258            if let Err(e) = fs::write(path, &cleaned) {
259                eprintln!("  ✗ Failed to update {} config: {}", name, e);
260            } else {
261                println!("  ✓ MCP config removed from {name}");
262                removed = true;
263            }
264        }
265    }
266
267    let zed_path = crate::core::editor_registry::zed_settings_path(home);
268    if zed_path.exists() {
269        if let Ok(content) = fs::read_to_string(&zed_path) {
270            if content.contains("lean-ctx") {
271                println!(
272                    "  ⚠ Zed: manually remove lean-ctx from {}",
273                    shorten(&zed_path, home)
274                );
275            }
276        }
277    }
278
279    let vscode_path = crate::core::editor_registry::vscode_mcp_path();
280    if vscode_path.exists() {
281        if let Ok(content) = fs::read_to_string(&vscode_path) {
282            if content.contains("lean-ctx") {
283                if let Some(cleaned) = remove_lean_ctx_from_json(&content) {
284                    if let Err(e) = fs::write(&vscode_path, &cleaned) {
285                        eprintln!("  ✗ Failed to update VS Code config: {e}");
286                    } else {
287                        println!("  ✓ MCP config removed from VS Code / Copilot");
288                        removed = true;
289                    }
290                }
291            }
292        }
293    }
294
295    removed
296}
297
298fn remove_rules_files(home: &Path) -> bool {
299    let rules_files: Vec<(&str, PathBuf)> = vec![
300        (
301            "Claude Code",
302            crate::core::editor_registry::claude_rules_dir(home).join("lean-ctx.md"),
303        ),
304        // Legacy: shared CLAUDE.md (older releases).
305        (
306            "Claude Code (legacy)",
307            crate::core::editor_registry::claude_state_dir(home).join("CLAUDE.md"),
308        ),
309        // Legacy: hardcoded home path (very old releases).
310        ("Claude Code (legacy home)", home.join(".claude/CLAUDE.md")),
311        ("Cursor", home.join(".cursor/rules/lean-ctx.mdc")),
312        ("Gemini CLI", home.join(".gemini/GEMINI.md")),
313        (
314            "Gemini CLI (legacy)",
315            home.join(".gemini/rules/lean-ctx.md"),
316        ),
317        ("Codex CLI", home.join(".codex/LEAN-CTX.md")),
318        ("Codex CLI", home.join(".codex/instructions.md")),
319        ("Windsurf", home.join(".codeium/windsurf/rules/lean-ctx.md")),
320        ("Zed", home.join(".config/zed/rules/lean-ctx.md")),
321        ("Cline", home.join(".cline/rules/lean-ctx.md")),
322        ("Roo Code", home.join(".roo/rules/lean-ctx.md")),
323        ("OpenCode", home.join(".config/opencode/rules/lean-ctx.md")),
324        ("Continue", home.join(".continue/rules/lean-ctx.md")),
325        ("Aider", home.join(".aider/rules/lean-ctx.md")),
326        ("Amp", home.join(".ampcoder/rules/lean-ctx.md")),
327        ("Qwen Code", home.join(".qwen/rules/lean-ctx.md")),
328        ("Trae", home.join(".trae/rules/lean-ctx.md")),
329        (
330            "Amazon Q Developer",
331            home.join(".aws/amazonq/rules/lean-ctx.md"),
332        ),
333        ("JetBrains IDEs", home.join(".jb-rules/lean-ctx.md")),
334        (
335            "Antigravity",
336            home.join(".gemini/antigravity/rules/lean-ctx.md"),
337        ),
338        ("Pi Coding Agent", home.join(".pi/rules/lean-ctx.md")),
339        ("AWS Kiro", home.join(".kiro/steering/lean-ctx.md")),
340        ("Verdent", home.join(".verdent/rules/lean-ctx.md")),
341        ("Crush", home.join(".config/crush/rules/lean-ctx.md")),
342    ];
343
344    let mut removed = false;
345    for (name, path) in &rules_files {
346        if !path.exists() {
347            continue;
348        }
349        if let Ok(content) = fs::read_to_string(path) {
350            if content.contains("lean-ctx") {
351                if let Err(e) = fs::remove_file(path) {
352                    eprintln!("  ✗ Failed to remove {name} rules: {e}");
353                } else {
354                    println!("  ✓ Rules removed from {name}");
355                    removed = true;
356                }
357            }
358        }
359    }
360
361    let hermes_md = home.join(".hermes/HERMES.md");
362    if hermes_md.exists() {
363        if let Ok(content) = fs::read_to_string(&hermes_md) {
364            if content.contains("lean-ctx") {
365                let cleaned = remove_lean_ctx_block_from_md(&content);
366                if cleaned.trim().is_empty() {
367                    let _ = fs::remove_file(&hermes_md);
368                } else {
369                    let _ = fs::write(&hermes_md, &cleaned);
370                }
371                println!("  ✓ Rules removed from Hermes Agent");
372                removed = true;
373            }
374        }
375    }
376
377    if let Ok(cwd) = std::env::current_dir() {
378        let project_hermes = cwd.join(".hermes.md");
379        if project_hermes.exists() {
380            if let Ok(content) = fs::read_to_string(&project_hermes) {
381                if content.contains("lean-ctx") {
382                    let cleaned = remove_lean_ctx_block_from_md(&content);
383                    if cleaned.trim().is_empty() {
384                        let _ = fs::remove_file(&project_hermes);
385                    } else {
386                        let _ = fs::write(&project_hermes, &cleaned);
387                    }
388                    println!("  ✓ Rules removed from .hermes.md");
389                    removed = true;
390                }
391            }
392        }
393    }
394
395    if !removed {
396        println!("  · No rules files found");
397    }
398    removed
399}
400
401fn remove_lean_ctx_block_from_md(content: &str) -> String {
402    let mut out = String::with_capacity(content.len());
403    let mut in_block = false;
404
405    for line in content.lines() {
406        if !in_block && line.contains("lean-ctx") && line.starts_with('#') {
407            in_block = true;
408            continue;
409        }
410        if in_block {
411            if line.starts_with('#') && !line.contains("lean-ctx") {
412                in_block = false;
413                out.push_str(line);
414                out.push('\n');
415            }
416            continue;
417        }
418        out.push_str(line);
419        out.push('\n');
420    }
421
422    while out.starts_with('\n') {
423        out.remove(0);
424    }
425    while out.ends_with("\n\n") {
426        out.pop();
427    }
428    out
429}
430
431fn remove_hook_files(home: &Path) -> bool {
432    let claude_hooks_dir = crate::core::editor_registry::claude_state_dir(home).join("hooks");
433    let hook_files: Vec<PathBuf> = vec![
434        claude_hooks_dir.join("lean-ctx-rewrite.sh"),
435        claude_hooks_dir.join("lean-ctx-redirect.sh"),
436        claude_hooks_dir.join("lean-ctx-rewrite-native"),
437        claude_hooks_dir.join("lean-ctx-redirect-native"),
438        home.join(".cursor/hooks/lean-ctx-rewrite.sh"),
439        home.join(".cursor/hooks/lean-ctx-redirect.sh"),
440        home.join(".cursor/hooks/lean-ctx-rewrite-native"),
441        home.join(".cursor/hooks/lean-ctx-redirect-native"),
442        home.join(".gemini/hooks/lean-ctx-rewrite-gemini.sh"),
443        home.join(".gemini/hooks/lean-ctx-redirect-gemini.sh"),
444        home.join(".gemini/hooks/lean-ctx-hook-gemini.sh"),
445        home.join(".codex/hooks/lean-ctx-rewrite-codex.sh"),
446    ];
447
448    let mut removed = false;
449    for path in &hook_files {
450        if path.exists() {
451            if let Err(e) = fs::remove_file(path) {
452                eprintln!("  ✗ Failed to remove hook {}: {e}", path.display());
453            } else {
454                removed = true;
455            }
456        }
457    }
458
459    if removed {
460        println!("  ✓ Hook scripts removed");
461    }
462
463    for (label, hj_path) in [
464        ("Cursor", home.join(".cursor/hooks.json")),
465        ("Codex", home.join(".codex/hooks.json")),
466    ] {
467        if hj_path.exists() {
468            if let Ok(content) = fs::read_to_string(&hj_path) {
469                if content.contains("lean-ctx") {
470                    if let Err(e) = fs::remove_file(&hj_path) {
471                        eprintln!("  ✗ Failed to remove {label} hooks.json: {e}");
472                    } else {
473                        println!("  ✓ {label} hooks.json removed");
474                        removed = true;
475                    }
476                }
477            }
478        }
479    }
480
481    removed
482}
483
484fn remove_data_dir(home: &Path) -> bool {
485    let data_dir = home.join(".lean-ctx");
486    if !data_dir.exists() {
487        println!("  · No data directory found");
488        return false;
489    }
490
491    match fs::remove_dir_all(&data_dir) {
492        Ok(_) => {
493            println!("  ✓ Data directory removed (~/.lean-ctx/)");
494            true
495        }
496        Err(e) => {
497            eprintln!("  ✗ Failed to remove ~/.lean-ctx/: {e}");
498            false
499        }
500    }
501}
502
503fn print_binary_removal_instructions() {
504    let binary_path = std::env::current_exe()
505        .map(|p| p.display().to_string())
506        .unwrap_or_else(|_| "lean-ctx".to_string());
507
508    println!("  To complete uninstallation, remove the binary:\n");
509
510    if binary_path.contains(".cargo") {
511        println!("    cargo uninstall lean-ctx\n");
512    } else if binary_path.contains("homebrew") || binary_path.contains("Cellar") {
513        println!("    brew uninstall lean-ctx\n");
514    } else {
515        println!("    rm {binary_path}\n");
516    }
517
518    println!("  Then restart your shell.\n");
519}
520
521fn remove_lean_ctx_block(content: &str) -> String {
522    if content.contains("# lean-ctx shell hook — end") {
523        return remove_lean_ctx_block_by_marker(content);
524    }
525    remove_lean_ctx_block_legacy(content)
526}
527
528fn remove_lean_ctx_block_by_marker(content: &str) -> String {
529    let mut result = String::new();
530    let mut in_block = false;
531
532    for line in content.lines() {
533        if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
534            in_block = true;
535            continue;
536        }
537        if in_block {
538            if line.trim() == "# lean-ctx shell hook — end" {
539                in_block = false;
540            }
541            continue;
542        }
543        result.push_str(line);
544        result.push('\n');
545    }
546    result
547}
548
549fn remove_lean_ctx_block_legacy(content: &str) -> String {
550    let mut result = String::new();
551    let mut in_block = false;
552
553    for line in content.lines() {
554        if line.contains("lean-ctx shell hook") {
555            in_block = true;
556            continue;
557        }
558        if in_block {
559            if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
560                if line.trim() == "fi" || line.trim() == "end" {
561                    in_block = false;
562                }
563                continue;
564            }
565            if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
566                in_block = false;
567                result.push_str(line);
568                result.push('\n');
569            }
570            continue;
571        }
572        result.push_str(line);
573        result.push('\n');
574    }
575    result
576}
577
578fn remove_lean_ctx_from_json(content: &str) -> Option<String> {
579    let mut parsed: serde_json::Value = serde_json::from_str(content).ok()?;
580    let mut modified = false;
581
582    if let Some(servers) = parsed.get_mut("mcpServers").and_then(|s| s.as_object_mut()) {
583        modified |= servers.remove("lean-ctx").is_some();
584    }
585
586    if let Some(servers) = parsed.get_mut("servers").and_then(|s| s.as_object_mut()) {
587        modified |= servers.remove("lean-ctx").is_some();
588    }
589
590    if let Some(servers) = parsed.get_mut("servers").and_then(|s| s.as_array_mut()) {
591        let before = servers.len();
592        servers.retain(|entry| entry.get("name").and_then(|n| n.as_str()) != Some("lean-ctx"));
593        modified |= servers.len() < before;
594    }
595
596    if let Some(mcp) = parsed.get_mut("mcp").and_then(|s| s.as_object_mut()) {
597        modified |= mcp.remove("lean-ctx").is_some();
598    }
599
600    if let Some(amp) = parsed
601        .get_mut("amp.mcpServers")
602        .and_then(|s| s.as_object_mut())
603    {
604        modified |= amp.remove("lean-ctx").is_some();
605    }
606
607    if modified {
608        Some(serde_json::to_string_pretty(&parsed).ok()? + "\n")
609    } else {
610        None
611    }
612}
613
614fn remove_lean_ctx_from_yaml(content: &str) -> String {
615    let mut out = String::with_capacity(content.len());
616    let mut skip_depth: Option<usize> = None;
617
618    for line in content.lines() {
619        if let Some(depth) = skip_depth {
620            let indent = line.len() - line.trim_start().len();
621            if indent > depth || line.trim().is_empty() {
622                continue;
623            }
624            skip_depth = None;
625        }
626
627        let trimmed = line.trim();
628        if trimmed == "lean-ctx:" || trimmed.starts_with("lean-ctx:") {
629            let indent = line.len() - line.trim_start().len();
630            skip_depth = Some(indent);
631            continue;
632        }
633
634        out.push_str(line);
635        out.push('\n');
636    }
637
638    out
639}
640
641fn remove_lean_ctx_from_toml(content: &str) -> String {
642    let mut out = String::with_capacity(content.len());
643    let mut skip = false;
644
645    for line in content.lines() {
646        let trimmed = line.trim();
647
648        if trimmed.starts_with('[') && trimmed.ends_with(']') {
649            let section = trimmed.trim_start_matches('[').trim_end_matches(']').trim();
650            if section == "mcp_servers.lean-ctx"
651                || section == "mcp_servers.\"lean-ctx\""
652                || section.starts_with("mcp_servers.lean-ctx.")
653                || section.starts_with("mcp_servers.\"lean-ctx\".")
654            {
655                skip = true;
656                continue;
657            }
658            skip = false;
659        }
660
661        if skip {
662            continue;
663        }
664
665        if trimmed.contains("codex_hooks") && trimmed.contains("true") {
666            out.push_str(&line.replace("true", "false"));
667            out.push('\n');
668            continue;
669        }
670
671        out.push_str(line);
672        out.push('\n');
673    }
674
675    let cleaned: String = out
676        .lines()
677        .filter(|l| l.trim() != "[]")
678        .collect::<Vec<_>>()
679        .join("\n");
680    if cleaned.is_empty() {
681        cleaned
682    } else {
683        cleaned + "\n"
684    }
685}
686
687fn shorten(path: &Path, home: &Path) -> String {
688    match path.strip_prefix(home) {
689        Ok(rel) => format!("~/{}", rel.display()),
690        Err(_) => path.display().to_string(),
691    }
692}
693
694// moved to core/editor_registry/paths.rs
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699
700    #[test]
701    fn remove_toml_mcp_server_section() {
702        let input = "\
703[features]
704codex_hooks = true
705
706[mcp_servers.lean-ctx]
707command = \"/usr/local/bin/lean-ctx\"
708args = []
709
710[mcp_servers.other-tool]
711command = \"/usr/bin/other\"
712";
713        let result = remove_lean_ctx_from_toml(input);
714        assert!(
715            !result.contains("lean-ctx"),
716            "lean-ctx section should be removed"
717        );
718        assert!(
719            result.contains("[mcp_servers.other-tool]"),
720            "other sections should be preserved"
721        );
722        assert!(
723            result.contains("codex_hooks = false"),
724            "codex_hooks should be set to false"
725        );
726    }
727
728    #[test]
729    fn remove_toml_only_lean_ctx() {
730        let input = "\
731[mcp_servers.lean-ctx]
732command = \"lean-ctx\"
733";
734        let result = remove_lean_ctx_from_toml(input);
735        assert!(
736            result.trim().is_empty(),
737            "should produce empty output: {result}"
738        );
739    }
740
741    #[test]
742    fn remove_toml_no_lean_ctx() {
743        let input = "\
744[mcp_servers.other]
745command = \"other\"
746";
747        let result = remove_lean_ctx_from_toml(input);
748        assert!(
749            result.contains("[mcp_servers.other]"),
750            "other content should be preserved"
751        );
752    }
753}