claude-mpm 5.4.73__py3-none-any.whl → 5.4.75__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -656,7 +656,7 @@ class ConfigureCommand(BaseCommand):
656
656
  self.behavior_manager.manage_behaviors()
657
657
 
658
658
  def _manage_skills(self) -> None:
659
- """Skills management interface."""
659
+ """Skills management interface with questionary checkbox selection."""
660
660
  from ...cli.interactive.skills_wizard import SkillsWizard
661
661
  from ...skills.skill_manager import get_manager
662
662
 
@@ -667,22 +667,22 @@ class ConfigureCommand(BaseCommand):
667
667
  self.console.clear()
668
668
  self._display_header()
669
669
 
670
- self.console.print("\n[bold]Skills Management Options:[/bold]\n")
671
- self.console.print(" [1] View Available Skills")
672
- self.console.print(" [2] Configure Skills for Agents")
673
- self.console.print(" [3] View Current Skill Mappings")
674
- self.console.print(" [4] Auto-Link Skills to Agents")
675
- self.console.print(" [b] Back to Main Menu")
670
+ self.console.print("\n[bold]Skills Management[/bold]")
671
+
672
+ # Show action options
673
+ self.console.print("\n[bold]Actions:[/bold]")
674
+ self.console.print(" [1] Install/Uninstall skills")
675
+ self.console.print(" [2] Configure skills for agents")
676
+ self.console.print(" [3] View current skill mappings")
677
+ self.console.print(" [4] Auto-link skills to agents")
678
+ self.console.print(" [b] Back to main menu")
676
679
  self.console.print()
677
680
 
678
681
  choice = Prompt.ask("[bold blue]Select an option[/bold blue]", default="b")
679
682
 
680
683
  if choice == "1":
681
- # View available skills
682
- self.console.clear()
683
- self._display_header()
684
- wizard.list_available_skills()
685
- Prompt.ask("\nPress Enter to continue")
684
+ # Install/Uninstall skills with category-based selection
685
+ self._manage_skill_installation()
686
686
 
687
687
  elif choice == "2":
688
688
  # Configure skills interactively
@@ -805,6 +805,391 @@ class ConfigureCommand(BaseCommand):
805
805
  self.console.print("[red]Invalid choice. Please try again.[/red]")
806
806
  Prompt.ask("\nPress Enter to continue")
807
807
 
808
+ def _manage_skill_installation(self) -> None:
809
+ """Manage skill installation with category-based questionary checkbox selection."""
810
+ import questionary
811
+
812
+ # Get all skills
813
+ all_skills = self._get_all_skills_from_git()
814
+ if not all_skills:
815
+ self.console.print(
816
+ "[yellow]No skills available. Try syncing skills first.[/yellow]"
817
+ )
818
+ Prompt.ask("\nPress Enter to continue")
819
+ return
820
+
821
+ # Get deployed skills
822
+ deployed = self._get_deployed_skill_ids()
823
+
824
+ # Group by category
825
+ grouped = {}
826
+ for skill in all_skills:
827
+ # Try to get category from tags or use toolchain
828
+ category = None
829
+ tags = skill.get("tags", [])
830
+
831
+ # Look for category tag
832
+ for tag in tags:
833
+ if tag in [
834
+ "universal",
835
+ "python",
836
+ "typescript",
837
+ "javascript",
838
+ "go",
839
+ "rust",
840
+ ]:
841
+ category = tag
842
+ break
843
+
844
+ # Fallback to toolchain or universal
845
+ if not category:
846
+ category = skill.get("toolchain", "universal")
847
+
848
+ if category not in grouped:
849
+ grouped[category] = []
850
+ grouped[category].append(skill)
851
+
852
+ # Category icons
853
+ icons = {
854
+ "universal": "🌐",
855
+ "python": "🐍",
856
+ "typescript": "📘",
857
+ "javascript": "📒",
858
+ "go": "🔷",
859
+ "rust": "⚙️",
860
+ }
861
+
862
+ # Sort categories: universal first, then alphabetically
863
+ categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
864
+
865
+ while True:
866
+ # Show category selection first
867
+ self.console.clear()
868
+ self._display_header()
869
+ self.console.print("\n[bold cyan]Skills Management[/bold cyan]")
870
+ self.console.print(
871
+ f"[dim]{len(all_skills)} skills available, {len(deployed)} installed[/dim]\n"
872
+ )
873
+
874
+ cat_choices = [
875
+ Choice(
876
+ title=f"{icons.get(cat, '📦')} {cat.title()} ({len(grouped[cat])} skills)",
877
+ value=cat,
878
+ )
879
+ for cat in categories
880
+ ]
881
+ cat_choices.append(Choice(title="← Back to main menu", value="back"))
882
+
883
+ selected_cat = questionary.select(
884
+ "Select a category:", choices=cat_choices, style=self.QUESTIONARY_STYLE
885
+ ).ask()
886
+
887
+ if selected_cat is None or selected_cat == "back":
888
+ return
889
+
890
+ # Show skills in category with checkbox selection
891
+ category_skills = grouped[selected_cat]
892
+
893
+ # Build choices with current installation status
894
+ skill_choices = []
895
+ for skill in sorted(category_skills, key=lambda x: x.get("name", "")):
896
+ skill_id = skill.get("name", skill.get("skill_id", "unknown"))
897
+ deploy_name = skill.get("deployment_name", skill_id)
898
+ description = skill.get("description", "")[:50]
899
+
900
+ # Check if installed
901
+ is_installed = deploy_name in deployed or skill_id in deployed
902
+
903
+ skill_choices.append(
904
+ Choice(
905
+ title=f"{skill_id} - {description}",
906
+ value=skill_id,
907
+ checked=is_installed,
908
+ )
909
+ )
910
+
911
+ self.console.clear()
912
+ self._display_header()
913
+ self.console.print(
914
+ f"\n{icons.get(selected_cat, '📦')} [bold]{selected_cat.title()}[/bold]"
915
+ )
916
+ self.console.print("[dim]Use spacebar to toggle, enter to confirm[/dim]\n")
917
+
918
+ selected = questionary.checkbox(
919
+ "Select skills to install:",
920
+ choices=skill_choices,
921
+ style=self.QUESTIONARY_STYLE,
922
+ ).ask()
923
+
924
+ if selected is None:
925
+ continue # User cancelled, go back to category selection
926
+
927
+ # Process changes
928
+ selected_set = set(selected)
929
+ current_in_cat = set()
930
+
931
+ # Find currently installed skills in this category
932
+ for skill in category_skills:
933
+ skill_id = skill.get("name", skill.get("skill_id", "unknown"))
934
+ deploy_name = skill.get("deployment_name", skill_id)
935
+ if deploy_name in deployed or skill_id in deployed:
936
+ current_in_cat.add(skill_id)
937
+
938
+ # Install newly selected
939
+ to_install = selected_set - current_in_cat
940
+ for skill_id in to_install:
941
+ skill = next(
942
+ (
943
+ s
944
+ for s in category_skills
945
+ if s.get("name") == skill_id or s.get("skill_id") == skill_id
946
+ ),
947
+ None,
948
+ )
949
+ if skill:
950
+ self._install_skill_from_dict(skill)
951
+ self.console.print(f"[green]✓ Installed {skill_id}[/green]")
952
+
953
+ # Uninstall deselected
954
+ to_uninstall = current_in_cat - selected_set
955
+ for skill_id in to_uninstall:
956
+ # Find the skill to get deployment_name
957
+ skill = next(
958
+ (
959
+ s
960
+ for s in category_skills
961
+ if s.get("name") == skill_id or s.get("skill_id") == skill_id
962
+ ),
963
+ None,
964
+ )
965
+ if skill:
966
+ deploy_name = skill.get("deployment_name", skill_id)
967
+ # Use the name that's actually in deployed set
968
+ name_to_uninstall = (
969
+ deploy_name if deploy_name in deployed else skill_id
970
+ )
971
+ self._uninstall_skill_by_name(name_to_uninstall)
972
+ self.console.print(f"[yellow]✗ Uninstalled {skill_id}[/yellow]")
973
+
974
+ # Update deployed set for next iteration
975
+ deployed = self._get_deployed_skill_ids()
976
+
977
+ # Show completion message
978
+ if to_install or to_uninstall:
979
+ Prompt.ask("\nPress Enter to continue")
980
+
981
+ def _get_all_skills_from_git(self) -> list:
982
+ """Get all skills from Git-based skill manager.
983
+
984
+ Returns:
985
+ List of skill dicts with full metadata from GitSkillSourceManager.
986
+ """
987
+ from ...config.skill_sources import SkillSourceConfiguration
988
+ from ...services.skills.git_skill_source_manager import GitSkillSourceManager
989
+
990
+ try:
991
+ config = SkillSourceConfiguration()
992
+ manager = GitSkillSourceManager(config)
993
+ return manager.get_all_skills()
994
+ except Exception as e:
995
+ self.console.print(
996
+ f"[yellow]Warning: Could not load Git skills: {e}[/yellow]"
997
+ )
998
+ return []
999
+
1000
+ def _display_skills_table_grouped(self) -> None:
1001
+ """Display skills in a table grouped by category, like agents."""
1002
+ from rich import box
1003
+ from rich.table import Table
1004
+
1005
+ # Get all skills from Git manager
1006
+ all_skills = self._get_all_skills_from_git()
1007
+ deployed_ids = self._get_deployed_skill_ids()
1008
+
1009
+ if not all_skills:
1010
+ self.console.print(
1011
+ "[yellow]No skills available. Try syncing skills first.[/yellow]"
1012
+ )
1013
+ return
1014
+
1015
+ # Group skills by category/toolchain
1016
+ grouped = {}
1017
+ for skill in all_skills:
1018
+ # Try to get category from tags or use toolchain
1019
+ category = None
1020
+ tags = skill.get("tags", [])
1021
+
1022
+ # Look for category tag
1023
+ for tag in tags:
1024
+ if tag in [
1025
+ "universal",
1026
+ "python",
1027
+ "typescript",
1028
+ "javascript",
1029
+ "go",
1030
+ "rust",
1031
+ ]:
1032
+ category = tag
1033
+ break
1034
+
1035
+ # Fallback to toolchain or universal
1036
+ if not category:
1037
+ category = skill.get("toolchain", "universal")
1038
+
1039
+ if category not in grouped:
1040
+ grouped[category] = []
1041
+ grouped[category].append(skill)
1042
+
1043
+ # Sort categories: universal first, then alphabetically
1044
+ categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
1045
+
1046
+ # Track global skill number across all categories
1047
+ skill_counter = 0
1048
+
1049
+ for category in categories:
1050
+ category_skills = grouped[category]
1051
+
1052
+ # Category header with icon
1053
+ icons = {
1054
+ "universal": "🌐",
1055
+ "python": "🐍",
1056
+ "typescript": "📘",
1057
+ "javascript": "📒",
1058
+ "go": "🔷",
1059
+ "rust": "⚙️",
1060
+ }
1061
+ icon = icons.get(category, "📦")
1062
+ self.console.print(
1063
+ f"\n{icon} [bold cyan]{category.title()}[/bold cyan] ({len(category_skills)} skills)"
1064
+ )
1065
+
1066
+ # Create table for this category
1067
+ table = Table(show_header=True, header_style="bold", box=box.SIMPLE)
1068
+ table.add_column("#", style="dim", width=4)
1069
+ table.add_column("Skill ID", style="cyan", width=35)
1070
+ table.add_column("Description", style="white", width=45)
1071
+ table.add_column("Status", style="green", width=12)
1072
+
1073
+ for skill in sorted(category_skills, key=lambda x: x.get("name", "")):
1074
+ skill_counter += 1
1075
+ skill_id = skill.get("name", skill.get("skill_id", "unknown"))
1076
+ # Use deployment_name for matching if available
1077
+ deploy_name = skill.get("deployment_name", skill_id)
1078
+ description = skill.get("description", "")[:45]
1079
+
1080
+ # Check if installed - handle both deployment_name and skill_id
1081
+ is_installed = deploy_name in deployed_ids or skill_id in deployed_ids
1082
+ status = "[green]✓ Installed[/green]" if is_installed else "Available"
1083
+
1084
+ table.add_row(str(skill_counter), skill_id, description, status)
1085
+
1086
+ self.console.print(table)
1087
+
1088
+ # Summary
1089
+ total = len(all_skills)
1090
+ installed = sum(
1091
+ 1
1092
+ for s in all_skills
1093
+ if s.get("deployment_name", s.get("name", "")) in deployed_ids
1094
+ or s.get("name", "") in deployed_ids
1095
+ )
1096
+ self.console.print(
1097
+ f"\n[dim]Showing {total} skills ({installed} installed)[/dim]"
1098
+ )
1099
+
1100
+ def _get_deployed_skill_ids(self) -> set:
1101
+ """Get set of deployed skill IDs from .claude/skills/ directory.
1102
+
1103
+ Returns:
1104
+ Set of skill directory names and common variations for matching.
1105
+ """
1106
+ from pathlib import Path
1107
+
1108
+ skills_dir = Path.cwd() / ".claude" / "skills"
1109
+ if not skills_dir.exists():
1110
+ return set()
1111
+
1112
+ # Each deployed skill is a directory in .claude/skills/
1113
+ deployed_ids = set()
1114
+ for skill_dir in skills_dir.iterdir():
1115
+ if skill_dir.is_dir() and not skill_dir.name.startswith("."):
1116
+ # Add both the directory name and common variations
1117
+ deployed_ids.add(skill_dir.name)
1118
+ # Also add without prefix for matching (e.g., universal-testing -> testing)
1119
+ if skill_dir.name.startswith("universal-"):
1120
+ deployed_ids.add(skill_dir.name.replace("universal-", "", 1))
1121
+
1122
+ return deployed_ids
1123
+
1124
+ def _install_skill(self, skill) -> None:
1125
+ """Install a skill to .claude/skills/ directory."""
1126
+ import shutil
1127
+ from pathlib import Path
1128
+
1129
+ # Target directory
1130
+ target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
1131
+ target_dir.mkdir(parents=True, exist_ok=True)
1132
+
1133
+ # Copy skill file(s)
1134
+ if skill.path.is_file():
1135
+ # Single file skill - copy to skill.md in target directory
1136
+ shutil.copy2(skill.path, target_dir / "skill.md")
1137
+ elif skill.path.is_dir():
1138
+ # Directory-based skill - copy all contents
1139
+ for item in skill.path.iterdir():
1140
+ if item.is_file():
1141
+ shutil.copy2(item, target_dir / item.name)
1142
+ elif item.is_dir():
1143
+ shutil.copytree(item, target_dir / item.name, dirs_exist_ok=True)
1144
+
1145
+ def _uninstall_skill(self, skill) -> None:
1146
+ """Uninstall a skill from .claude/skills/ directory."""
1147
+ import shutil
1148
+ from pathlib import Path
1149
+
1150
+ target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
1151
+ if target_dir.exists():
1152
+ shutil.rmtree(target_dir)
1153
+
1154
+ def _install_skill_from_dict(self, skill_dict: dict) -> None:
1155
+ """Install a skill from Git skill dict to .claude/skills/ directory.
1156
+
1157
+ Args:
1158
+ skill_dict: Skill metadata dict from GitSkillSourceManager.get_all_skills()
1159
+ """
1160
+ from pathlib import Path
1161
+
1162
+ skill_id = skill_dict.get("name", skill_dict.get("skill_id", "unknown"))
1163
+ content = skill_dict.get("content", "")
1164
+
1165
+ if not content:
1166
+ self.console.print(
1167
+ f"[yellow]Warning: Skill '{skill_id}' has no content[/yellow]"
1168
+ )
1169
+ return
1170
+
1171
+ # Target directory using deployment_name if available
1172
+ deploy_name = skill_dict.get("deployment_name", skill_id)
1173
+ target_dir = Path.cwd() / ".claude" / "skills" / deploy_name
1174
+ target_dir.mkdir(parents=True, exist_ok=True)
1175
+
1176
+ # Write skill content to skill.md
1177
+ skill_file = target_dir / "skill.md"
1178
+ skill_file.write_text(content, encoding="utf-8")
1179
+
1180
+ def _uninstall_skill_by_name(self, skill_name: str) -> None:
1181
+ """Uninstall a skill by name from .claude/skills/ directory.
1182
+
1183
+ Args:
1184
+ skill_name: Name of skill directory to remove
1185
+ """
1186
+ import shutil
1187
+ from pathlib import Path
1188
+
1189
+ target_dir = Path.cwd() / ".claude" / "skills" / skill_name
1190
+ if target_dir.exists():
1191
+ shutil.rmtree(target_dir)
1192
+
808
1193
  def _display_behavior_files(self) -> None:
809
1194
  """Display current behavior files."""
810
1195
  self.behavior_manager.display_behavior_files()
@@ -1437,18 +1822,20 @@ class ConfigureCommand(BaseCommand):
1437
1822
 
1438
1823
  # Add inline control: Select/Deselect all from this collection
1439
1824
  if all_selected:
1825
+ deselect_value = f"__DESELECT_ALL_{collection_id}__"
1440
1826
  choices.append(
1441
1827
  Choice(
1442
- f" [Deselect all from {collection_id}]",
1443
- value=f"__DESELECT_ALL_{collection_id}__",
1828
+ f" [Deselect all from {collection_id}]", # nosec B608
1829
+ value=deselect_value,
1444
1830
  checked=False,
1445
1831
  )
1446
1832
  )
1447
1833
  else:
1834
+ select_value = f"__SELECT_ALL_{collection_id}__"
1448
1835
  choices.append(
1449
1836
  Choice(
1450
- f" [Select all from {collection_id}]",
1451
- value=f"__SELECT_ALL_{collection_id}__",
1837
+ f" [Select all from {collection_id}]", # nosec B608
1838
+ value=select_value,
1452
1839
  checked=False,
1453
1840
  )
1454
1841
  )
@@ -2329,7 +2716,8 @@ class ConfigureCommand(BaseCommand):
2329
2716
  f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
2330
2717
  )
2331
2718
  self.console.print()
2332
- except Exception:
2719
+ except Exception: # nosec B110 - Suppress broad except for failed safety check
2720
+ # Silent failure on safety check - non-critical feature
2333
2721
  pass
2334
2722
 
2335
2723
  # Build mapping: agent_id -> AgentConfig
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 5.4.73
3
+ Version: 5.4.75
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team
@@ -60,7 +60,7 @@ claude_mpm/cli/commands/auto_configure.py,sha256=0Suzil6O0SBNeHUCwHOkt2q7gfuXRTy
60
60
  claude_mpm/cli/commands/cleanup.py,sha256=RQikOGLuLFWXzjeoHArdr5FA4Pf7tSK9w2NXL4vCrok,19769
61
61
  claude_mpm/cli/commands/cleanup_orphaned_agents.py,sha256=JR8crvgrz7Sa6d-SI-gKywok5S9rwc_DzDVk_h85sVs,4467
62
62
  claude_mpm/cli/commands/config.py,sha256=2M9VUPYcQkBUCIyyB-v1qTL3xYvao9YI2l_JGBUDauA,23374
63
- claude_mpm/cli/commands/configure.py,sha256=IrmAtzR4jnTt0YYWYd4fFEOzMVVxB_NU4Xzg2cbxKtk,113160
63
+ claude_mpm/cli/commands/configure.py,sha256=yav4ipRbZ5tc1tjBHkU8Pf6Ls4FtGirqZmN94H0WPic,127811
64
64
  claude_mpm/cli/commands/configure_agent_display.py,sha256=oSvUhR861o_Pyqmop4ACAQNjwL02-Rf6TMqFvmQNh24,10575
65
65
  claude_mpm/cli/commands/configure_behavior_manager.py,sha256=_tfpcKW0KgMGO52q6IHFXL3W5xwjC8-q2_KpIvHVuoI,6827
66
66
  claude_mpm/cli/commands/configure_hook_manager.py,sha256=1X5brU6cgKRRF-2lQYA0aiKD7ZjTClqNHUSWuayktEw,9205
@@ -993,10 +993,10 @@ claude_mpm/utils/subprocess_utils.py,sha256=D0izRT8anjiUb_JG72zlJR_JAw1cDkb7kalN
993
993
  claude_mpm/validation/__init__.py,sha256=YZhwE3mhit-lslvRLuwfX82xJ_k4haZeKmh4IWaVwtk,156
994
994
  claude_mpm/validation/agent_validator.py,sha256=GprtAvu80VyMXcKGsK_VhYiXWA6BjKHv7O6HKx0AB9w,20917
995
995
  claude_mpm/validation/frontmatter_validator.py,sha256=YpJlYNNYcV8u6hIOi3_jaRsDnzhbcQpjCBE6eyBKaFY,7076
996
- claude_mpm-5.4.73.dist-info/licenses/LICENSE,sha256=ca3y_Rk4aPrbF6f62z8Ht5MJM9OAvbGlHvEDcj9vUQ4,3867
997
- claude_mpm-5.4.73.dist-info/licenses/LICENSE-FAQ.md,sha256=TxfEkXVCK98RzDOer09puc7JVCP_q_bN4dHtZKHCMcM,5104
998
- claude_mpm-5.4.73.dist-info/METADATA,sha256=ssUdTTD0LjoFtCfQfcTF4kmOZYeJ6zc5MD1hGZygw3c,38503
999
- claude_mpm-5.4.73.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1000
- claude_mpm-5.4.73.dist-info/entry_points.txt,sha256=n-Uk4vwHPpuvu-g_I7-GHORzTnN_m6iyOsoLveKKD0E,228
1001
- claude_mpm-5.4.73.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
1002
- claude_mpm-5.4.73.dist-info/RECORD,,
996
+ claude_mpm-5.4.75.dist-info/licenses/LICENSE,sha256=ca3y_Rk4aPrbF6f62z8Ht5MJM9OAvbGlHvEDcj9vUQ4,3867
997
+ claude_mpm-5.4.75.dist-info/licenses/LICENSE-FAQ.md,sha256=TxfEkXVCK98RzDOer09puc7JVCP_q_bN4dHtZKHCMcM,5104
998
+ claude_mpm-5.4.75.dist-info/METADATA,sha256=64ZleLArD4nY_HFCsrqzXn1K2RzAscpaNflWxTNRWOg,38503
999
+ claude_mpm-5.4.75.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1000
+ claude_mpm-5.4.75.dist-info/entry_points.txt,sha256=n-Uk4vwHPpuvu-g_I7-GHORzTnN_m6iyOsoLveKKD0E,228
1001
+ claude_mpm-5.4.75.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
1002
+ claude_mpm-5.4.75.dist-info/RECORD,,