1use crate::claude_init::{ClaudeInit, McpInstaller};
11use anyhow::{Context, Result};
12
13#[derive(Debug, Clone, Copy, Default, PartialEq)]
15pub enum InstallScope {
16 #[default]
18 Project,
19 User,
21}
22
23#[derive(Debug, Clone, Copy, Default, PartialEq)]
25pub enum AiTarget {
26 #[default]
28 Claude,
29 Chatgpt,
31 Gemini,
33 Universal,
35}
36use serde_json::{json, Value};
37use std::fs;
38use std::io::{self, Write};
39use std::path::PathBuf;
40
41pub struct AiInstaller {
43 scope: InstallScope,
45 target: AiTarget,
47 interactive: bool,
49 project_path: PathBuf,
51}
52
53#[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, }
72 }
73}
74
75impl AiInstaller {
76 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 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 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 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 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 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 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 fn run_non_interactive(&self) -> Result<()> {
216 let options = InstallOptions::default();
217 self.execute_install(&options)
218 }
219
220 fn discover_options(&self) -> InstallOptions {
222 let mut options = InstallOptions::default();
223
224 match self.scope {
225 InstallScope::Project => {
226 options.install_claude_md = true;
228 options.create_settings = true;
229 options.install_hooks = true;
230
231 options.install_mcp = matches!(self.target, AiTarget::Claude | AiTarget::Universal | AiTarget::Gemini);
233 }
234 InstallScope::User => {
235 options.install_mcp = matches!(self.target, AiTarget::Claude | AiTarget::Universal | AiTarget::Gemini);
237 options.install_hooks = true;
238 options.install_claude_md = false; options.create_settings = true;
240 }
241 }
242
243 options
244 }
245
246 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 fn execute_install(&self, options: &InstallOptions) -> Result<()> {
272 let mut installed = Vec::new();
273 let mut errors = Vec::new();
274
275 if options.cleanup_foreign {
278 match self.cleanup_foreign_integrations() {
279 Ok(count) if count > 0 => installed.push("Foreign integrations cleaned"),
280 Ok(_) => {} Err(e) => errors.push(format!("Cleanup: {}", e)),
282 }
283 }
284
285 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 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 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 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 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 fn install_mcp(&self) -> Result<()> {
344 match self.target {
345 AiTarget::Claude | AiTarget::Universal | AiTarget::Gemini => {
346 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 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 fn ensure_project_mcp_json(&self) -> Result<()> {
372 let mcp_json_path = self.project_path.join(".mcp.json");
373
374 let st_stdio_config = json!({
376 "type": "stdio",
377 "command": "st",
378 "args": ["--mcp"],
379 "env": {}
380 });
381
382 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 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 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 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 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 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 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 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 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 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 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 fn cleanup_foreign_integrations(&self) -> Result<usize> {
564 let mut cleaned = 0;
565
566 let foreign_patterns = [
569 "claude-flow",
571 "agentic-flow",
572 "ruv-swarm",
573 "flow-nexus",
574 "hive-mind",
575 "superdisco",
576 "agent-booster",
577 "ipfs.io",
579 "dweb.link",
580 "cloudflare-ipfs.com",
581 "gateway.pinata.cloud",
582 "w3s.link",
583 "4everland.io",
584 "k51qzi5uqu5",
586 "@alpha",
588 "@beta",
589 "@latest",
590 "@next",
591 "@canary",
592 "npx ", "swarm",
595 "queen",
596 "worker",
597 "registry",
599 "BOOTSTRAP_REGISTRIES",
600 "ipnsName",
601 "registrySignature",
602 ];
603
604 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 cleaned += self.clean_parent_mcp_json(&mcp_json, &foreign_patterns)?;
612 }
613 if let Some(parent) = current.parent() {
614 if parent == current {
615 break; }
617 current = parent.to_path_buf();
618 } else {
619 break;
620 }
621 }
622
623 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 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 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 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 if content.trim().is_empty() {
662 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 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 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 if cleaned > 0 {
705 fs::write(path, serde_json::to_string_pretty(&config)?)?;
706 }
707
708 Ok(cleaned)
709 }
710
711 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 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 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 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 if cleaned > 0 {
761 fs::write(path, serde_json::to_string_pretty(&config)?)?;
762 }
763
764 Ok(cleaned)
765 }
766
767 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 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 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
810pub 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#[derive(Debug)]
822pub struct ConfigStatus {
823 pub name: String,
824 pub enabled: bool,
825 pub path: Option<PathBuf>,
826 pub details: String,
827}
828
829pub struct ConfigManager {
831 scope: InstallScope,
832}
833
834impl ConfigManager {
835 pub fn new(scope: InstallScope) -> Self {
836 Self { scope }
837 }
838
839 pub fn list_configs(&self) -> Vec<ConfigStatus> {
841 let mut configs = Vec::new();
842
843 configs.push(self.check_mcp_status());
845
846 configs.push(self.check_hooks_status());
848
849 configs.push(self.check_settings_status());
851
852 if matches!(self.scope, InstallScope::Project) {
854 configs.push(self.check_claude_md_status());
855 }
856
857 configs
858 }
859
860 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
1005pub fn show_ai_config_status(scope: InstallScope) {
1007 let manager = ConfigManager::new(scope);
1008 manager.display_configs();
1009}
1010
1011const MALICIOUS_PACKAGES: &[&str] = &[
1017 "claude-flow",
1019 "agentic-flow",
1020 "superdisco",
1021 "agent-booster",
1022 "ruv-swarm",
1023 "flow-nexus",
1024 "hive-mind",
1025];
1026
1027const MALICIOUS_DIRECTORIES: &[&str] = &[
1030 ".claude-flow", ".agentic-flow", ".superdisco", ".agent-booster", ".flow-nexus", ".ruv-swarm", ".hive-mind", ".ipfs-registry", ".pattern-cache", ".seraphine", ];
1041
1042const CLAUDE_SUBDIRS_TO_SCAN: &[&str] = &[
1044 "skills",
1045 "commands",
1046 "hooks",
1047 "plugins",
1048 "extensions",
1049 "tools",
1050];
1051
1052#[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
1082pub 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 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 self.scan_hidden_directories()?;
1104
1105 self.scan_claude_subdirectories()?;
1107
1108 self.scan_mcp_configurations()?;
1110
1111 self.scan_claude_settings()?;
1113
1114 self.scan_parent_mcp_files()?;
1116
1117 self.display_findings();
1119
1120 if !self.findings.is_empty() {
1122 self.offer_remediation()?;
1123 }
1124
1125 Ok(())
1126 }
1127
1128 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 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 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 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 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 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; }
1231 }
1232
1233 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 fn scan_mcp_configurations(&mut self) -> Result<()> {
1260 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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) => {} 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 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 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 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 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 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 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 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
1789pub fn run_security_cleanup(yes: bool) -> Result<()> {
1791 let mut cleanup = SecurityCleanup::new(yes);
1792 cleanup.run()
1793}