Skip to main content

st/
ai_install.rs

1//! AI Integration Installer - Unified setup for all AI platforms
2//!
3//! "One command to rule them all!" - The Cheet
4//!
5//! This module provides interactive and non-interactive installation
6//! of Smart Tree's AI integrations: MCP servers, hooks, plugins, and configs.
7//!
8//! Note: This is a daemon-only feature. Use `std install-ai` instead of `st -i`.
9
10use crate::claude_init::{ClaudeInit, McpInstaller};
11use anyhow::{Context, Result};
12
13/// Installation scope for AI integration
14#[derive(Debug, Clone, Copy, Default, PartialEq)]
15pub enum InstallScope {
16    /// Project-local installation (.claude/ in current directory)
17    #[default]
18    Project,
19    /// User-wide installation (~/.claude/ or ~/.config/)
20    User,
21}
22
23/// Target AI platform for configuration
24#[derive(Debug, Clone, Copy, Default, PartialEq)]
25pub enum AiTarget {
26    /// Claude (Anthropic) - default, most features
27    #[default]
28    Claude,
29    /// ChatGPT (OpenAI)
30    Chatgpt,
31    /// Gemini (Google)
32    Gemini,
33    /// Universal - generic config for any AI
34    Universal,
35}
36use serde_json::{json, Value};
37use std::fs;
38use std::io::{self, Write};
39use std::path::PathBuf;
40
41/// AI Integration Installer - handles setup for all AI platforms
42pub struct AiInstaller {
43    /// Installation scope (project-local or user-wide)
44    scope: InstallScope,
45    /// Target AI platform
46    target: AiTarget,
47    /// Whether to run in interactive mode
48    interactive: bool,
49    /// Project path (for project-scoped installations)
50    project_path: PathBuf,
51}
52
53/// Installation options discovered during interactive mode
54#[derive(Debug, Clone)]
55pub struct InstallOptions {
56    pub install_mcp: bool,
57    pub install_hooks: bool,
58    pub install_claude_md: bool,
59    pub create_settings: bool,
60    pub cleanup_foreign: bool,
61}
62
63impl Default for InstallOptions {
64    fn default() -> Self {
65        Self {
66            install_mcp: true,
67            install_hooks: true,
68            install_claude_md: true,
69            create_settings: true,
70            cleanup_foreign: true, // Clean by default - opinionated!
71        }
72    }
73}
74
75impl AiInstaller {
76    /// Create a new AI installer
77    pub fn new(scope: InstallScope, target: AiTarget, interactive: bool) -> Result<Self> {
78        let project_path = std::env::current_dir().context("Failed to get current directory")?;
79        Ok(Self {
80            scope,
81            target,
82            interactive,
83            project_path,
84        })
85    }
86
87    /// Run the installation process
88    pub fn install(&self) -> Result<()> {
89        println!("\n{}", self.get_header());
90
91        if self.interactive {
92            self.run_interactive()
93        } else {
94            self.run_non_interactive()
95        }
96    }
97
98    /// Get a colorful header based on target
99    fn get_header(&self) -> String {
100        match self.target {
101            AiTarget::Claude => "๐Ÿค– Smart Tree AI Integration - Claude Setup".to_string(),
102            AiTarget::Chatgpt => "๐Ÿค– Smart Tree AI Integration - ChatGPT Setup".to_string(),
103            AiTarget::Gemini => "๐Ÿค– Smart Tree AI Integration - Gemini Setup".to_string(),
104            AiTarget::Universal => "๐Ÿค– Smart Tree AI Integration - Universal Setup".to_string(),
105        }
106    }
107
108    /// Run interactive installation with user prompts
109    fn run_interactive(&self) -> Result<()> {
110        println!(
111            "\nThis will configure Smart Tree for {}.",
112            self.target_name()
113        );
114        println!("Scope: {}\n", self.scope_description());
115
116        // Show existing configuration status first
117        let manager = ConfigManager::new(self.scope);
118        let existing = manager.list_configs();
119
120        println!("Current Status:");
121        for config in &existing {
122            let icon = if config.enabled { "โœ…" } else { "โฌœ" };
123            println!("  {} {}", icon, config.name);
124        }
125
126        // Discover what can be installed/updated
127        let available = self.discover_options();
128
129        println!("\nActions:");
130        println!("  [a] Install/Update ALL integrations (includes cleanup)");
131        println!("  [c] Clean foreign MCPs/hooks only - remove tool sprawl");
132        if available.install_mcp {
133            let status = if existing.iter().any(|c| c.name.contains("MCP") && c.enabled) {
134                "(update)"
135            } else {
136                "(install)"
137            };
138            println!(
139                "  [1] MCP Server {} - Enable 30+ tools in your AI assistant",
140                status
141            );
142        }
143        if available.install_hooks {
144            let status = if existing
145                .iter()
146                .any(|c| c.name.contains("Hooks") && c.enabled)
147            {
148                "(update)"
149            } else {
150                "(install)"
151            };
152            println!("  [2] Hooks {} - Automatic context on every prompt", status);
153        }
154        if available.install_claude_md {
155            let status = if existing
156                .iter()
157                .any(|c| c.name.contains("CLAUDE.md") && c.enabled)
158            {
159                "(update)"
160            } else {
161                "(create)"
162            };
163            println!("  [3] CLAUDE.md {} - Project-specific AI guidance", status);
164        }
165        if available.create_settings {
166            let status = if existing
167                .iter()
168                .any(|c| c.name.contains("Settings") && c.enabled)
169            {
170                "(update)"
171            } else {
172                "(create)"
173            };
174            println!("  [4] Settings {} - AI-optimized configuration", status);
175        }
176        println!("  [s] Show detailed status only");
177        println!("  [q] Quit without changes");
178
179        print!("\nChoice [a/1-4/s/q]: ");
180        io::stdout().flush()?;
181
182        let mut input = String::new();
183        io::stdin().read_line(&mut input)?;
184        let input = input.trim().to_lowercase();
185
186        match input.as_str() {
187            "q" | "quit" | "exit" => {
188                println!("No changes made.");
189                Ok(())
190            }
191            "s" | "status" => {
192                manager.display_configs();
193                Ok(())
194            }
195            "c" | "clean" | "cleanup" => {
196                // Cleanup only, no installations
197                let cleanup_only = InstallOptions {
198                    install_mcp: false,
199                    install_hooks: false,
200                    install_claude_md: false,
201                    create_settings: false,
202                    cleanup_foreign: true,
203                };
204                self.execute_install(&cleanup_only)
205            }
206            "a" | "all" | "" => self.execute_install(&available),
207            _ => {
208                let options = self.parse_selection(&input, &available);
209                self.execute_install(&options)
210            }
211        }
212    }
213
214    /// Run non-interactive installation with defaults
215    fn run_non_interactive(&self) -> Result<()> {
216        let options = InstallOptions::default();
217        self.execute_install(&options)
218    }
219
220    /// Discover what installation options are available
221    fn discover_options(&self) -> InstallOptions {
222        let mut options = InstallOptions::default();
223
224        match self.scope {
225            InstallScope::Project => {
226                // Project-level installations
227                options.install_claude_md = true;
228                options.create_settings = true;
229                options.install_hooks = true;
230
231                // MCP is user-level only for Claude Desktop
232                options.install_mcp = matches!(self.target, AiTarget::Claude | AiTarget::Universal | AiTarget::Gemini);
233            }
234            InstallScope::User => {
235                // User-level installations
236                options.install_mcp = matches!(self.target, AiTarget::Claude | AiTarget::Universal | AiTarget::Gemini);
237                options.install_hooks = true;
238                options.install_claude_md = false; // No project to add CLAUDE.md to
239                options.create_settings = true;
240            }
241        }
242
243        options
244    }
245
246    /// Parse user selection
247    fn parse_selection(&self, input: &str, available: &InstallOptions) -> InstallOptions {
248        let mut options = InstallOptions {
249            install_mcp: false,
250            install_hooks: false,
251            install_claude_md: false,
252            create_settings: false,
253            cleanup_foreign: false,
254        };
255
256        for c in input.chars() {
257            match c {
258                '1' if available.install_mcp => options.install_mcp = true,
259                '2' if available.install_hooks => options.install_hooks = true,
260                '3' if available.install_claude_md => options.install_claude_md = true,
261                '4' if available.create_settings => options.create_settings = true,
262                'c' => options.cleanup_foreign = true,
263                _ => {}
264            }
265        }
266
267        options
268    }
269
270    /// Execute the installation with the given options
271    fn execute_install(&self, options: &InstallOptions) -> Result<()> {
272        let mut installed = Vec::new();
273        let mut errors = Vec::new();
274
275        // FIRST: Clean up foreign MCPs and hooks if requested
276        // This runs before any installations to ensure a clean slate
277        if options.cleanup_foreign {
278            match self.cleanup_foreign_integrations() {
279                Ok(count) if count > 0 => installed.push("Foreign integrations cleaned"),
280                Ok(_) => {} // Nothing to clean
281                Err(e) => errors.push(format!("Cleanup: {}", e)),
282            }
283        }
284
285        // Install MCP server
286        if options.install_mcp {
287            match self.install_mcp() {
288                Ok(_) => installed.push("MCP Server"),
289                Err(e) => errors.push(format!("MCP: {}", e)),
290            }
291        }
292
293        // Install hooks
294        if options.install_hooks {
295            match self.install_hooks() {
296                Ok(_) => installed.push("Hooks"),
297                Err(e) => errors.push(format!("Hooks: {}", e)),
298            }
299        }
300
301        // Create CLAUDE.md (or equivalent for other AIs)
302        if options.install_claude_md {
303            match self.create_ai_guidance() {
304                Ok(_) => installed.push("AI Guidance File"),
305                Err(e) => errors.push(format!("AI Guidance: {}", e)),
306            }
307        }
308
309        // Create settings
310        if options.create_settings {
311            match self.create_settings() {
312                Ok(_) => installed.push("Settings"),
313                Err(e) => errors.push(format!("Settings: {}", e)),
314            }
315        }
316
317        // Summary
318        println!("\n๐Ÿ“‹ Installation Summary:");
319        if !installed.is_empty() {
320            println!("  โœ… Installed: {}", installed.join(", "));
321        }
322        if !errors.is_empty() {
323            println!("  โŒ Errors:");
324            for error in &errors {
325                println!("     โ€ข {}", error);
326            }
327        }
328
329        if errors.is_empty() {
330            println!("\n๐ŸŽ‰ Smart Tree AI integration complete!");
331            self.show_next_steps();
332            Ok(())
333        } else if !installed.is_empty() {
334            println!("\nโš ๏ธ  Some components installed with errors");
335            self.show_next_steps();
336            Ok(())
337        } else {
338            anyhow::bail!("Installation failed: {}", errors.join("; "))
339        }
340    }
341
342    /// Install MCP server
343    fn install_mcp(&self) -> Result<()> {
344        match self.target {
345            AiTarget::Claude | AiTarget::Universal | AiTarget::Gemini => {
346                // 1. Install to Desktop configs
347                let installer = McpInstaller::new()?;
348                let results = installer.install_all()?;
349                for result in results {
350                    if result.success {
351                        println!(
352                            "  โœ… {}",
353                            result.message.lines().next().unwrap_or("MCP installed")
354                        );
355                    }
356                }
357
358                // 2. Also create/update project's .mcp.json so Claude Code can find it
359                self.ensure_project_mcp_json()?;
360
361                Ok(())
362            }
363            _ => {
364                println!("  โ„น๏ธ  MCP not supported for {} yet", self.target_name());
365                Ok(())
366            }
367        }
368    }
369
370    /// Ensure the project has a .mcp.json with st configured
371    fn ensure_project_mcp_json(&self) -> Result<()> {
372        let mcp_json_path = self.project_path.join(".mcp.json");
373
374        // stdio MCP configuration (traditional, always works)
375        let st_stdio_config = json!({
376            "type": "stdio",
377            "command": "st",
378            "args": ["--mcp"],
379            "env": {}
380        });
381
382        // HTTP MCP configuration (The Custodian watches here! ๐Ÿงน)
383        // Uses SSE transport - daemon must be running: st --http-daemon
384        let st_http_config = json!({
385            "type": "sse",
386            "url": "http://localhost:28428/mcp",
387            "_note": "Run 'st --http-daemon' first. The Custodian monitors all operations!"
388        });
389
390        if mcp_json_path.exists() {
391            // Read and update existing config
392            let content = fs::read_to_string(&mcp_json_path).context("Failed to read .mcp.json")?;
393            let mut config: Value =
394                serde_json::from_str(&content).unwrap_or_else(|_| json!({"mcpServers": {}}));
395
396            // Ensure mcpServers exists and has both st and st-http
397            if let Some(obj) = config.as_object_mut() {
398                let servers = obj
399                    .entry("mcpServers".to_string())
400                    .or_insert_with(|| json!({}));
401                if let Some(servers_obj) = servers.as_object_mut() {
402                    let mut updated = false;
403                    if !servers_obj.contains_key("st") {
404                        servers_obj.insert("st".to_string(), st_stdio_config);
405                        updated = true;
406                    }
407                    if !servers_obj.contains_key("st-http") {
408                        servers_obj.insert("st-http".to_string(), st_http_config);
409                        updated = true;
410                    }
411                    if updated {
412                        fs::write(&mcp_json_path, serde_json::to_string_pretty(&config)?)?;
413                        println!("  โœ… Updated {}", mcp_json_path.display());
414                    }
415                }
416            }
417        } else {
418            // Create new .mcp.json with both st servers
419            let config = json!({
420                "mcpServers": {
421                    "st": st_stdio_config,
422                    "st-http": st_http_config
423                },
424                "_comment": "st: stdio (always works), st-http: HTTP with The Custodian (run 'st --http-daemon' first)"
425            });
426            fs::write(&mcp_json_path, serde_json::to_string_pretty(&config)?)?;
427            println!(
428                "  โœ… Created {} with st MCP servers (stdio + HTTP)",
429                mcp_json_path.display()
430            );
431        }
432
433        Ok(())
434    }
435
436    /// Install hooks
437    fn install_hooks(&self) -> Result<()> {
438        let hooks_dir = match self.scope {
439            InstallScope::Project => self.project_path.join(".claude"),
440            InstallScope::User => dirs::home_dir()
441                .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?
442                .join(".claude"),
443        };
444
445        fs::create_dir_all(&hooks_dir)?;
446
447        let hooks_config = match self.target {
448            AiTarget::Claude => self.get_claude_hooks(),
449            AiTarget::Chatgpt => self.get_generic_hooks("chatgpt"),
450            AiTarget::Gemini => self.get_generic_hooks("gemini"),
451            AiTarget::Universal => self.get_generic_hooks("universal"),
452        };
453
454        let hooks_file = hooks_dir.join("hooks.json");
455        fs::write(&hooks_file, serde_json::to_string_pretty(&hooks_config)?)?;
456        println!("  โœ… Hooks configured at {}", hooks_file.display());
457        Ok(())
458    }
459
460    /// Get Claude-specific hooks (matches claude_init.rs format)
461    /// NO automatic UserPromptSubmit dumps - AI requests context via MCP tools when needed
462    fn get_claude_hooks(&self) -> Value {
463        json!({
464            "SessionStart": [{
465                "matcher": "",
466                "hooks": [{
467                    "type": "command",
468                    "command": "st --claude-restore"
469                }]
470            }],
471            "SessionEnd": [{
472                "matcher": "",
473                "hooks": [{
474                    "type": "command",
475                    "command": "st --claude-save"
476                }]
477            }]
478        })
479    }
480
481    /// Get generic hooks for other AI platforms
482    fn get_generic_hooks(&self, platform: &str) -> Value {
483        json!({
484            "context_provider": {
485                "command": format!("st -m context --depth 3 ."),
486                "platform": platform,
487                "description": "Provides project context on demand"
488            }
489        })
490    }
491
492    /// Create AI guidance file (CLAUDE.md or equivalent)
493    fn create_ai_guidance(&self) -> Result<()> {
494        if matches!(self.scope, InstallScope::User) {
495            println!("  โ„น๏ธ  AI guidance file is project-specific, skipping for user scope");
496            return Ok(());
497        }
498
499        let init = ClaudeInit::new(self.project_path.clone())?;
500        init.setup()?;
501        Ok(())
502    }
503
504    /// Create settings file
505    fn create_settings(&self) -> Result<()> {
506        let settings_dir = match self.scope {
507            InstallScope::Project => self.project_path.join(".claude"),
508            InstallScope::User => dirs::home_dir()
509                .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?
510                .join(".claude"),
511        };
512
513        fs::create_dir_all(&settings_dir)?;
514
515        let settings = json!({
516            "smart_tree": {
517                "version": env!("CARGO_PKG_VERSION"),
518                "target": self.target_name(),
519                "scope": match self.scope {
520                    InstallScope::Project => "project",
521                    InstallScope::User => "user",
522                },
523                "auto_configured": true,
524                "features": {
525                    "context_on_prompt": true,
526                    "session_persistence": true,
527                    "mcp_integration": matches!(self.target, AiTarget::Claude | AiTarget::Universal)
528                }
529            }
530        });
531
532        let settings_file = settings_dir.join("settings.json");
533
534        // Merge with existing if present
535        let final_settings = if settings_file.exists() {
536            let existing: Value = serde_json::from_str(&fs::read_to_string(&settings_file)?)?;
537            self.merge_settings(existing, settings)
538        } else {
539            settings
540        };
541
542        fs::write(
543            &settings_file,
544            serde_json::to_string_pretty(&final_settings)?,
545        )?;
546        println!("  โœ… Settings saved to {}", settings_file.display());
547        Ok(())
548    }
549
550    /// Merge existing settings with new ones
551    fn merge_settings(&self, existing: Value, new: Value) -> Value {
552        let mut result = existing;
553        if let (Some(existing_obj), Some(new_obj)) = (result.as_object_mut(), new.as_object()) {
554            for (key, value) in new_obj {
555                existing_obj.insert(key.clone(), value.clone());
556            }
557        }
558        result
559    }
560
561    /// Clean up foreign MCP integrations and invasive hooks
562    /// Returns the number of items cleaned
563    fn cleanup_foreign_integrations(&self) -> Result<usize> {
564        let mut cleaned = 0;
565
566        // Patterns that indicate foreign/unwanted integrations
567        // Based on Security documentation analysis of supply chain attacks
568        let foreign_patterns = [
569            // Known malicious packages from security disclosure
570            "claude-flow",
571            "agentic-flow",
572            "ruv-swarm",
573            "flow-nexus",
574            "hive-mind",
575            "superdisco",
576            "agent-booster",
577            // IPFS/IPNS patterns - phone home endpoints
578            "ipfs.io",
579            "dweb.link",
580            "cloudflare-ipfs.com",
581            "gateway.pinata.cloud",
582            "w3s.link",
583            "4everland.io",
584            // IPNS mutable names (k51qzi5uqu5...)
585            "k51qzi5uqu5",
586            // Dynamic npm execution with volatile tags
587            "@alpha",
588            "@beta",
589            "@latest",
590            "@next",
591            "@canary",
592            "npx ", // External npm packages running on every command
593            // Malicious swarm patterns
594            "swarm",
595            "queen",
596            "worker",
597            // Registry and pattern fetching
598            "registry",
599            "BOOTSTRAP_REGISTRIES",
600            "ipnsName",
601            "registrySignature",
602        ];
603
604        // 1. Clean parent directory .mcp.json files (inherited MCPs!)
605        // Walk up from project to root, cleaning any .mcp.json with foreign servers
606        let mut current = self.project_path.clone();
607        loop {
608            let mcp_json = current.join(".mcp.json");
609            if mcp_json.exists() && mcp_json != self.project_path.join(".mcp.json") {
610                // Don't clean the project's own .mcp.json, just parents
611                cleaned += self.clean_parent_mcp_json(&mcp_json, &foreign_patterns)?;
612            }
613            if let Some(parent) = current.parent() {
614                if parent == current {
615                    break; // Reached root
616                }
617                current = parent.to_path_buf();
618            } else {
619                break;
620            }
621        }
622
623        // 2. Clean ~/.claude/.claude/settings.json (the nested one with enabledMcpjsonServers)
624        let nested_settings = dirs::home_dir().map(|h| h.join(".claude/.claude/settings.json"));
625
626        if let Some(path) = nested_settings {
627            if path.exists() {
628                cleaned += self.clean_settings_file(&path, &foreign_patterns)?;
629            }
630        }
631
632        // 3. Clean ~/.claude/settings.json
633        let user_settings = dirs::home_dir().map(|h| h.join(".claude/settings.json"));
634
635        if let Some(path) = user_settings {
636            if path.exists() {
637                cleaned += self.clean_settings_file(&path, &foreign_patterns)?;
638            }
639        }
640
641        // 4. Clean project-level .claude/settings.json if in project scope
642        if matches!(self.scope, InstallScope::Project) {
643            let project_settings = self.project_path.join(".claude/settings.json");
644            if project_settings.exists() {
645                cleaned += self.clean_settings_file(&project_settings, &foreign_patterns)?;
646            }
647        }
648
649        if cleaned > 0 {
650            println!("  ๐Ÿงน Cleaned {} foreign integration(s)", cleaned);
651        }
652
653        Ok(cleaned)
654    }
655
656    /// Clean a parent .mcp.json file of foreign MCP servers
657    fn clean_parent_mcp_json(&self, path: &std::path::Path, patterns: &[&str]) -> Result<usize> {
658        let content = fs::read_to_string(path).context("Failed to read .mcp.json")?;
659
660        // Handle empty or whitespace-only files
661        if content.trim().is_empty() {
662            // Delete the empty file as it's not useful
663            let _ = fs::remove_file(path);
664            return Ok(0);
665        }
666
667        let mut config: Value = match serde_json::from_str(&content) {
668            Ok(v) => v,
669            Err(_) => {
670                // Invalid JSON - delete the malformed file
671                let _ = fs::remove_file(path);
672                return Ok(0);
673            }
674        };
675
676        let mut cleaned = 0;
677
678        if let Some(obj) = config.as_object_mut() {
679            if let Some(servers) = obj.get_mut("mcpServers") {
680                if let Some(servers_obj) = servers.as_object_mut() {
681                    let server_names: Vec<String> = servers_obj.keys().cloned().collect();
682
683                    for name in server_names {
684                        // Check if server name or config matches foreign patterns
685                        let config_str = servers_obj
686                            .get(&name)
687                            .map(|v| serde_json::to_string(v).unwrap_or_default())
688                            .unwrap_or_default();
689
690                        if patterns
691                            .iter()
692                            .any(|p| name.contains(p) || config_str.contains(p))
693                        {
694                            servers_obj.remove(&name);
695                            cleaned += 1;
696                            println!("    Removed MCP server '{}' from {}", name, path.display());
697                        }
698                    }
699                }
700            }
701        }
702
703        // Write back if we made changes
704        if cleaned > 0 {
705            fs::write(path, serde_json::to_string_pretty(&config)?)?;
706        }
707
708        Ok(cleaned)
709    }
710
711    /// Clean a specific settings file of foreign integrations
712    fn clean_settings_file(&self, path: &std::path::Path, patterns: &[&str]) -> Result<usize> {
713        let content = fs::read_to_string(path).context("Failed to read settings file")?;
714
715        let mut config: Value =
716            serde_json::from_str(&content).context("Failed to parse settings JSON")?;
717
718        let mut cleaned = 0;
719
720        // Remove enabledMcpjsonServers entirely or filter it
721        if let Some(obj) = config.as_object_mut() {
722            if obj.contains_key("enabledMcpjsonServers") {
723                obj.remove("enabledMcpjsonServers");
724                cleaned += 1;
725                println!("    Removed enabledMcpjsonServers from {}", path.display());
726            }
727
728            // Clean hooks that match foreign patterns
729            if let Some(hooks) = obj.get_mut("hooks") {
730                if let Some(hooks_obj) = hooks.as_object_mut() {
731                    let hook_types: Vec<String> = hooks_obj.keys().cloned().collect();
732
733                    for hook_type in hook_types {
734                        if let Some(hook_array) = hooks_obj.get_mut(&hook_type) {
735                            if let Some(arr) = hook_array.as_array_mut() {
736                                let original_len = arr.len();
737
738                                // Filter out hooks with foreign patterns
739                                arr.retain(|hook| {
740                                    let hook_str = serde_json::to_string(hook).unwrap_or_default();
741                                    !patterns.iter().any(|p| hook_str.contains(p))
742                                });
743
744                                let removed = original_len - arr.len();
745                                if removed > 0 {
746                                    cleaned += removed;
747                                    println!(
748                                        "    Removed {} foreign {} hook(s)",
749                                        removed, hook_type
750                                    );
751                                }
752                            }
753                        }
754                    }
755                }
756            }
757        }
758
759        // Write back if we made changes
760        if cleaned > 0 {
761            fs::write(path, serde_json::to_string_pretty(&config)?)?;
762        }
763
764        Ok(cleaned)
765    }
766
767    /// Get human-readable target name
768    fn target_name(&self) -> &'static str {
769        match self.target {
770            AiTarget::Claude => "Claude",
771            AiTarget::Chatgpt => "ChatGPT",
772            AiTarget::Gemini => "Gemini",
773            AiTarget::Universal => "Universal AI",
774        }
775    }
776
777    /// Get scope description
778    fn scope_description(&self) -> &'static str {
779        match self.scope {
780            InstallScope::Project => "Project-local (.claude/ in current directory)",
781            InstallScope::User => "User-wide (~/.claude/ or ~/.config/)",
782        }
783    }
784
785    /// Show next steps after installation
786    fn show_next_steps(&self) {
787        println!("\n๐Ÿ“š Next Steps:");
788
789        match self.target {
790            AiTarget::Claude => {
791                println!("  1. Restart Claude Desktop to load MCP tools");
792                println!("  2. Try: 'st -m context .' to see project context");
793                println!("  3. Use '/hooks' in Claude Code to manage hooks");
794            }
795            AiTarget::Chatgpt | AiTarget::Gemini => {
796                println!("  1. Run 'st -m context .' and paste the output");
797                println!("  2. The AI will understand your project structure");
798            }
799            AiTarget::Universal => {
800                println!("  1. Use 'st -m ai' for AI-optimized output");
801                println!("  2. Use 'st -m quantum' for compressed context");
802                println!("  3. MCP integration available for Claude Desktop");
803            }
804        }
805
806        println!("\n๐Ÿ’ก Pro tip: Run 'st --help' to explore all features!");
807    }
808}
809
810/// Quick installation function for CLI use
811pub fn run_ai_install(scope: InstallScope, target: AiTarget, interactive: bool) -> Result<()> {
812    let installer = AiInstaller::new(scope, target, interactive)?;
813    installer.install()
814}
815
816// =============================================================================
817// Configuration Manager - View and manage existing AI integrations
818// =============================================================================
819
820/// Existing configuration status
821#[derive(Debug)]
822pub struct ConfigStatus {
823    pub name: String,
824    pub enabled: bool,
825    pub path: Option<PathBuf>,
826    pub details: String,
827}
828
829/// AI Configuration Manager - lists and manages existing configs
830pub struct ConfigManager {
831    scope: InstallScope,
832}
833
834impl ConfigManager {
835    pub fn new(scope: InstallScope) -> Self {
836        Self { scope }
837    }
838
839    /// Get all existing configurations
840    pub fn list_configs(&self) -> Vec<ConfigStatus> {
841        let mut configs = Vec::new();
842
843        // Check MCP installation
844        configs.push(self.check_mcp_status());
845
846        // Check hooks
847        configs.push(self.check_hooks_status());
848
849        // Check settings
850        configs.push(self.check_settings_status());
851
852        // Check CLAUDE.md (project only)
853        if matches!(self.scope, InstallScope::Project) {
854            configs.push(self.check_claude_md_status());
855        }
856
857        configs
858    }
859
860    /// Display configurations in a nice format
861    pub fn display_configs(&self) {
862        let configs = self.list_configs();
863
864        println!(
865            "\n๐Ÿ“‹ AI Integration Status ({})",
866            match self.scope {
867                InstallScope::Project => "Project",
868                InstallScope::User => "User",
869            }
870        );
871        println!("{}", "โ”€".repeat(50));
872
873        for config in &configs {
874            let status_icon = if config.enabled { "โœ…" } else { "โŒ" };
875            println!("\n{} {}", status_icon, config.name);
876            println!("   {}", config.details);
877            if let Some(path) = &config.path {
878                println!("   ๐Ÿ“ {}", path.display());
879            }
880        }
881
882        println!("\n{}", "โ”€".repeat(50));
883        println!("๐Ÿ’ก Use 'st -i' to install/update integrations");
884    }
885
886    fn check_mcp_status(&self) -> ConfigStatus {
887        let installer = McpInstaller::default();
888        let installed = installer.is_installed().unwrap_or(false);
889        let configs = McpInstaller::get_all_target_configs();
890        let first_config = configs.first().map(|(_, p)| p.clone());
891
892        ConfigStatus {
893            name: "MCP Server (Agents)".to_string(),
894            enabled: installed,
895            path: first_config,
896            details: if installed {
897                "Smart Tree MCP tools available in desktop agents".to_string()
898            } else {
899                "Not installed - run 'st -i' to enable 30+ AI tools".to_string()
900            },
901        }
902    }
903
904    fn check_hooks_status(&self) -> ConfigStatus {
905        let hooks_dir = match self.scope {
906            InstallScope::Project => std::env::current_dir().ok(),
907            InstallScope::User => dirs::home_dir(),
908        }
909        .map(|p| p.join(".claude"));
910
911        let hooks_file = hooks_dir.as_ref().map(|d| d.join("hooks.json"));
912        let exists = hooks_file.as_ref().map(|p| p.exists()).unwrap_or(false);
913
914        let details = if exists {
915            if let Some(path) = &hooks_file {
916                if let Ok(content) = fs::read_to_string(path) {
917                    if let Ok(config) = serde_json::from_str::<Value>(&content) {
918                        let hook_count = config.as_object().map(|o| o.len()).unwrap_or(0);
919                        format!("{} hook(s) configured", hook_count)
920                    } else {
921                        "Configuration file exists but may be invalid".to_string()
922                    }
923                } else {
924                    "Configuration file exists".to_string()
925                }
926            } else {
927                "Hooks configured".to_string()
928            }
929        } else {
930            "Not configured - automatic context on prompts".to_string()
931        };
932
933        ConfigStatus {
934            name: "Claude Code Hooks".to_string(),
935            enabled: exists,
936            path: hooks_file,
937            details,
938        }
939    }
940
941    fn check_settings_status(&self) -> ConfigStatus {
942        let settings_dir = match self.scope {
943            InstallScope::Project => std::env::current_dir().ok(),
944            InstallScope::User => dirs::home_dir(),
945        }
946        .map(|p| p.join(".claude"));
947
948        let settings_file = settings_dir.as_ref().map(|d| d.join("settings.json"));
949        let exists = settings_file.as_ref().map(|p| p.exists()).unwrap_or(false);
950
951        let details = if exists {
952            if let Some(path) = &settings_file {
953                if let Ok(content) = fs::read_to_string(path) {
954                    if let Ok(config) = serde_json::from_str::<Value>(&content) {
955                        if let Some(st) = config.get("smart_tree") {
956                            let version = st
957                                .get("version")
958                                .and_then(|v| v.as_str())
959                                .unwrap_or("unknown");
960                            format!("Smart Tree v{} settings", version)
961                        } else {
962                            "Settings file exists (no Smart Tree config)".to_string()
963                        }
964                    } else {
965                        "Settings file exists".to_string()
966                    }
967                } else {
968                    "Settings file exists".to_string()
969                }
970            } else {
971                "Settings configured".to_string()
972            }
973        } else {
974            "Not configured".to_string()
975        };
976
977        ConfigStatus {
978            name: "Smart Tree Settings".to_string(),
979            enabled: exists,
980            path: settings_file,
981            details,
982        }
983    }
984
985    fn check_claude_md_status(&self) -> ConfigStatus {
986        let claude_md = std::env::current_dir()
987            .ok()
988            .map(|p| p.join(".claude/CLAUDE.md"));
989
990        let exists = claude_md.as_ref().map(|p| p.exists()).unwrap_or(false);
991
992        ConfigStatus {
993            name: "AI Guidance (CLAUDE.md)".to_string(),
994            enabled: exists,
995            path: claude_md,
996            details: if exists {
997                "Project-specific AI instructions available".to_string()
998            } else {
999                "Not created - helps AI understand your project".to_string()
1000            },
1001        }
1002    }
1003}
1004
1005/// Show configuration status for CLI
1006pub fn show_ai_config_status(scope: InstallScope) {
1007    let manager = ConfigManager::new(scope);
1008    manager.display_configs();
1009}
1010
1011// =============================================================================
1012// Security Cleanup - Remove malicious AI integrations
1013// =============================================================================
1014
1015/// Known malicious packages and directories
1016const MALICIOUS_PACKAGES: &[&str] = &[
1017    // Known malicious packages from security disclosure
1018    "claude-flow",
1019    "agentic-flow",
1020    "superdisco",
1021    "agent-booster",
1022    "ruv-swarm",
1023    "flow-nexus",
1024    "hive-mind",
1025];
1026
1027/// Hidden directories that may contain malware persistence
1028/// Based on security disclosure analysis of supply chain attacks
1029const MALICIOUS_DIRECTORIES: &[&str] = &[
1030    ".claude-flow",       // Primary malicious package from security disclosure
1031    ".agentic-flow",      // Related malicious package variant
1032    ".superdisco",        // Known malicious pattern
1033    ".agent-booster",     // Known malicious pattern
1034    ".flow-nexus",        // Malicious swarm coordination package
1035    ".ruv-swarm",         // Malicious swarm coordination package
1036    ".hive-mind",         // Malicious swarm pattern
1037    ".ipfs-registry",     // IPFS/IPNS remote injection cache
1038    ".pattern-cache",     // Cached remote patterns/behaviors
1039    ".seraphine",         // Genesis pattern cache name from disclosure
1040];
1041
1042/// Subdirectories within ~/.claude/ that malicious packages may install into
1043const CLAUDE_SUBDIRS_TO_SCAN: &[&str] = &[
1044    "skills",
1045    "commands",
1046    "hooks",
1047    "plugins",
1048    "extensions",
1049    "tools",
1050];
1051
1052/// Finding from the security cleanup scan
1053#[derive(Debug)]
1054pub struct CleanupFinding {
1055    pub category: CleanupCategory,
1056    pub path: PathBuf,
1057    pub description: String,
1058    pub risk_level: String,
1059}
1060
1061#[derive(Debug, Clone, Copy)]
1062pub enum CleanupCategory {
1063    HiddenDirectory,
1064    ClaudeSubdirectory,
1065    McpServer,
1066    Hook,
1067    EnabledServer,
1068}
1069
1070impl std::fmt::Display for CleanupCategory {
1071    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1072        match self {
1073            CleanupCategory::HiddenDirectory => write!(f, "Hidden Directory"),
1074            CleanupCategory::ClaudeSubdirectory => write!(f, "Claude Subdirectory"),
1075            CleanupCategory::McpServer => write!(f, "MCP Server"),
1076            CleanupCategory::Hook => write!(f, "Hook"),
1077            CleanupCategory::EnabledServer => write!(f, "Enabled Server"),
1078        }
1079    }
1080}
1081
1082/// Security cleanup scanner and remediator
1083pub struct SecurityCleanup {
1084    yes: bool,
1085    findings: Vec<CleanupFinding>,
1086}
1087
1088impl SecurityCleanup {
1089    pub fn new(yes: bool) -> Self {
1090        Self {
1091            yes,
1092            findings: Vec::new(),
1093        }
1094    }
1095
1096    /// Run the full cleanup scan and remediation
1097    pub fn run(&mut self) -> Result<()> {
1098        println!("\n๐Ÿ”’ Smart Tree Security Cleanup");
1099        println!("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n");
1100        println!("Scanning for known supply chain attack patterns...\n");
1101
1102        // Phase 1: Scan for hidden malware directories
1103        self.scan_hidden_directories()?;
1104
1105        // Phase 2: Scan ~/.claude/ subdirectories (skills, commands, hooks, etc.)
1106        self.scan_claude_subdirectories()?;
1107
1108        // Phase 3: Scan MCP configurations
1109        self.scan_mcp_configurations()?;
1110
1111        // Phase 4: Scan Claude settings for malicious hooks
1112        self.scan_claude_settings()?;
1113
1114        // Phase 5: Scan parent directory .mcp.json files
1115        self.scan_parent_mcp_files()?;
1116
1117        // Display findings
1118        self.display_findings();
1119
1120        // Offer remediation
1121        if !self.findings.is_empty() {
1122            self.offer_remediation()?;
1123        }
1124
1125        Ok(())
1126    }
1127
1128    /// Scan for hidden malware directories in home
1129    fn scan_hidden_directories(&mut self) -> Result<()> {
1130        let home = match dirs::home_dir() {
1131            Some(h) => h,
1132            None => return Ok(()),
1133        };
1134
1135        for dir_name in MALICIOUS_DIRECTORIES {
1136            let dir_path = home.join(dir_name);
1137            if dir_path.exists() && dir_path.is_dir() {
1138                // Check if it has suspicious content
1139                let mut suspicious = false;
1140                if let Ok(entries) = fs::read_dir(&dir_path) {
1141                    for entry in entries.flatten() {
1142                        let name = entry.file_name().to_string_lossy().to_string();
1143                        if name.contains("config")
1144                            || name.contains("cache")
1145                            || name.contains("session")
1146                            || name.ends_with(".json")
1147                            || name.ends_with(".js")
1148                        {
1149                            suspicious = true;
1150                            break;
1151                        }
1152                    }
1153                }
1154
1155                self.findings.push(CleanupFinding {
1156                    category: CleanupCategory::HiddenDirectory,
1157                    path: dir_path,
1158                    description: format!(
1159                        "Hidden directory from known malicious package '{}'{}",
1160                        dir_name.trim_start_matches('.'),
1161                        if suspicious {
1162                            " (contains config/cache files)"
1163                        } else {
1164                            ""
1165                        }
1166                    ),
1167                    risk_level: if suspicious {
1168                        "CRITICAL".to_string()
1169                    } else {
1170                        "HIGH".to_string()
1171                    },
1172                });
1173            }
1174        }
1175
1176        Ok(())
1177    }
1178
1179    /// Scan ~/.claude/ subdirectories for malicious content
1180    fn scan_claude_subdirectories(&mut self) -> Result<()> {
1181        let home = match dirs::home_dir() {
1182            Some(h) => h,
1183            None => return Ok(()),
1184        };
1185
1186        let claude_dir = home.join(".claude");
1187        if !claude_dir.exists() {
1188            return Ok(());
1189        }
1190
1191        for subdir in CLAUDE_SUBDIRS_TO_SCAN {
1192            let subdir_path = claude_dir.join(subdir);
1193            if subdir_path.exists() && subdir_path.is_dir() {
1194                // Check contents for malicious patterns
1195                if let Ok(entries) = fs::read_dir(&subdir_path) {
1196                    for entry in entries.flatten() {
1197                        let entry_name = entry.file_name().to_string_lossy().to_string();
1198                        let entry_path = entry.path();
1199
1200                        // Check if entry name matches malicious packages
1201                        for malicious in MALICIOUS_PACKAGES {
1202                            if entry_name.contains(malicious) {
1203                                self.findings.push(CleanupFinding {
1204                                    category: CleanupCategory::ClaudeSubdirectory,
1205                                    path: entry_path.clone(),
1206                                    description: format!(
1207                                        "~/.claude/{}/{} - matches malicious package '{}'",
1208                                        subdir, entry_name, malicious
1209                                    ),
1210                                    risk_level: "CRITICAL".to_string(),
1211                                });
1212                            }
1213                        }
1214
1215                        // Also check file contents for malicious patterns if it's a file
1216                        if entry_path.is_file() {
1217                            if let Ok(content) = fs::read_to_string(&entry_path) {
1218                                for malicious in MALICIOUS_PACKAGES {
1219                                    if content.contains(malicious) {
1220                                        self.findings.push(CleanupFinding {
1221                                            category: CleanupCategory::ClaudeSubdirectory,
1222                                            path: entry_path.clone(),
1223                                            description: format!(
1224                                                "~/.claude/{}/{} - references malicious package '{}'",
1225                                                subdir, entry_name, malicious
1226                                            ),
1227                                            risk_level: "CRITICAL".to_string(),
1228                                        });
1229                                        break; // Only report once per file
1230                                    }
1231                                }
1232
1233                                // Check for IPFS/IPNS patterns
1234                                if content.contains("ipfs.io")
1235                                    || content.contains("dweb.link")
1236                                    || content.contains("k51qzi5uqu5")
1237                                {
1238                                    self.findings.push(CleanupFinding {
1239                                        category: CleanupCategory::ClaudeSubdirectory,
1240                                        path: entry_path.clone(),
1241                                        description: format!(
1242                                            "~/.claude/{}/{} - contains IPFS/IPNS references (potential C2)",
1243                                            subdir, entry_name
1244                                        ),
1245                                        risk_level: "CRITICAL".to_string(),
1246                                    });
1247                                }
1248                            }
1249                        }
1250                    }
1251                }
1252            }
1253        }
1254
1255        Ok(())
1256    }
1257
1258    /// Scan MCP configurations for malicious servers
1259    fn scan_mcp_configurations(&mut self) -> Result<()> {
1260        // Desktop configs
1261        for (_, config_path) in crate::claude_init::McpInstaller::get_all_target_configs() {
1262            if config_path.exists() {
1263                self.scan_mcp_file(&config_path)?;
1264            }
1265        }
1266
1267        // Project .mcp.json
1268        if let Ok(cwd) = std::env::current_dir() {
1269            let mcp_json = cwd.join(".mcp.json");
1270            if mcp_json.exists() {
1271                self.scan_mcp_file(&mcp_json)?;
1272            }
1273        }
1274
1275        Ok(())
1276    }
1277
1278    /// Scan a single MCP configuration file
1279    fn scan_mcp_file(&mut self, path: &std::path::Path) -> Result<()> {
1280        let content = match fs::read_to_string(path) {
1281            Ok(c) => c,
1282            Err(_) => return Ok(()),
1283        };
1284
1285        let config: Value = match serde_json::from_str(&content) {
1286            Ok(v) => v,
1287            Err(_) => return Ok(()),
1288        };
1289
1290        if let Some(obj) = config.as_object() {
1291            if let Some(servers) = obj.get("mcpServers") {
1292                if let Some(servers_obj) = servers.as_object() {
1293                    for (name, server_config) in servers_obj {
1294                        // Check if server name or config matches malicious patterns
1295                        let config_str = serde_json::to_string(server_config).unwrap_or_default();
1296
1297                        for malicious in MALICIOUS_PACKAGES {
1298                            if name.contains(malicious) || config_str.contains(malicious) {
1299                                self.findings.push(CleanupFinding {
1300                                    category: CleanupCategory::McpServer,
1301                                    path: path.to_path_buf(),
1302                                    description: format!(
1303                                        "MCP server '{}' references malicious package '{}'",
1304                                        name, malicious
1305                                    ),
1306                                    risk_level: "CRITICAL".to_string(),
1307                                });
1308                            }
1309                        }
1310
1311                        // Check for IPFS/IPNS patterns
1312                        if config_str.contains("ipfs.io")
1313                            || config_str.contains("dweb.link")
1314                            || config_str.contains("cloudflare-ipfs.com")
1315                            || config_str.contains("gateway.pinata.cloud")
1316                            || config_str.contains("w3s.link")
1317                            || config_str.contains("4everland.io")
1318                            || config_str.contains("k51qzi5uqu5")
1319                        {
1320                            self.findings.push(CleanupFinding {
1321                                category: CleanupCategory::McpServer,
1322                                path: path.to_path_buf(),
1323                                description: format!(
1324                                    "MCP server '{}' uses IPFS/IPNS (potential C2 channel)",
1325                                    name
1326                                ),
1327                                risk_level: "CRITICAL".to_string(),
1328                            });
1329                        }
1330                    }
1331                }
1332            }
1333        }
1334
1335        Ok(())
1336    }
1337
1338    /// Scan Claude settings for malicious hooks
1339    fn scan_claude_settings(&mut self) -> Result<()> {
1340        let settings_paths = [
1341            dirs::home_dir().map(|h| h.join(".claude/settings.json")),
1342            dirs::home_dir().map(|h| h.join(".claude/.claude/settings.json")),
1343            std::env::current_dir()
1344                .ok()
1345                .map(|c| c.join(".claude/settings.json")),
1346        ];
1347
1348        for path_opt in settings_paths.iter().flatten() {
1349            if path_opt.exists() {
1350                self.scan_settings_file(path_opt)?;
1351            }
1352        }
1353
1354        Ok(())
1355    }
1356
1357    /// Scan a single settings file for malicious hooks
1358    fn scan_settings_file(&mut self, path: &std::path::Path) -> Result<()> {
1359        let content = match fs::read_to_string(path) {
1360            Ok(c) => c,
1361            Err(_) => return Ok(()),
1362        };
1363
1364        let config: Value = match serde_json::from_str(&content) {
1365            Ok(v) => v,
1366            Err(_) => return Ok(()),
1367        };
1368
1369        if let Some(obj) = config.as_object() {
1370            // Check enabledMcpjsonServers (inheritance attack vector)
1371            if obj.contains_key("enabledMcpjsonServers") {
1372                self.findings.push(CleanupFinding {
1373                    category: CleanupCategory::EnabledServer,
1374                    path: path.to_path_buf(),
1375                    description:
1376                        "enabledMcpjsonServers found - allows inherited MCP server execution"
1377                            .to_string(),
1378                    risk_level: "HIGH".to_string(),
1379                });
1380            }
1381
1382            // Check hooks for malicious patterns
1383            if let Some(hooks) = obj.get("hooks") {
1384                if let Some(hooks_obj) = hooks.as_object() {
1385                    for (hook_type, hook_config) in hooks_obj {
1386                        let hook_str = serde_json::to_string(hook_config).unwrap_or_default();
1387
1388                        for malicious in MALICIOUS_PACKAGES {
1389                            if hook_str.contains(malicious) {
1390                                self.findings.push(CleanupFinding {
1391                                    category: CleanupCategory::Hook,
1392                                    path: path.to_path_buf(),
1393                                    description: format!(
1394                                        "'{}' hook references malicious package '{}'",
1395                                        hook_type, malicious
1396                                    ),
1397                                    risk_level: "CRITICAL".to_string(),
1398                                });
1399                            }
1400                        }
1401
1402                        // Check for IPFS/IPNS patterns in hooks
1403                        if hook_str.contains("ipfs.io")
1404                            || hook_str.contains("dweb.link")
1405                            || hook_str.contains("cloudflare-ipfs.com")
1406                            || hook_str.contains("gateway.pinata.cloud")
1407                            || hook_str.contains("w3s.link")
1408                            || hook_str.contains("4everland.io")
1409                            || hook_str.contains("k51qzi5uqu5")
1410                        {
1411                            self.findings.push(CleanupFinding {
1412                                category: CleanupCategory::Hook,
1413                                path: path.to_path_buf(),
1414                                description: format!(
1415                                    "'{}' hook uses IPFS/IPNS gateway (potential remote injection)",
1416                                    hook_type
1417                                ),
1418                                risk_level: "CRITICAL".to_string(),
1419                            });
1420                        }
1421
1422                        // Check for npx with volatile tags
1423                        if hook_str.contains("npx ")
1424                            && (hook_str.contains("@latest")
1425                                || hook_str.contains("@alpha")
1426                                || hook_str.contains("@beta")
1427                                || hook_str.contains("@next")
1428                                || hook_str.contains("@canary"))
1429                        {
1430                            self.findings.push(CleanupFinding {
1431                                category: CleanupCategory::Hook,
1432                                path: path.to_path_buf(),
1433                                description: format!(
1434                                    "'{}' hook uses volatile npm tag (content can change anytime)",
1435                                    hook_type
1436                                ),
1437                                risk_level: "HIGH".to_string(),
1438                            });
1439                        }
1440
1441                        // Check for auto-execution hooks (from security disclosure)
1442                        if (hook_type == "PreToolUse"
1443                            || hook_type == "PostToolUse"
1444                            || hook_type == "SessionStart"
1445                            || hook_type == "UserPromptSubmit")
1446                            && hook_str.contains("npx")
1447                        {
1448                            self.findings.push(CleanupFinding {
1449                                category: CleanupCategory::Hook,
1450                                path: path.to_path_buf(),
1451                                description: format!(
1452                                    "'{}' hook auto-executes npm package on every operation",
1453                                    hook_type
1454                                ),
1455                                risk_level: "HIGH".to_string(),
1456                            });
1457                        }
1458                    }
1459                }
1460            }
1461        }
1462
1463        Ok(())
1464    }
1465
1466    /// Scan parent directories for .mcp.json files with malicious servers
1467    fn scan_parent_mcp_files(&mut self) -> Result<()> {
1468        let mut current = match std::env::current_dir() {
1469            Ok(c) => c,
1470            Err(_) => return Ok(()),
1471        };
1472
1473        // Walk up to root, checking each directory
1474        while let Some(parent) = current.parent() {
1475            let parent = parent.to_path_buf();
1476
1477            let mcp_json = parent.join(".mcp.json");
1478            if mcp_json.exists() {
1479                self.scan_mcp_file(&mcp_json)?;
1480            }
1481
1482            if parent == current {
1483                break;
1484            }
1485            current = parent;
1486        }
1487
1488        Ok(())
1489    }
1490
1491    /// Display all findings
1492    fn display_findings(&self) {
1493        if self.findings.is_empty() {
1494            println!("โœ… No malicious AI integrations detected.\n");
1495            println!("Your system appears clean of known supply chain attack patterns.");
1496            return;
1497        }
1498
1499        println!(
1500            "๐Ÿšจ FINDINGS: {} potential security issues detected\n",
1501            self.findings.len()
1502        );
1503
1504        // Group by category
1505        let mut by_category: std::collections::HashMap<&str, Vec<&CleanupFinding>> =
1506            std::collections::HashMap::new();
1507
1508        for finding in &self.findings {
1509            let cat = match finding.category {
1510                CleanupCategory::HiddenDirectory => "Hidden Directories",
1511                CleanupCategory::ClaudeSubdirectory => "Claude Subdirectories (~/.claude/)",
1512                CleanupCategory::McpServer => "MCP Server Configurations",
1513                CleanupCategory::Hook => "Claude Hooks",
1514                CleanupCategory::EnabledServer => "Enabled Server Inheritance",
1515            };
1516            by_category.entry(cat).or_default().push(finding);
1517        }
1518
1519        for (category, findings) in &by_category {
1520            println!("๐Ÿ“ {} ({} found)", category, findings.len());
1521            println!("{}", "-".repeat(60));
1522
1523            for finding in findings {
1524                let icon = match finding.risk_level.as_str() {
1525                    "CRITICAL" => "๐Ÿ”ด",
1526                    "HIGH" => "๐ŸŸ ",
1527                    _ => "๐ŸŸก",
1528                };
1529                println!(
1530                    "  {} [{}] {}",
1531                    icon, finding.risk_level, finding.description
1532                );
1533                println!("     Path: {}", finding.path.display());
1534            }
1535            println!();
1536        }
1537    }
1538
1539    /// Offer to remediate findings
1540    fn offer_remediation(&mut self) -> Result<()> {
1541        println!("๐Ÿ›ก๏ธ REMEDIATION OPTIONS");
1542        println!("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n");
1543
1544        if !self.yes {
1545            println!("The following actions will be taken:");
1546            println!("  1. Remove hidden malware directories (~/.claude-flow/, etc.)");
1547            println!("  2. Remove malicious files from ~/.claude/ subdirectories");
1548            println!("  3. Remove malicious MCP server entries from configs");
1549            println!("  4. Remove malicious hooks from settings");
1550            println!("  5. Remove enabledMcpjsonServers entries\n");
1551
1552            print!("Proceed with cleanup? [y/N] ");
1553            io::stdout().flush()?;
1554
1555            let mut input = String::new();
1556            io::stdin().read_line(&mut input)?;
1557            let input = input.trim().to_lowercase();
1558
1559            if input != "y" && input != "yes" {
1560                println!("\nCleanup cancelled. No changes made.");
1561                println!("\nTo manually review:");
1562                for finding in &self.findings {
1563                    println!("  - {}", finding.path.display());
1564                }
1565                return Ok(());
1566            }
1567        }
1568
1569        println!("\n๐Ÿงน Performing cleanup...\n");
1570
1571        let mut cleaned = 0;
1572        let mut errors = Vec::new();
1573
1574        // Process each finding
1575        for finding in &self.findings {
1576            match finding.category {
1577                CleanupCategory::HiddenDirectory => match fs::remove_dir_all(&finding.path) {
1578                    Ok(_) => {
1579                        println!("  โœ… Removed directory: {}", finding.path.display());
1580                        cleaned += 1;
1581                    }
1582                    Err(e) => {
1583                        errors.push(format!(
1584                            "Failed to remove {}: {}",
1585                            finding.path.display(),
1586                            e
1587                        ));
1588                    }
1589                },
1590                CleanupCategory::ClaudeSubdirectory => {
1591                    // Remove the file or directory
1592                    let result = if finding.path.is_dir() {
1593                        fs::remove_dir_all(&finding.path)
1594                    } else {
1595                        fs::remove_file(&finding.path)
1596                    };
1597                    match result {
1598                        Ok(_) => {
1599                            println!("  โœ… Removed: {}", finding.path.display());
1600                            cleaned += 1;
1601                        }
1602                        Err(e) => {
1603                            errors.push(format!(
1604                                "Failed to remove {}: {}",
1605                                finding.path.display(),
1606                                e
1607                            ));
1608                        }
1609                    }
1610                }
1611                CleanupCategory::McpServer => {
1612                    match self.remove_mcp_server(&finding.path, &finding.description) {
1613                        Ok(true) => {
1614                            println!("  โœ… Removed MCP server from: {}", finding.path.display());
1615                            cleaned += 1;
1616                        }
1617                        Ok(false) => {} // Already removed or not found
1618                        Err(e) => {
1619                            errors.push(format!(
1620                                "Failed to clean {}: {}",
1621                                finding.path.display(),
1622                                e
1623                            ));
1624                        }
1625                    }
1626                }
1627                CleanupCategory::Hook => match self.remove_malicious_hooks(&finding.path) {
1628                    Ok(count) if count > 0 => {
1629                        println!(
1630                            "  โœ… Removed {} malicious hook(s) from: {}",
1631                            count,
1632                            finding.path.display()
1633                        );
1634                        cleaned += count;
1635                    }
1636                    Ok(_) => {}
1637                    Err(e) => {
1638                        errors.push(format!(
1639                            "Failed to clean hooks in {}: {}",
1640                            finding.path.display(),
1641                            e
1642                        ));
1643                    }
1644                },
1645                CleanupCategory::EnabledServer => {
1646                    match self.remove_enabled_servers(&finding.path) {
1647                        Ok(true) => {
1648                            println!(
1649                                "  โœ… Removed enabledMcpjsonServers from: {}",
1650                                finding.path.display()
1651                            );
1652                            cleaned += 1;
1653                        }
1654                        Ok(false) => {}
1655                        Err(e) => {
1656                            errors.push(format!(
1657                                "Failed to clean {}: {}",
1658                                finding.path.display(),
1659                                e
1660                            ));
1661                        }
1662                    }
1663                }
1664            }
1665        }
1666
1667        // Summary
1668        println!("\n๐Ÿ“‹ CLEANUP SUMMARY");
1669        println!("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
1670        println!("  โœ… Successfully cleaned: {} items", cleaned);
1671
1672        if !errors.is_empty() {
1673            println!("  โŒ Errors encountered: {}", errors.len());
1674            for error in &errors {
1675                println!("     โ€ข {}", error);
1676            }
1677        }
1678
1679        println!("\n๐Ÿ” NEXT STEPS");
1680        println!("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
1681        println!("  1. Restart Claude Desktop / Claude Code to apply changes");
1682        println!("  2. Run 'st --security-scan .' to verify your codebase");
1683        println!("  3. Review ~/.claude/settings.json manually for any missed items");
1684        println!("  4. DO NOT reinstall the flagged npm packages\n");
1685
1686        Ok(())
1687    }
1688
1689    /// Remove an MCP server from a config file
1690    fn remove_mcp_server(&self, path: &std::path::Path, description: &str) -> Result<bool> {
1691        let content = fs::read_to_string(path)?;
1692        let mut config: Value = serde_json::from_str(&content)?;
1693
1694        let mut removed = false;
1695
1696        if let Some(obj) = config.as_object_mut() {
1697            if let Some(servers) = obj.get_mut("mcpServers") {
1698                if let Some(servers_obj) = servers.as_object_mut() {
1699                    // Find and remove the server
1700                    let server_names: Vec<String> = servers_obj.keys().cloned().collect();
1701                    for name in server_names {
1702                        let config_str = servers_obj
1703                            .get(&name)
1704                            .map(|v| serde_json::to_string(v).unwrap_or_default())
1705                            .unwrap_or_default();
1706
1707                        // Check if this is the malicious server
1708                        for malicious in MALICIOUS_PACKAGES {
1709                            if name.contains(malicious) || config_str.contains(malicious) {
1710                                servers_obj.remove(&name);
1711                                removed = true;
1712                            }
1713                        }
1714
1715                        // Also check for IPFS patterns mentioned in description
1716                        if description.contains("IPFS") || description.contains("IPNS") {
1717                            if config_str.contains("ipfs.io")
1718                                || config_str.contains("dweb.link")
1719                                || config_str.contains("k51qzi5uqu5")
1720                            {
1721                                servers_obj.remove(&name);
1722                                removed = true;
1723                            }
1724                        }
1725                    }
1726                }
1727            }
1728        }
1729
1730        if removed {
1731            fs::write(path, serde_json::to_string_pretty(&config)?)?;
1732        }
1733
1734        Ok(removed)
1735    }
1736
1737    /// Remove malicious hooks from a settings file
1738    fn remove_malicious_hooks(&self, path: &std::path::Path) -> Result<usize> {
1739        let content = fs::read_to_string(path)?;
1740        let mut config: Value = serde_json::from_str(&content)?;
1741
1742        let mut removed = 0;
1743
1744        if let Some(obj) = config.as_object_mut() {
1745            if let Some(hooks) = obj.get_mut("hooks") {
1746                if let Some(hooks_obj) = hooks.as_object_mut() {
1747                    for (_hook_type, hook_array) in hooks_obj.iter_mut() {
1748                        if let Some(arr) = hook_array.as_array_mut() {
1749                            let original_len = arr.len();
1750
1751                            arr.retain(|hook| {
1752                                let hook_str = serde_json::to_string(hook).unwrap_or_default();
1753                                !MALICIOUS_PACKAGES.iter().any(|p| hook_str.contains(p))
1754                            });
1755
1756                            removed += original_len - arr.len();
1757                        }
1758                    }
1759                }
1760            }
1761        }
1762
1763        if removed > 0 {
1764            fs::write(path, serde_json::to_string_pretty(&config)?)?;
1765        }
1766
1767        Ok(removed)
1768    }
1769
1770    /// Remove enabledMcpjsonServers from a settings file
1771    fn remove_enabled_servers(&self, path: &std::path::Path) -> Result<bool> {
1772        let content = fs::read_to_string(path)?;
1773        let mut config: Value = serde_json::from_str(&content)?;
1774
1775        let removed = if let Some(obj) = config.as_object_mut() {
1776            obj.remove("enabledMcpjsonServers").is_some()
1777        } else {
1778            false
1779        };
1780
1781        if removed {
1782            fs::write(path, serde_json::to_string_pretty(&config)?)?;
1783        }
1784
1785        Ok(removed)
1786    }
1787}
1788
1789/// Run the security cleanup
1790pub fn run_security_cleanup(yes: bool) -> Result<()> {
1791    let mut cleanup = SecurityCleanup::new(yes);
1792    cleanup.run()
1793}