Skip to main content

st/
claude_init.rs

1//! Claude integration initializer for Smart Tree
2//! Automatically sets up optimal .claude directory configuration for any project
3//! Also handles MCP server auto-installation for Claude Desktop! šŸš€
4
5use anyhow::{Context, Result};
6use chrono::Local;
7use serde_json::{json, Value};
8use std::fs;
9use std::io::{self, Write as IoWrite};
10use std::path::{Path, PathBuf};
11
12use crate::scanner::{Scanner, ScannerConfig};
13use crate::TreeStats;
14
15/// Valid Claude Code hook event types
16const VALID_HOOK_KEYS: &[&str] = &[
17    "SessionStart",
18    "UserPromptSubmit",
19    "PreToolUse",
20    "PermissionRequest",
21    "PostToolUse",
22    "PostToolUseFailure",
23    "SubagentStart",
24    "SubagentStop",
25    "Stop",
26    "PreCompact",
27    "SessionEnd",
28    "Notification",
29    "Setup",
30];
31
32/// Ask user for confirmation before overwriting a file
33fn confirm_overwrite(path: &Path) -> bool {
34    print!("   āš ļø  {} exists. Overwrite? [y/N]: ", path.display());
35    io::stdout().flush().unwrap();
36
37    let mut input = String::new();
38    if io::stdin().read_line(&mut input).is_ok() {
39        let response = input.trim().to_lowercase();
40        return response == "y" || response == "yes";
41    }
42    false
43}
44
45/// Validate that settings.json has correct hook format
46/// Returns error description if invalid, None if valid
47pub fn validate_settings(path: &Path) -> Result<Option<String>> {
48    if !path.exists() {
49        return Ok(None);
50    }
51
52    let content = fs::read_to_string(path)?;
53    let parsed: Result<Value, _> = serde_json::from_str(&content);
54
55    match parsed {
56        Err(e) => Ok(Some(format!("Invalid JSON: {}", e))),
57        Ok(json) => {
58            if let Some(hooks) = json.get("hooks") {
59                if let Some(obj) = hooks.as_object() {
60                    for key in obj.keys() {
61                        if !VALID_HOOK_KEYS.contains(&key.as_str()) {
62                            return Ok(Some(format!(
63                                "Invalid hook key '{}'. Valid: {}",
64                                key,
65                                VALID_HOOK_KEYS.join(", ")
66                            )));
67                        }
68                    }
69                }
70            }
71            Ok(None)
72        }
73    }
74}
75
76/// Project type detection for optimal hook configuration
77#[derive(Debug, Clone)]
78pub enum ProjectType {
79    Rust,
80    Python,
81    JavaScript,
82    TypeScript,
83    Mixed,
84    Unknown,
85}
86
87/// Claude integration initializer
88pub struct ClaudeInit {
89    project_path: PathBuf,
90    project_type: ProjectType,
91    stats: TreeStats,
92}
93
94impl ClaudeInit {
95    /// Create new initializer for a project
96    pub fn new(project_path: PathBuf) -> Result<Self> {
97        // Scan project to understand structure
98        let config = ScannerConfig {
99            max_depth: 3,
100            show_hidden: false,
101            follow_symlinks: false,
102            ..Default::default()
103        };
104
105        let scanner = Scanner::new(&project_path, config)?;
106        let (nodes, stats) = scanner.scan()?;
107
108        // Detect project type based on files
109        let project_type = Self::detect_project_type(&nodes, &stats);
110
111        Ok(Self {
112            project_path,
113            project_type,
114            stats,
115        })
116    }
117
118    /// Detect project type from file extensions
119    fn detect_project_type(nodes: &[crate::FileNode], _stats: &TreeStats) -> ProjectType {
120        let mut rust_score = 0;
121        let mut python_score = 0;
122        let mut js_score = 0;
123        let mut ts_score = 0;
124
125        // Check for key files
126        for node in nodes {
127            let path_str = node.path.to_string_lossy();
128
129            // Project markers
130            if path_str.contains("Cargo.toml") {
131                rust_score += 100;
132            }
133            if path_str.contains("package.json") {
134                js_score += 50;
135                ts_score += 30;
136            }
137            if path_str.contains("pyproject.toml") || path_str.contains("requirements.txt") {
138                python_score += 100;
139            }
140            if path_str.contains("tsconfig.json") {
141                ts_score += 100;
142            }
143
144            // File extensions
145            if path_str.ends_with(".rs") {
146                rust_score += 1;
147            }
148            if path_str.ends_with(".py") {
149                python_score += 1;
150            }
151            if path_str.ends_with(".js") || path_str.ends_with(".jsx") {
152                js_score += 1;
153            }
154            if path_str.ends_with(".ts") || path_str.ends_with(".tsx") {
155                ts_score += 1;
156            }
157        }
158
159        // Determine primary type
160        let max_score = rust_score.max(python_score).max(js_score).max(ts_score);
161
162        if max_score == 0 {
163            ProjectType::Unknown
164        } else if rust_score == max_score {
165            ProjectType::Rust
166        } else if python_score == max_score {
167            ProjectType::Python
168        } else if ts_score == max_score {
169            ProjectType::TypeScript
170        } else if js_score == max_score {
171            ProjectType::JavaScript
172        } else {
173            ProjectType::Mixed
174        }
175    }
176
177    /// Smart setup - initializes if new, updates if exists
178    pub fn setup(&self) -> Result<()> {
179        let claude_dir = self.project_path.join(".claude");
180
181        if claude_dir.exists() {
182            // Update existing configuration
183            self.update_existing(&claude_dir)
184        } else {
185            // Initialize new configuration
186            self.init_new(&claude_dir)
187        }
188    }
189
190    /// Initialize new Claude configuration
191    fn init_new(&self, claude_dir: &Path) -> Result<()> {
192        // Create .claude directory
193        fs::create_dir_all(claude_dir).context("Failed to create .claude directory")?;
194
195        // Generate settings.json (force=true for new projects)
196        self.create_settings_json(claude_dir, true)?;
197
198        // Generate CLAUDE.md (force=true for new projects)
199        self.create_claude_md(claude_dir, true)?;
200
201        println!(
202            "✨ Claude integration initialized for {:?} project!",
203            self.project_type
204        );
205        println!("šŸ“ Created .claude/ directory with:");
206        println!("   • settings.json - Smart hooks configured");
207        println!("   • CLAUDE.md - Project-specific AI guidance");
208        println!("\nšŸ’” Tip: Run 'st --setup-claude' anytime to update");
209
210        Ok(())
211    }
212
213    /// Update existing Claude configuration
214    fn update_existing(&self, claude_dir: &Path) -> Result<()> {
215        println!("šŸ”„ Checking existing Claude integration...");
216
217        let settings_path = claude_dir.join("settings.json");
218        let claude_md_path = claude_dir.join("CLAUDE.md");
219
220        let mut updated = false;
221
222        // Validate existing settings if present
223        if settings_path.exists() {
224            if let Some(error) = validate_settings(&settings_path)? {
225                println!("   āš ļø  settings.json has issues: {}", error);
226                println!("   šŸ’” Suggested fix:");
227                self.show_suggested()?;
228                return Ok(());
229            }
230
231            // Check if auto-configured (safe to update silently)
232            let existing: Value = serde_json::from_str(&fs::read_to_string(&settings_path)?)?;
233            let is_auto = existing
234                .get("smart_tree")
235                .and_then(|st| st.get("auto_configured"))
236                .and_then(|v| v.as_bool())
237                .unwrap_or(false);
238
239            if is_auto {
240                // Auto-configured: safe to update
241                if self.create_settings_json(claude_dir, true)? {
242                    println!("   āœ… Updated settings.json");
243                    updated = true;
244                }
245            } else {
246                // Manual config: ask first (force=false)
247                println!("   ā„¹ļø  settings.json has manual configuration");
248                if self.create_settings_json(claude_dir, false)? {
249                    println!("   āœ… Updated settings.json");
250                    updated = true;
251                } else {
252                    println!("   ā­ļø  Skipped settings.json");
253                }
254            }
255        } else if self.create_settings_json(claude_dir, true)? {
256            println!("   āœ… Created settings.json");
257            updated = true;
258        }
259
260        // CLAUDE.md - ask before overwriting (force=false)
261        if claude_md_path.exists() {
262            if self.create_claude_md(claude_dir, false)? {
263                println!("   āœ… Updated CLAUDE.md");
264                updated = true;
265            } else {
266                println!("   ā­ļø  Skipped CLAUDE.md");
267            }
268        } else if self.create_claude_md(claude_dir, true)? {
269            println!("   āœ… Created CLAUDE.md");
270            updated = true;
271        }
272
273        if updated {
274            println!(
275                "\nšŸŽ‰ Claude integration updated for {:?} project!",
276                self.project_type
277            );
278        } else {
279            println!("\n✨ No changes made. Use --force to overwrite.");
280        }
281
282        Ok(())
283    }
284
285    /// Create settings.json with smart hook configuration
286    /// If file exists, asks for confirmation unless force=true
287    fn create_settings_json(&self, claude_dir: &Path, force: bool) -> Result<bool> {
288        let settings_path = claude_dir.join("settings.json");
289
290        // Check if file exists and ask for confirmation
291        if settings_path.exists() && !force {
292            if !confirm_overwrite(&settings_path) {
293                return Ok(false);
294            }
295            // Backup existing file
296            let backup = settings_path.with_extension("json.bak");
297            fs::copy(&settings_path, &backup)?;
298        }
299
300        // Build hook configuration - NO automatic context dump on every prompt!
301        // AI should request context via MCP tools when needed, not get flooded every message.
302        // Only SessionStart/End for consciousness persistence, and targeted PreToolUse hooks.
303        let hooks = match self.project_type {
304            ProjectType::Rust => {
305                json!({
306                    "SessionStart": [{
307                        "matcher": "",
308                        "hooks": [{
309                            "type": "command",
310                            "command": "st --claude-restore"
311                        }]
312                    }],
313                    "SessionEnd": [{
314                        "matcher": "",
315                        "hooks": [{
316                            "type": "command",
317                            "command": "st --claude-save"
318                        }]
319                    }],
320                    "PreToolUse": [{
321                        "matcher": "cargo (build|test|run)",
322                        "hooks": [{
323                            "type": "command",
324                            "command": "st -m summary --depth 3 ."
325                        }]
326                    }]
327                })
328            }
329            ProjectType::Python => {
330                json!({
331                    "SessionStart": [{
332                        "matcher": "",
333                        "hooks": [{
334                            "type": "command",
335                            "command": "st --claude-restore"
336                        }]
337                    }],
338                    "SessionEnd": [{
339                        "matcher": "",
340                        "hooks": [{
341                            "type": "command",
342                            "command": "st --claude-save"
343                        }]
344                    }],
345                    "PreToolUse": [{
346                        "matcher": "pytest|python.*test",
347                        "hooks": [{
348                            "type": "command",
349                            "command": "st -m summary --depth 3 ."
350                        }]
351                    }]
352                })
353            }
354            ProjectType::JavaScript | ProjectType::TypeScript => {
355                json!({
356                    "SessionStart": [{
357                        "matcher": "",
358                        "hooks": [{
359                            "type": "command",
360                            "command": "st --claude-restore"
361                        }]
362                    }],
363                    "SessionEnd": [{
364                        "matcher": "",
365                        "hooks": [{
366                            "type": "command",
367                            "command": "st --claude-save"
368                        }]
369                    }],
370                    "PreToolUse": [{
371                        "matcher": "npm (test|build|run)",
372                        "hooks": [{
373                            "type": "command",
374                            "command": "st -m summary --depth 3 ."
375                        }]
376                    }]
377                })
378            }
379            _ => {
380                // Generic configuration - just consciousness persistence
381                json!({
382                    "SessionStart": [{
383                        "matcher": "",
384                        "hooks": [{
385                            "type": "command",
386                            "command": "st --claude-restore"
387                        }]
388                    }],
389                    "SessionEnd": [{
390                        "matcher": "",
391                        "hooks": [{
392                            "type": "command",
393                            "command": "st --claude-save"
394                        }]
395                    }]
396                })
397            }
398        };
399
400        let settings = json!({
401            "hooks": hooks,
402            "smart_tree": {
403                "version": env!("CARGO_PKG_VERSION"),
404                "project_type": format!("{:?}", self.project_type),
405                "auto_configured": true,
406                "stats": {
407                    "files": self.stats.total_files,
408                    "directories": self.stats.total_dirs,
409                    "size": self.stats.total_size
410                }
411            }
412        });
413
414        let content = serde_json::to_string_pretty(&settings)?;
415        fs::write(&settings_path, &content)?;
416
417        // Validate what we wrote
418        if let Some(error) = validate_settings(&settings_path)? {
419            // Revert from backup
420            let backup = settings_path.with_extension("json.bak");
421            if backup.exists() {
422                fs::copy(&backup, &settings_path)?;
423                fs::remove_file(&backup)?;
424            }
425            anyhow::bail!("Validation failed, reverted: {}", error);
426        }
427
428        Ok(true)
429    }
430
431    /// Create CLAUDE.md with project-specific guidance
432    /// If file exists, asks for confirmation unless force=true
433    fn create_claude_md(&self, claude_dir: &Path, force: bool) -> Result<bool> {
434        let claude_md_path = claude_dir.join("CLAUDE.md");
435
436        // Check if file exists and ask for confirmation
437        if claude_md_path.exists() && !force && !confirm_overwrite(&claude_md_path) {
438            return Ok(false);
439        }
440
441        let content = match self.project_type {
442            ProjectType::Rust => {
443                format!(
444                    r#"# CLAUDE.md
445
446This Rust project uses Smart Tree for optimal AI context management.
447
448## Project Stats
449- Files: {}
450- Directories: {}
451- Total size: {} bytes
452
453## Essential Commands
454
455```bash
456# Build & Test
457cargo build --release
458cargo test -- --nocapture
459cargo clippy -- -D warnings
460
461# Smart Tree context
462st -m context .          # Full context with git info
463st -m quantum .           # Compressed for large contexts
464st -m relations --focus main.rs  # Code relationships
465```
466
467## Key Patterns
468- Always use `Result<T>` for error handling
469- Prefer `&str` over `String` for function parameters
470- Use `anyhow` for error context
471- Run clippy before commits
472
473## Smart Tree Integration
474This project has hooks configured to automatically provide context.
475The quantum-semantic mode is used for optimal token efficiency.
476"#,
477                    self.stats.total_files, self.stats.total_dirs, self.stats.total_size
478                )
479            }
480            ProjectType::Python => {
481                format!(
482                    r#"# CLAUDE.md
483
484This Python project uses Smart Tree for optimal AI context management.
485
486## Project Stats
487- Files: {}
488- Directories: {}
489- Total size: {} bytes
490
491## Essential Commands
492
493```bash
494# Environment & Testing
495uv sync                   # Install dependencies with uv
496pytest -v                 # Run tests
497ruff check .             # Lint code
498mypy .                   # Type checking
499
500# Smart Tree context
501st -m context .          # Full context with git info
502st -m quantum .          # Compressed for large contexts
503```
504
505## Key Patterns
506- Use type hints for all functions
507- Prefer uv over pip for package management
508- Follow PEP 8 style guide
509- Write docstrings for all public functions
510
511## Smart Tree Integration
512Hooks provide automatic context on prompt submission.
513Test runs trigger summary of test directories.
514"#,
515                    self.stats.total_files, self.stats.total_dirs, self.stats.total_size
516                )
517            }
518            ProjectType::TypeScript | ProjectType::JavaScript => {
519                format!(
520                    r#"# CLAUDE.md
521
522This {0} project uses Smart Tree for optimal AI context management.
523
524## Project Stats
525- Files: {1}
526- Directories: {2}
527- Total size: {3} bytes
528
529## Essential Commands
530
531```bash
532# Development
533pnpm install             # Install dependencies
534pnpm run dev            # Start dev server
535pnpm test               # Run tests
536pnpm build              # Production build
537
538# Smart Tree context
539st -m context .          # Full context with git info
540st -m quantum .          # Compressed for large contexts
541```
542
543## Key Patterns
544- Use pnpm for package management
545- Implement proper TypeScript types
546- Follow ESLint rules
547- Component-based architecture
548
549## Smart Tree Integration
550Automatic context provision via hooks.
551Node_modules excluded from summaries.
552"#,
553                    if matches!(self.project_type, ProjectType::TypeScript) {
554                        "TypeScript"
555                    } else {
556                        "JavaScript"
557                    },
558                    self.stats.total_files,
559                    self.stats.total_dirs,
560                    self.stats.total_size
561                )
562            }
563            _ => {
564                format!(
565                    r#"# CLAUDE.md
566
567This project uses Smart Tree for optimal AI context management.
568
569## Project Stats
570- Files: {}
571- Directories: {}
572- Total size: {} bytes
573- Type: {:?}
574
575## Smart Tree Commands
576
577```bash
578st -m context .          # Full context with git info
579st -m quantum .          # Compressed for large contexts
580st -m summary .          # Human-readable summary
581st -m quantum-semantic . # Maximum compression
582```
583
584## Smart Tree Integration
585This project has been configured with automatic hooks that provide
586context to Claude on every prompt. The hook mode is optimized based
587on your project size.
588
589Use `st --help` to explore more features!
590"#,
591                    self.stats.total_files,
592                    self.stats.total_dirs,
593                    self.stats.total_size,
594                    self.project_type
595                )
596            }
597        };
598
599        fs::write(claude_md_path, content)?;
600
601        Ok(true)
602    }
603
604    /// Show what settings would be generated without writing
605    pub fn show_suggested(&self) -> Result<()> {
606        println!(
607            "šŸ“‹ Suggested Claude integration for {:?} project:\n",
608            self.project_type
609        );
610
611        // NO automatic UserPromptSubmit dumps - AI requests context via MCP tools when needed
612        let hooks = match self.project_type {
613            ProjectType::Rust => json!({
614                "SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
615                "SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}],
616                "PreToolUse": [{"matcher": "cargo (build|test|run)", "hooks": [{"type": "command", "command": "st -m summary --depth 1 target/"}]}]
617            }),
618            ProjectType::Python => json!({
619                "SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
620                "SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}],
621                "PreToolUse": [{"matcher": "pytest|python.*test", "hooks": [{"type": "command", "command": "st -m summary --depth 2 tests/"}]}]
622            }),
623            _ => json!({
624                "SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
625                "SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}]
626            }),
627        };
628
629        let settings = json!({"hooks": hooks});
630        println!("━━━ Add to .claude/settings.json ━━━");
631        println!("{}\n", serde_json::to_string_pretty(&settings)?);
632
633        println!("šŸ’” Or run: st --setup-claude (will ask before overwriting)");
634        Ok(())
635    }
636}
637
638// =============================================================================
639// MCP Server Auto-Installer - "One command, infinite context!" šŸŽÆ
640// =============================================================================
641
642/// Result of MCP installation attempt
643#[derive(Debug)]
644pub struct McpInstallResult {
645    pub success: bool,
646    pub config_path: PathBuf,
647    pub backup_path: Option<PathBuf>,
648    pub message: String,
649    pub was_update: bool,
650}
651
652/// MCP Server installer for Claude Desktop
653/// Automatically adds Smart Tree to claude_desktop_config.json
654pub struct McpInstaller {
655    /// Path to the st binary (defaults to current exe or 'st' in PATH)
656    st_binary_path: PathBuf,
657    /// Custom config path override (for testing)
658    custom_config_path: Option<PathBuf>,
659}
660
661impl McpInstaller {
662    /// Create new installer with auto-detected st binary path
663    pub fn new() -> Result<Self> {
664        // Try to find the st binary
665        let st_binary_path = Self::find_st_binary()?;
666
667        Ok(Self {
668            st_binary_path,
669            custom_config_path: None,
670        })
671    }
672
673    /// Create installer with custom binary path
674    pub fn with_binary_path(path: PathBuf) -> Self {
675        Self {
676            st_binary_path: path,
677            custom_config_path: None,
678        }
679    }
680
681    /// Set custom config path (for testing)
682    pub fn with_config_path(mut self, path: PathBuf) -> Self {
683        self.custom_config_path = Some(path);
684        self
685    }
686
687    /// Find the st binary in common locations
688    fn find_st_binary() -> Result<PathBuf> {
689        // First, try the current executable
690        if let Ok(exe) = std::env::current_exe() {
691            if exe.file_name().map(|n| n == "st").unwrap_or(false) {
692                return Ok(exe);
693            }
694        }
695
696        // Try common install locations
697        let candidates = vec![
698            // User's cargo bin
699            dirs::home_dir().map(|h| h.join(".cargo/bin/st")),
700            // /usr/local/bin (common for manual installs)
701            Some(PathBuf::from("/usr/local/bin/st")),
702            // homebrew (macOS)
703            Some(PathBuf::from("/opt/homebrew/bin/st")),
704        ];
705
706        for candidate in candidates.into_iter().flatten() {
707            if candidate.exists() {
708                return Ok(candidate);
709            }
710        }
711
712        // Fall back to just "st" and hope it's in PATH
713        Ok(PathBuf::from("st"))
714    }
715
716    /// Get MCP target config paths for all known local desktop AI setups
717    /// Returns a list of (AgentName, ConfigPath)
718    pub fn get_all_target_configs() -> Vec<(&'static str, PathBuf)> {
719        let mut paths = Vec::new();
720
721        // 1. Claude Desktop
722        #[cfg(target_os = "macos")]
723        if let Some(h) = dirs::home_dir() {
724            paths.push(("Claude Desktop", h.join("Library/Application Support/Claude/claude_desktop_config.json")));
725        }
726        #[cfg(target_os = "windows")]
727        if let Some(c) = dirs::config_dir() {
728            paths.push(("Claude Desktop", c.join("Claude/claude_desktop_config.json")));
729        }
730        #[cfg(target_os = "linux")]
731        if let Some(c) = dirs::config_dir() {
732            paths.push(("Claude Desktop", c.join("Claude/claude_desktop_config.json")));
733        }
734
735        // 2. Gemini / Antigravity
736        if let Some(h) = dirs::home_dir() {
737            paths.push(("Antigravity", h.join(".gemini/antigravity/mcp_config.json")));
738            paths.push(("Gemini", h.join(".gemini/mcp_config.json")));
739        }
740
741        paths
742    }
743
744    /// Install Smart Tree MCP server to all detected Desktop configs
745    pub fn install_all(&self) -> Result<Vec<McpInstallResult>> {
746        let targets = if let Some(custom) = &self.custom_config_path {
747            vec![("Custom", custom.clone())]
748        } else {
749            Self::get_all_target_configs()
750        };
751
752        if targets.is_empty() {
753            anyhow::bail!("No supported agent configurations found for this OS.");
754        }
755
756        let mut results = Vec::new();
757
758        for (agent_name, config_path) in targets {
759            // Ensure parent directory exists
760            if let Some(parent) = config_path.parent() {
761                if fs::create_dir_all(parent).is_err() {
762                    continue; // Skip if we don't have permissions or bad path
763                }
764            }
765
766            // Read existing config or create new
767            let (mut config, was_update) = if config_path.exists() {
768                if let Ok(content) = fs::read_to_string(&config_path) {
769                    if let Ok(json_val) = serde_json::from_str::<Value>(&content) {
770                        (json_val, true)
771                    } else {
772                        (json!({}), false)
773                    }
774                } else {
775                    (json!({}), false)
776                }
777            } else {
778                (json!({}), false)
779            };
780
781            // Create backup if updating existing config
782            let backup_path = if was_update {
783                let backup = config_path.with_extension(format!(
784                    "json.backup.{}",
785                    Local::now().format("%Y%m%d_%H%M%S")
786                ));
787                let _ = fs::copy(&config_path, &backup);
788                Some(backup)
789            } else {
790                None
791            };
792
793            // Build the Smart Tree MCP server config
794            let st_config = json!({
795                "command": self.st_binary_path.to_string_lossy(),
796                "args": ["--mcp"],
797                "env": {}
798            });
799
800            // Update or create mcpServers section
801            if config.get("mcpServers").is_none() {
802                config["mcpServers"] = json!({});
803            }
804
805            // Check if already installed
806            let already_installed = config["mcpServers"].get("smart-tree").is_some();
807
808            // Add/update Smart Tree entry
809            config["mcpServers"]["smart-tree"] = st_config;
810
811            // Write updated config with pretty formatting
812            if let Ok(formatted) = serde_json::to_string_pretty(&config) {
813                if fs::write(&config_path, formatted).is_err() {
814                    continue; // Skip on write failure
815                }
816            }
817
818            let message = if already_installed {
819                format!(
820                    "✨ Updated Smart Tree MCP server in {}!\n\
821                       šŸ“ Config: {}\n\
822                       šŸ”§ Binary: {}",
823                    agent_name,
824                    config_path.display(),
825                    self.st_binary_path.display()
826                )
827            } else {
828                format!(
829                    "šŸŽ‰ Smart Tree MCP server installed to {}!\n\
830                       šŸ“ Config: {}\n\
831                       šŸ”§ Binary: {}",
832                    agent_name,
833                    config_path.display(),
834                    self.st_binary_path.display()
835                )
836            };
837
838            results.push(McpInstallResult {
839                success: true,
840                config_path,
841                backup_path,
842                message,
843                was_update: already_installed,
844            });
845        }
846
847        Ok(results)
848    }
849
850    /// Uninstall Smart Tree from Desktop configs
851    pub fn uninstall_all(&self) -> Result<Vec<McpInstallResult>> {
852        let targets = if let Some(custom) = &self.custom_config_path {
853            vec![("Custom", custom.clone())]
854        } else {
855            Self::get_all_target_configs()
856        };
857
858        let mut results = Vec::new();
859
860        for (agent_name, config_path) in targets {
861            if !config_path.exists() {
862                continue;
863            }
864
865            let content = match fs::read_to_string(&config_path) {
866                Ok(c) => c,
867                Err(_) => continue,
868            };
869
870            let mut config: Value = match serde_json::from_str(&content) {
871                Ok(c) => c,
872                Err(_) => continue,
873            };
874
875            let was_removed = if let Some(servers) = config.get_mut("mcpServers").and_then(|s| s.as_object_mut()) {
876                servers.remove("smart-tree").is_some()
877            } else {
878                false
879            };
880
881            if was_removed {
882                let backup = config_path.with_extension(format!(
883                    "json.backup.{}",
884                    Local::now().format("%Y%m%d_%H%M%S")
885                ));
886                let _ = fs::copy(&config_path, &backup);
887
888                if let Ok(formatted) = serde_json::to_string_pretty(&config) {
889                    let _ = fs::write(&config_path, formatted);
890                }
891
892                results.push(McpInstallResult {
893                    success: true,
894                    config_path: config_path.clone(),
895                    backup_path: Some(backup),
896                    message: format!(
897                        "šŸ—‘ļø Removed Smart Tree MCP server from {}.\n\
898                           šŸ“ Config: {}",
899                        agent_name,
900                        config_path.display()
901                    ),
902                    was_update: true,
903                });
904            }
905        }
906
907        Ok(results)
908    }
909
910    /// Check if Smart Tree is installed in any target config
911    pub fn is_installed(&self) -> Result<bool> {
912        let targets = if let Some(custom) = &self.custom_config_path {
913            vec![("Custom", custom.clone())]
914        } else {
915            Self::get_all_target_configs()
916        };
917
918        for (_, path) in targets {
919            if path.exists() {
920                if let Ok(content) = fs::read_to_string(&path) {
921                    if let Ok(config) = serde_json::from_str::<Value>(&content) {
922                        if config["mcpServers"].get("smart-tree").is_some() {
923                            return Ok(true);
924                        }
925                    }
926                }
927            }
928        }
929
930        Ok(false)
931    }
932
933    /// Get status information about current installation across agents
934    pub fn status(&self) -> Result<Value> {
935        let targets = Self::get_all_target_configs();
936        let is_installed = self.is_installed().unwrap_or(false);
937        
938        let paths: Vec<String> = targets.into_iter()
939            .map(|(_, p)| p.display().to_string())
940            .collect();
941
942        Ok(json!({
943            "installed": is_installed,
944            "config_paths": paths,
945            "binary_path": self.st_binary_path.display().to_string(),
946            "binary_exists": self.st_binary_path.exists(),
947        }))
948    }
949}
950
951impl Default for McpInstaller {
952    fn default() -> Self {
953        Self::new().unwrap_or_else(|_| Self {
954            st_binary_path: PathBuf::from("st"),
955            custom_config_path: None,
956        })
957    }
958}
959
960/// Quick installation function for CLI use
961/// Returns a human-readable result message
962pub fn install_mcp_to_desktop() -> Result<String> {
963    let installer = McpInstaller::new()?;
964    let results = installer.install_all()?;
965    let msg = results.into_iter()
966        .filter(|r| r.success)
967        .map(|r| r.message)
968        .collect::<Vec<_>>()
969        .join("\n\n");
970    if msg.is_empty() {
971        Ok("Nothing to install or update.".to_string())
972    } else {
973        Ok(msg)
974    }
975}
976
977/// Quick uninstall function for CLI use
978pub fn uninstall_mcp_from_desktop() -> Result<String> {
979    let installer = McpInstaller::new()?;
980    let results = installer.uninstall_all()?;
981    let msg = results.into_iter()
982        .filter(|r| r.success)
983        .map(|r| r.message)
984        .collect::<Vec<_>>()
985        .join("\n\n");
986    if msg.is_empty() {
987        Ok("No installations found to remove.".to_string())
988    } else {
989        Ok(msg)
990    }
991}
992
993/// Check MCP installation status
994pub fn check_mcp_installation_status() -> Result<String> {
995    let installer = McpInstaller::new()?;
996    let status = installer.status()?;
997
998    let installed = status["installed"].as_bool().unwrap_or(false);
999    let config_paths = status["config_paths"].as_array();
1000
1001    if installed {
1002        Ok(format!(
1003            "āœ… Smart Tree MCP server is installed!\n\
1004             šŸ“ Configs: {:?}\n\
1005             šŸ”§ Binary: {}",
1006            config_paths,
1007            status["binary_path"].as_str().unwrap_or("st")
1008        ))
1009    } else {
1010        Ok(format!(
1011            "āŒ Smart Tree MCP server is NOT installed.\n\
1012             šŸ“ Expected configs: {:?}\n\
1013             šŸ’” Run 'st --mcp-install' to install",
1014            config_paths
1015        ))
1016    }
1017}