claude-mpm 5.4.74__py3-none-any.whl → 5.4.81__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/configure.py +549 -115
- claude_mpm/cli/startup.py +7 -4
- claude_mpm/core/claude_runner.py +2 -2
- claude_mpm/core/interactive_session.py +7 -7
- claude_mpm/core/output_style_manager.py +7 -7
- claude_mpm/core/unified_config.py +16 -0
- claude_mpm/core/unified_paths.py +30 -13
- claude_mpm/services/agents/deployment/deployment_reconciler.py +39 -1
- claude_mpm/services/skills/git_skill_source_manager.py +5 -1
- claude_mpm/services/skills/selective_skill_deployer.py +0 -10
- {claude_mpm-5.4.74.dist-info → claude_mpm-5.4.81.dist-info}/METADATA +1 -1
- {claude_mpm-5.4.74.dist-info → claude_mpm-5.4.81.dist-info}/RECORD +18 -42
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- {claude_mpm-5.4.74.dist-info → claude_mpm-5.4.81.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.74.dist-info → claude_mpm-5.4.81.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.74.dist-info → claude_mpm-5.4.81.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.74.dist-info → claude_mpm-5.4.81.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.74.dist-info → claude_mpm-5.4.81.dist-info}/top_level.txt +0 -0
|
@@ -26,6 +26,7 @@ from rich.prompt import Confirm, Prompt
|
|
|
26
26
|
from rich.text import Text
|
|
27
27
|
|
|
28
28
|
from ...core.config import Config
|
|
29
|
+
from ...core.unified_config import UnifiedConfig
|
|
29
30
|
from ...services.agents.agent_recommendation_service import AgentRecommendationService
|
|
30
31
|
from ...services.version_service import VersionService
|
|
31
32
|
from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
|
|
@@ -79,6 +80,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
79
80
|
self._template_editor = None # Lazy-initialized
|
|
80
81
|
self._startup_manager = None # Lazy-initialized
|
|
81
82
|
self._recommendation_service = None # Lazy-initialized
|
|
83
|
+
self._unified_config = None # Lazy-initialized
|
|
82
84
|
|
|
83
85
|
def validate_args(self, args) -> Optional[str]:
|
|
84
86
|
"""Validate command arguments."""
|
|
@@ -162,6 +164,18 @@ class ConfigureCommand(BaseCommand):
|
|
|
162
164
|
self._recommendation_service = AgentRecommendationService()
|
|
163
165
|
return self._recommendation_service
|
|
164
166
|
|
|
167
|
+
@property
|
|
168
|
+
def unified_config(self) -> UnifiedConfig:
|
|
169
|
+
"""Lazy-initialize unified config."""
|
|
170
|
+
if self._unified_config is None:
|
|
171
|
+
try:
|
|
172
|
+
self._unified_config = UnifiedConfig()
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self.logger.warning(f"Failed to load unified config: {e}")
|
|
175
|
+
# Fallback to default config
|
|
176
|
+
self._unified_config = UnifiedConfig()
|
|
177
|
+
return self._unified_config
|
|
178
|
+
|
|
165
179
|
def run(self, args) -> CommandResult:
|
|
166
180
|
"""Execute the configure command."""
|
|
167
181
|
# Set configuration scope
|
|
@@ -656,25 +670,22 @@ class ConfigureCommand(BaseCommand):
|
|
|
656
670
|
self.behavior_manager.manage_behaviors()
|
|
657
671
|
|
|
658
672
|
def _manage_skills(self) -> None:
|
|
659
|
-
"""Skills management interface with
|
|
673
|
+
"""Skills management interface with questionary checkbox selection."""
|
|
660
674
|
from ...cli.interactive.skills_wizard import SkillsWizard
|
|
661
|
-
from ...skills.registry import get_registry
|
|
662
675
|
from ...skills.skill_manager import get_manager
|
|
663
676
|
|
|
664
677
|
wizard = SkillsWizard()
|
|
665
678
|
manager = get_manager()
|
|
666
|
-
registry = get_registry()
|
|
667
679
|
|
|
668
680
|
while True:
|
|
669
681
|
self.console.clear()
|
|
670
682
|
self._display_header()
|
|
671
683
|
|
|
672
|
-
|
|
673
|
-
self._display_skills_table(registry)
|
|
684
|
+
self.console.print("\n[bold]Skills Management[/bold]")
|
|
674
685
|
|
|
675
686
|
# Show action options
|
|
676
687
|
self.console.print("\n[bold]Actions:[/bold]")
|
|
677
|
-
self.console.print(" [1]
|
|
688
|
+
self.console.print(" [1] Install/Uninstall skills")
|
|
678
689
|
self.console.print(" [2] Configure skills for agents")
|
|
679
690
|
self.console.print(" [3] View current skill mappings")
|
|
680
691
|
self.console.print(" [4] Auto-link skills to agents")
|
|
@@ -684,57 +695,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
684
695
|
choice = Prompt.ask("[bold blue]Select an option[/bold blue]", default="b")
|
|
685
696
|
|
|
686
697
|
if choice == "1":
|
|
687
|
-
#
|
|
688
|
-
self.
|
|
689
|
-
self._display_header()
|
|
690
|
-
self._display_skills_table(registry)
|
|
691
|
-
|
|
692
|
-
skill_num = Prompt.ask(
|
|
693
|
-
"\n[bold blue]Enter skill number to toggle (or 'b' to go back)[/bold blue]",
|
|
694
|
-
default="b",
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
if skill_num == "b":
|
|
698
|
-
continue
|
|
699
|
-
|
|
700
|
-
try:
|
|
701
|
-
skill_idx = int(skill_num) - 1
|
|
702
|
-
all_skills = self._get_all_skills_sorted(registry)
|
|
703
|
-
|
|
704
|
-
if 0 <= skill_idx < len(all_skills):
|
|
705
|
-
skill = all_skills[skill_idx]
|
|
706
|
-
deployed_ids = self._get_deployed_skill_ids()
|
|
707
|
-
|
|
708
|
-
if skill.skill_id in deployed_ids:
|
|
709
|
-
# Uninstall
|
|
710
|
-
confirm = Confirm.ask(
|
|
711
|
-
f"\n[yellow]Uninstall skill '{skill.name}'?[/yellow]",
|
|
712
|
-
default=False,
|
|
713
|
-
)
|
|
714
|
-
if confirm:
|
|
715
|
-
self._uninstall_skill(skill)
|
|
716
|
-
self.console.print(
|
|
717
|
-
f"\n[green]✓ Skill '{skill.name}' uninstalled[/green]"
|
|
718
|
-
)
|
|
719
|
-
else:
|
|
720
|
-
# Install
|
|
721
|
-
confirm = Confirm.ask(
|
|
722
|
-
f"\n[cyan]Install skill '{skill.name}'?[/cyan]",
|
|
723
|
-
default=True,
|
|
724
|
-
)
|
|
725
|
-
if confirm:
|
|
726
|
-
self._install_skill(skill)
|
|
727
|
-
self.console.print(
|
|
728
|
-
f"\n[green]✓ Skill '{skill.name}' installed[/green]"
|
|
729
|
-
)
|
|
730
|
-
else:
|
|
731
|
-
self.console.print("[red]Invalid skill number[/red]")
|
|
732
|
-
except ValueError:
|
|
733
|
-
self.console.print(
|
|
734
|
-
"[red]Invalid input. Please enter a number.[/red]"
|
|
735
|
-
)
|
|
736
|
-
|
|
737
|
-
Prompt.ask("\nPress Enter to continue")
|
|
698
|
+
# Install/Uninstall skills with category-based selection
|
|
699
|
+
self._manage_skill_installation()
|
|
738
700
|
|
|
739
701
|
elif choice == "2":
|
|
740
702
|
# Configure skills interactively
|
|
@@ -857,72 +819,461 @@ class ConfigureCommand(BaseCommand):
|
|
|
857
819
|
self.console.print("[red]Invalid choice. Please try again.[/red]")
|
|
858
820
|
Prompt.ask("\nPress Enter to continue")
|
|
859
821
|
|
|
860
|
-
def
|
|
861
|
-
"""
|
|
862
|
-
from rich import box
|
|
863
|
-
from rich.table import Table
|
|
822
|
+
def _detect_skill_patterns(self, skills: list[dict]) -> dict[str, list[dict]]:
|
|
823
|
+
"""Group skills by detected common prefixes.
|
|
864
824
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
deployed_ids = self._get_deployed_skill_ids()
|
|
825
|
+
Args:
|
|
826
|
+
skills: List of skill dictionaries
|
|
868
827
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
table.add_column("Source", style="bright_yellow")
|
|
875
|
-
table.add_column("Status", style="bright_black")
|
|
876
|
-
|
|
877
|
-
# Populate table
|
|
878
|
-
for i, skill in enumerate(all_skills, 1):
|
|
879
|
-
# Determine status
|
|
880
|
-
if skill.skill_id in deployed_ids:
|
|
881
|
-
status = "[green]Installed[/green]"
|
|
882
|
-
else:
|
|
883
|
-
status = "Available"
|
|
828
|
+
Returns:
|
|
829
|
+
Dict mapping pattern prefix to list of skills.
|
|
830
|
+
Skills without pattern match go under "" (empty string) key.
|
|
831
|
+
"""
|
|
832
|
+
from collections import defaultdict
|
|
884
833
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
source = "MPM Skills"
|
|
888
|
-
elif skill.source == "user":
|
|
889
|
-
source = "User Skills"
|
|
890
|
-
elif skill.source == "project":
|
|
891
|
-
source = "Project Skills"
|
|
892
|
-
else:
|
|
893
|
-
source = skill.source.title()
|
|
834
|
+
# Count prefix occurrences (try 1-segment and 2-segment prefixes)
|
|
835
|
+
prefix_counts = defaultdict(list)
|
|
894
836
|
|
|
895
|
-
|
|
896
|
-
|
|
837
|
+
for skill in skills:
|
|
838
|
+
skill_id = skill.get("name", skill.get("skill_id", ""))
|
|
897
839
|
|
|
898
|
-
|
|
840
|
+
# Try to extract prefixes (split by hyphen)
|
|
841
|
+
parts = skill_id.split("-")
|
|
899
842
|
|
|
900
|
-
|
|
843
|
+
if len(parts) >= 2:
|
|
844
|
+
# Try 2-segment prefix first (e.g., "toolchains-universal")
|
|
845
|
+
two_seg_prefix = f"{parts[0]}-{parts[1]}"
|
|
846
|
+
prefix_counts[two_seg_prefix].append(skill)
|
|
901
847
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
848
|
+
# Also try 1-segment prefix (e.g., "digitalocean")
|
|
849
|
+
one_seg_prefix = parts[0]
|
|
850
|
+
if one_seg_prefix != two_seg_prefix:
|
|
851
|
+
prefix_counts[one_seg_prefix].append(skill)
|
|
852
|
+
|
|
853
|
+
# Build pattern groups (require at least 2 skills per pattern)
|
|
854
|
+
pattern_groups = defaultdict(list)
|
|
855
|
+
used_skills = set()
|
|
856
|
+
|
|
857
|
+
# Prefer longer (more specific) prefixes
|
|
858
|
+
sorted_prefixes = sorted(prefix_counts.keys(), key=lambda x: (-len(x), x))
|
|
859
|
+
|
|
860
|
+
for prefix in sorted_prefixes:
|
|
861
|
+
matching_skills = prefix_counts[prefix]
|
|
862
|
+
|
|
863
|
+
# Only create a pattern group if we have 2+ skills and they're not already grouped
|
|
864
|
+
available_skills = [s for s in matching_skills if id(s) not in used_skills]
|
|
865
|
+
|
|
866
|
+
if len(available_skills) >= 2:
|
|
867
|
+
pattern_groups[prefix] = available_skills
|
|
868
|
+
used_skills.update(id(s) for s in available_skills)
|
|
869
|
+
|
|
870
|
+
# Add ungrouped skills to "" (Other) group
|
|
871
|
+
for skill in skills:
|
|
872
|
+
if id(skill) not in used_skills:
|
|
873
|
+
pattern_groups[""].append(skill)
|
|
874
|
+
|
|
875
|
+
return dict(pattern_groups)
|
|
876
|
+
|
|
877
|
+
def _get_pattern_icon(self, prefix: str) -> str:
|
|
878
|
+
"""Get icon for a pattern prefix.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
prefix: Pattern prefix (e.g., "digitalocean", "vercel")
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Emoji icon for the pattern
|
|
885
|
+
"""
|
|
886
|
+
pattern_icons = {
|
|
887
|
+
"digitalocean": "🌊",
|
|
888
|
+
"aws": "☁️",
|
|
889
|
+
"github": "🐙",
|
|
890
|
+
"google": "🔍",
|
|
891
|
+
"vercel": "▲",
|
|
892
|
+
"netlify": "🦋",
|
|
893
|
+
"universal-testing": "🧪",
|
|
894
|
+
"universal-debugging": "🐛",
|
|
895
|
+
"universal-security": "🔒",
|
|
896
|
+
"toolchains-python": "🐍",
|
|
897
|
+
"toolchains-typescript": "📘",
|
|
898
|
+
"toolchains-javascript": "📒",
|
|
899
|
+
}
|
|
900
|
+
return pattern_icons.get(prefix, "📦")
|
|
901
|
+
|
|
902
|
+
def _manage_skill_installation(self) -> None:
|
|
903
|
+
"""Manage skill installation with category-based questionary checkbox selection."""
|
|
904
|
+
import questionary
|
|
905
|
+
|
|
906
|
+
# Get all skills
|
|
907
|
+
all_skills = self._get_all_skills_from_git()
|
|
908
|
+
if not all_skills:
|
|
909
|
+
self.console.print(
|
|
910
|
+
"[yellow]No skills available. Try syncing skills first.[/yellow]"
|
|
911
|
+
)
|
|
912
|
+
Prompt.ask("\nPress Enter to continue")
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
# Get deployed skills
|
|
916
|
+
deployed = self._get_deployed_skill_ids()
|
|
917
|
+
|
|
918
|
+
# Group by category
|
|
919
|
+
grouped = {}
|
|
920
|
+
for skill in all_skills:
|
|
921
|
+
# Try to get category from tags or use toolchain
|
|
922
|
+
category = None
|
|
923
|
+
tags = skill.get("tags", [])
|
|
924
|
+
|
|
925
|
+
# Look for category tag
|
|
926
|
+
for tag in tags:
|
|
927
|
+
if tag in [
|
|
928
|
+
"universal",
|
|
929
|
+
"python",
|
|
930
|
+
"typescript",
|
|
931
|
+
"javascript",
|
|
932
|
+
"go",
|
|
933
|
+
"rust",
|
|
934
|
+
]:
|
|
935
|
+
category = tag
|
|
936
|
+
break
|
|
937
|
+
|
|
938
|
+
# Fallback to toolchain or universal
|
|
939
|
+
if not category:
|
|
940
|
+
category = skill.get("toolchain", "universal")
|
|
941
|
+
|
|
942
|
+
if category not in grouped:
|
|
943
|
+
grouped[category] = []
|
|
944
|
+
grouped[category].append(skill)
|
|
945
|
+
|
|
946
|
+
# Category icons
|
|
947
|
+
icons = {
|
|
948
|
+
"universal": "🌐",
|
|
949
|
+
"python": "🐍",
|
|
950
|
+
"typescript": "📘",
|
|
951
|
+
"javascript": "📒",
|
|
952
|
+
"go": "🔷",
|
|
953
|
+
"rust": "⚙️",
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
# Sort categories: universal first, then alphabetically
|
|
957
|
+
categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
|
|
958
|
+
|
|
959
|
+
while True:
|
|
960
|
+
# Show category selection first
|
|
961
|
+
self.console.clear()
|
|
962
|
+
self._display_header()
|
|
963
|
+
self.console.print("\n[bold cyan]Skills Management[/bold cyan]")
|
|
964
|
+
self.console.print(
|
|
965
|
+
f"[dim]{len(all_skills)} skills available, {len(deployed)} installed[/dim]\n"
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
cat_choices = [
|
|
969
|
+
Choice(
|
|
970
|
+
title=f"{icons.get(cat, '📦')} {cat.title()} ({len(grouped[cat])} skills)",
|
|
971
|
+
value=cat,
|
|
972
|
+
)
|
|
973
|
+
for cat in categories
|
|
974
|
+
]
|
|
975
|
+
cat_choices.append(Choice(title="← Back to main menu", value="back"))
|
|
976
|
+
|
|
977
|
+
selected_cat = questionary.select(
|
|
978
|
+
"Select a category:", choices=cat_choices, style=self.QUESTIONARY_STYLE
|
|
979
|
+
).ask()
|
|
980
|
+
|
|
981
|
+
if selected_cat is None or selected_cat == "back":
|
|
982
|
+
return
|
|
983
|
+
|
|
984
|
+
# Show skills in category with checkbox selection
|
|
985
|
+
category_skills = grouped[selected_cat]
|
|
986
|
+
|
|
987
|
+
# Detect pattern groups within category
|
|
988
|
+
pattern_groups = self._detect_skill_patterns(category_skills)
|
|
989
|
+
|
|
990
|
+
# Build choices with pattern grouping and installation status
|
|
991
|
+
skill_choices = []
|
|
992
|
+
|
|
993
|
+
# Track which skills belong to which group for expansion later
|
|
994
|
+
group_to_skills = {}
|
|
995
|
+
|
|
996
|
+
# Sort pattern groups: "" (Other) last, rest alphabetically
|
|
997
|
+
sorted_patterns = sorted(pattern_groups.keys(), key=lambda x: (x == "", x))
|
|
998
|
+
|
|
999
|
+
for pattern in sorted_patterns:
|
|
1000
|
+
pattern_skills = pattern_groups[pattern]
|
|
1001
|
+
|
|
1002
|
+
# Skip empty groups
|
|
1003
|
+
if not pattern_skills:
|
|
1004
|
+
continue
|
|
1005
|
+
|
|
1006
|
+
# Collect skill IDs in this group
|
|
1007
|
+
skill_ids_in_group = []
|
|
1008
|
+
for skill in pattern_skills:
|
|
1009
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1010
|
+
skill_ids_in_group.append(skill_id)
|
|
1011
|
+
|
|
1012
|
+
# Check if all skills in group are installed
|
|
1013
|
+
all_installed = all(
|
|
1014
|
+
skill.get(
|
|
1015
|
+
"deployment_name", skill.get("name", skill.get("skill_id"))
|
|
1016
|
+
)
|
|
1017
|
+
in deployed
|
|
1018
|
+
or skill.get("name", skill.get("skill_id")) in deployed
|
|
1019
|
+
for skill in pattern_skills
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Add pattern group header as selectable choice
|
|
1023
|
+
if pattern:
|
|
1024
|
+
# Named pattern group
|
|
1025
|
+
pattern_icon = self._get_pattern_icon(pattern)
|
|
1026
|
+
skill_count = len(pattern_skills)
|
|
1027
|
+
group_key = f"__group__:{pattern}"
|
|
1028
|
+
group_to_skills[group_key] = skill_ids_in_group
|
|
1029
|
+
|
|
1030
|
+
skill_choices.append(
|
|
1031
|
+
Choice(
|
|
1032
|
+
title=f"{pattern_icon} {pattern} ({skill_count} skills) [Select All]",
|
|
1033
|
+
value=group_key,
|
|
1034
|
+
checked=all_installed,
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
elif pattern_skills:
|
|
1038
|
+
# "Other" group - only show if there are skills
|
|
1039
|
+
group_key = "__group__:Other"
|
|
1040
|
+
group_to_skills[group_key] = skill_ids_in_group
|
|
1041
|
+
|
|
1042
|
+
skill_choices.append(
|
|
1043
|
+
Choice(
|
|
1044
|
+
title=f"📦 Other ({len(pattern_skills)} skills) [Select All]",
|
|
1045
|
+
value=group_key,
|
|
1046
|
+
checked=all_installed,
|
|
1047
|
+
)
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Add skills in this pattern group
|
|
1051
|
+
for skill in sorted(pattern_skills, key=lambda x: x.get("name", "")):
|
|
1052
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1053
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1054
|
+
description = skill.get("description", "")[:50]
|
|
1055
|
+
|
|
1056
|
+
# Check if installed
|
|
1057
|
+
is_installed = deploy_name in deployed or skill_id in deployed
|
|
1058
|
+
|
|
1059
|
+
# Add indentation for pattern-grouped skills (all skills are indented)
|
|
1060
|
+
skill_choices.append(
|
|
1061
|
+
Choice(
|
|
1062
|
+
title=f" {skill_id} - {description}",
|
|
1063
|
+
value=skill_id,
|
|
1064
|
+
checked=is_installed,
|
|
1065
|
+
)
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
# Add spacing between pattern groups (not after last group)
|
|
1069
|
+
if pattern != sorted_patterns[-1]:
|
|
1070
|
+
skill_choices.append(Separator())
|
|
1071
|
+
|
|
1072
|
+
self.console.clear()
|
|
1073
|
+
self._display_header()
|
|
1074
|
+
self.console.print(
|
|
1075
|
+
f"\n{icons.get(selected_cat, '📦')} [bold]{selected_cat.title()}[/bold]"
|
|
1076
|
+
)
|
|
1077
|
+
self.console.print(
|
|
1078
|
+
"[dim]Use spacebar to toggle individual skills or entire groups, enter to confirm[/dim]\n"
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
selected = questionary.checkbox(
|
|
1082
|
+
"Select skills to install:",
|
|
1083
|
+
choices=skill_choices,
|
|
1084
|
+
style=self.QUESTIONARY_STYLE,
|
|
1085
|
+
).ask()
|
|
907
1086
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
# Get skills from all sources
|
|
911
|
-
bundled = registry.list_skills(source="bundled")
|
|
912
|
-
user = registry.list_skills(source="user")
|
|
913
|
-
project = registry.list_skills(source="project")
|
|
1087
|
+
if selected is None:
|
|
1088
|
+
continue # User cancelled, go back to category selection
|
|
914
1089
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1090
|
+
# Process group selections - expand to individual skills
|
|
1091
|
+
selected_set = set()
|
|
1092
|
+
for item in selected:
|
|
1093
|
+
if item.startswith("__group__:"):
|
|
1094
|
+
# Expand group selection to all skills in that group
|
|
1095
|
+
selected_set.update(group_to_skills[item])
|
|
1096
|
+
else:
|
|
1097
|
+
# Individual skill selection
|
|
1098
|
+
selected_set.add(item)
|
|
1099
|
+
|
|
1100
|
+
current_in_cat = set()
|
|
1101
|
+
|
|
1102
|
+
# Find currently installed skills in this category
|
|
1103
|
+
for skill in category_skills:
|
|
1104
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1105
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1106
|
+
if deploy_name in deployed or skill_id in deployed:
|
|
1107
|
+
current_in_cat.add(skill_id)
|
|
1108
|
+
|
|
1109
|
+
# Install newly selected
|
|
1110
|
+
to_install = selected_set - current_in_cat
|
|
1111
|
+
for skill_id in to_install:
|
|
1112
|
+
skill = next(
|
|
1113
|
+
(
|
|
1114
|
+
s
|
|
1115
|
+
for s in category_skills
|
|
1116
|
+
if s.get("name") == skill_id or s.get("skill_id") == skill_id
|
|
1117
|
+
),
|
|
1118
|
+
None,
|
|
1119
|
+
)
|
|
1120
|
+
if skill:
|
|
1121
|
+
self._install_skill_from_dict(skill)
|
|
1122
|
+
self.console.print(f"[green]✓ Installed {skill_id}[/green]")
|
|
1123
|
+
|
|
1124
|
+
# Uninstall deselected
|
|
1125
|
+
to_uninstall = current_in_cat - selected_set
|
|
1126
|
+
for skill_id in to_uninstall:
|
|
1127
|
+
# Find the skill to get deployment_name
|
|
1128
|
+
skill = next(
|
|
1129
|
+
(
|
|
1130
|
+
s
|
|
1131
|
+
for s in category_skills
|
|
1132
|
+
if s.get("name") == skill_id or s.get("skill_id") == skill_id
|
|
1133
|
+
),
|
|
1134
|
+
None,
|
|
1135
|
+
)
|
|
1136
|
+
if skill:
|
|
1137
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1138
|
+
# Use the name that's actually in deployed set
|
|
1139
|
+
name_to_uninstall = (
|
|
1140
|
+
deploy_name if deploy_name in deployed else skill_id
|
|
1141
|
+
)
|
|
1142
|
+
self._uninstall_skill_by_name(name_to_uninstall)
|
|
1143
|
+
self.console.print(f"[yellow]✗ Uninstalled {skill_id}[/yellow]")
|
|
921
1144
|
|
|
922
|
-
|
|
1145
|
+
# Update deployed set for next iteration
|
|
1146
|
+
deployed = self._get_deployed_skill_ids()
|
|
1147
|
+
|
|
1148
|
+
# Show completion message
|
|
1149
|
+
if to_install or to_uninstall:
|
|
1150
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1151
|
+
|
|
1152
|
+
def _get_all_skills_from_git(self) -> list:
|
|
1153
|
+
"""Get all skills from Git-based skill manager.
|
|
1154
|
+
|
|
1155
|
+
Returns:
|
|
1156
|
+
List of skill dicts with full metadata from GitSkillSourceManager.
|
|
1157
|
+
"""
|
|
1158
|
+
from ...config.skill_sources import SkillSourceConfiguration
|
|
1159
|
+
from ...services.skills.git_skill_source_manager import GitSkillSourceManager
|
|
1160
|
+
|
|
1161
|
+
try:
|
|
1162
|
+
config = SkillSourceConfiguration()
|
|
1163
|
+
manager = GitSkillSourceManager(config)
|
|
1164
|
+
return manager.get_all_skills()
|
|
1165
|
+
except Exception as e:
|
|
1166
|
+
self.console.print(
|
|
1167
|
+
f"[yellow]Warning: Could not load Git skills: {e}[/yellow]"
|
|
1168
|
+
)
|
|
1169
|
+
return []
|
|
1170
|
+
|
|
1171
|
+
def _display_skills_table_grouped(self) -> None:
|
|
1172
|
+
"""Display skills in a table grouped by category, like agents."""
|
|
1173
|
+
from rich import box
|
|
1174
|
+
from rich.table import Table
|
|
1175
|
+
|
|
1176
|
+
# Get all skills from Git manager
|
|
1177
|
+
all_skills = self._get_all_skills_from_git()
|
|
1178
|
+
deployed_ids = self._get_deployed_skill_ids()
|
|
1179
|
+
|
|
1180
|
+
if not all_skills:
|
|
1181
|
+
self.console.print(
|
|
1182
|
+
"[yellow]No skills available. Try syncing skills first.[/yellow]"
|
|
1183
|
+
)
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
# Group skills by category/toolchain
|
|
1187
|
+
grouped = {}
|
|
1188
|
+
for skill in all_skills:
|
|
1189
|
+
# Try to get category from tags or use toolchain
|
|
1190
|
+
category = None
|
|
1191
|
+
tags = skill.get("tags", [])
|
|
1192
|
+
|
|
1193
|
+
# Look for category tag
|
|
1194
|
+
for tag in tags:
|
|
1195
|
+
if tag in [
|
|
1196
|
+
"universal",
|
|
1197
|
+
"python",
|
|
1198
|
+
"typescript",
|
|
1199
|
+
"javascript",
|
|
1200
|
+
"go",
|
|
1201
|
+
"rust",
|
|
1202
|
+
]:
|
|
1203
|
+
category = tag
|
|
1204
|
+
break
|
|
1205
|
+
|
|
1206
|
+
# Fallback to toolchain or universal
|
|
1207
|
+
if not category:
|
|
1208
|
+
category = skill.get("toolchain", "universal")
|
|
1209
|
+
|
|
1210
|
+
if category not in grouped:
|
|
1211
|
+
grouped[category] = []
|
|
1212
|
+
grouped[category].append(skill)
|
|
1213
|
+
|
|
1214
|
+
# Sort categories: universal first, then alphabetically
|
|
1215
|
+
categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
|
|
1216
|
+
|
|
1217
|
+
# Track global skill number across all categories
|
|
1218
|
+
skill_counter = 0
|
|
1219
|
+
|
|
1220
|
+
for category in categories:
|
|
1221
|
+
category_skills = grouped[category]
|
|
1222
|
+
|
|
1223
|
+
# Category header with icon
|
|
1224
|
+
icons = {
|
|
1225
|
+
"universal": "🌐",
|
|
1226
|
+
"python": "🐍",
|
|
1227
|
+
"typescript": "📘",
|
|
1228
|
+
"javascript": "📒",
|
|
1229
|
+
"go": "🔷",
|
|
1230
|
+
"rust": "⚙️",
|
|
1231
|
+
}
|
|
1232
|
+
icon = icons.get(category, "📦")
|
|
1233
|
+
self.console.print(
|
|
1234
|
+
f"\n{icon} [bold cyan]{category.title()}[/bold cyan] ({len(category_skills)} skills)"
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
# Create table for this category
|
|
1238
|
+
table = Table(show_header=True, header_style="bold", box=box.SIMPLE)
|
|
1239
|
+
table.add_column("#", style="dim", width=4)
|
|
1240
|
+
table.add_column("Skill ID", style="cyan", width=35)
|
|
1241
|
+
table.add_column("Description", style="white", width=45)
|
|
1242
|
+
table.add_column("Status", style="green", width=12)
|
|
1243
|
+
|
|
1244
|
+
for skill in sorted(category_skills, key=lambda x: x.get("name", "")):
|
|
1245
|
+
skill_counter += 1
|
|
1246
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1247
|
+
# Use deployment_name for matching if available
|
|
1248
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1249
|
+
description = skill.get("description", "")[:45]
|
|
1250
|
+
|
|
1251
|
+
# Check if installed - handle both deployment_name and skill_id
|
|
1252
|
+
is_installed = deploy_name in deployed_ids or skill_id in deployed_ids
|
|
1253
|
+
status = "[green]✓ Installed[/green]" if is_installed else "Available"
|
|
1254
|
+
|
|
1255
|
+
table.add_row(str(skill_counter), skill_id, description, status)
|
|
1256
|
+
|
|
1257
|
+
self.console.print(table)
|
|
1258
|
+
|
|
1259
|
+
# Summary
|
|
1260
|
+
total = len(all_skills)
|
|
1261
|
+
installed = sum(
|
|
1262
|
+
1
|
|
1263
|
+
for s in all_skills
|
|
1264
|
+
if s.get("deployment_name", s.get("name", "")) in deployed_ids
|
|
1265
|
+
or s.get("name", "") in deployed_ids
|
|
1266
|
+
)
|
|
1267
|
+
self.console.print(
|
|
1268
|
+
f"\n[dim]Showing {total} skills ({installed} installed)[/dim]"
|
|
1269
|
+
)
|
|
923
1270
|
|
|
924
1271
|
def _get_deployed_skill_ids(self) -> set:
|
|
925
|
-
"""Get set of deployed skill IDs from .claude/skills/ directory.
|
|
1272
|
+
"""Get set of deployed skill IDs from .claude/skills/ directory.
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
Set of skill directory names and common variations for matching.
|
|
1276
|
+
"""
|
|
926
1277
|
from pathlib import Path
|
|
927
1278
|
|
|
928
1279
|
skills_dir = Path.cwd() / ".claude" / "skills"
|
|
@@ -933,7 +1284,11 @@ class ConfigureCommand(BaseCommand):
|
|
|
933
1284
|
deployed_ids = set()
|
|
934
1285
|
for skill_dir in skills_dir.iterdir():
|
|
935
1286
|
if skill_dir.is_dir() and not skill_dir.name.startswith("."):
|
|
1287
|
+
# Add both the directory name and common variations
|
|
936
1288
|
deployed_ids.add(skill_dir.name)
|
|
1289
|
+
# Also add without prefix for matching (e.g., universal-testing -> testing)
|
|
1290
|
+
if skill_dir.name.startswith("universal-"):
|
|
1291
|
+
deployed_ids.add(skill_dir.name.replace("universal-", "", 1))
|
|
937
1292
|
|
|
938
1293
|
return deployed_ids
|
|
939
1294
|
|
|
@@ -967,6 +1322,45 @@ class ConfigureCommand(BaseCommand):
|
|
|
967
1322
|
if target_dir.exists():
|
|
968
1323
|
shutil.rmtree(target_dir)
|
|
969
1324
|
|
|
1325
|
+
def _install_skill_from_dict(self, skill_dict: dict) -> None:
|
|
1326
|
+
"""Install a skill from Git skill dict to .claude/skills/ directory.
|
|
1327
|
+
|
|
1328
|
+
Args:
|
|
1329
|
+
skill_dict: Skill metadata dict from GitSkillSourceManager.get_all_skills()
|
|
1330
|
+
"""
|
|
1331
|
+
from pathlib import Path
|
|
1332
|
+
|
|
1333
|
+
skill_id = skill_dict.get("name", skill_dict.get("skill_id", "unknown"))
|
|
1334
|
+
content = skill_dict.get("content", "")
|
|
1335
|
+
|
|
1336
|
+
if not content:
|
|
1337
|
+
self.console.print(
|
|
1338
|
+
f"[yellow]Warning: Skill '{skill_id}' has no content[/yellow]"
|
|
1339
|
+
)
|
|
1340
|
+
return
|
|
1341
|
+
|
|
1342
|
+
# Target directory using deployment_name if available
|
|
1343
|
+
deploy_name = skill_dict.get("deployment_name", skill_id)
|
|
1344
|
+
target_dir = Path.cwd() / ".claude" / "skills" / deploy_name
|
|
1345
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
1346
|
+
|
|
1347
|
+
# Write skill content to skill.md
|
|
1348
|
+
skill_file = target_dir / "skill.md"
|
|
1349
|
+
skill_file.write_text(content, encoding="utf-8")
|
|
1350
|
+
|
|
1351
|
+
def _uninstall_skill_by_name(self, skill_name: str) -> None:
|
|
1352
|
+
"""Uninstall a skill by name from .claude/skills/ directory.
|
|
1353
|
+
|
|
1354
|
+
Args:
|
|
1355
|
+
skill_name: Name of skill directory to remove
|
|
1356
|
+
"""
|
|
1357
|
+
import shutil
|
|
1358
|
+
from pathlib import Path
|
|
1359
|
+
|
|
1360
|
+
target_dir = Path.cwd() / ".claude" / "skills" / skill_name
|
|
1361
|
+
if target_dir.exists():
|
|
1362
|
+
shutil.rmtree(target_dir)
|
|
1363
|
+
|
|
970
1364
|
def _display_behavior_files(self) -> None:
|
|
971
1365
|
"""Display current behavior files."""
|
|
972
1366
|
self.behavior_manager.display_behavior_files()
|
|
@@ -1682,18 +2076,32 @@ class ConfigureCommand(BaseCommand):
|
|
|
1682
2076
|
)
|
|
1683
2077
|
display_name = self._format_display_name(raw_display_name)
|
|
1684
2078
|
|
|
1685
|
-
# Check if agent is
|
|
2079
|
+
# Check if agent is required (cannot be unchecked)
|
|
2080
|
+
required_agents = set(self.unified_config.agents.required)
|
|
2081
|
+
is_required = (
|
|
2082
|
+
agent_leaf_name in required_agents
|
|
2083
|
+
or agent_id in required_agents
|
|
2084
|
+
)
|
|
2085
|
+
|
|
2086
|
+
# Format choice text with [Required] indicator
|
|
2087
|
+
if is_required:
|
|
2088
|
+
choice_text = f" {display_name} [Required]"
|
|
2089
|
+
else:
|
|
2090
|
+
choice_text = f" {display_name}"
|
|
1686
2091
|
|
|
1687
|
-
#
|
|
1688
|
-
|
|
2092
|
+
# Required agents are always selected
|
|
2093
|
+
is_selected = is_required or agent_id in current_selection
|
|
1689
2094
|
|
|
1690
|
-
|
|
2095
|
+
# Add to current selection if required
|
|
2096
|
+
if is_required:
|
|
2097
|
+
current_selection.add(agent_id)
|
|
1691
2098
|
|
|
1692
2099
|
choices.append(
|
|
1693
2100
|
Choice(
|
|
1694
2101
|
title=choice_text,
|
|
1695
2102
|
value=agent_id, # Use agent_id for value
|
|
1696
2103
|
checked=is_selected,
|
|
2104
|
+
disabled=is_required, # Disable checkbox for required agents
|
|
1697
2105
|
)
|
|
1698
2106
|
)
|
|
1699
2107
|
|
|
@@ -1702,6 +2110,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
1702
2110
|
self.console.print(
|
|
1703
2111
|
"[dim][ ] Unchecked = Available (check to install)[/dim]"
|
|
1704
2112
|
)
|
|
2113
|
+
self.console.print("[dim][Required] = Core agents (always installed)[/dim]")
|
|
1705
2114
|
self.console.print(
|
|
1706
2115
|
"[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
|
|
1707
2116
|
)
|
|
@@ -1788,12 +2197,37 @@ class ConfigureCommand(BaseCommand):
|
|
|
1788
2197
|
|
|
1789
2198
|
# No controls selected - use the individual selections as final
|
|
1790
2199
|
final_selection = set(selected_values)
|
|
2200
|
+
|
|
2201
|
+
# Ensure required agents are always in the final selection
|
|
2202
|
+
required_agents = set(self.unified_config.agents.required)
|
|
2203
|
+
for agent in agents:
|
|
2204
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2205
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
2206
|
+
if agent_leaf_name in required_agents or agent_id in required_agents:
|
|
2207
|
+
final_selection.add(agent_id)
|
|
2208
|
+
|
|
1791
2209
|
break
|
|
1792
2210
|
|
|
1793
2211
|
# Determine changes
|
|
1794
2212
|
to_deploy = final_selection - deployed_full_paths
|
|
1795
2213
|
to_remove = deployed_full_paths - final_selection
|
|
1796
2214
|
|
|
2215
|
+
# Prevent removal of required agents
|
|
2216
|
+
required_agents = set(self.unified_config.agents.required)
|
|
2217
|
+
to_remove_filtered = set()
|
|
2218
|
+
for agent_id in to_remove:
|
|
2219
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
2220
|
+
if (
|
|
2221
|
+
agent_leaf_name not in required_agents
|
|
2222
|
+
and agent_id not in required_agents
|
|
2223
|
+
):
|
|
2224
|
+
to_remove_filtered.add(agent_id)
|
|
2225
|
+
else:
|
|
2226
|
+
self.console.print(
|
|
2227
|
+
f"[yellow]⚠ Cannot remove required agent: {agent_id}[/yellow]"
|
|
2228
|
+
)
|
|
2229
|
+
to_remove = to_remove_filtered
|
|
2230
|
+
|
|
1797
2231
|
if not to_deploy and not to_remove:
|
|
1798
2232
|
self.console.print("[yellow]No changes needed[/yellow]")
|
|
1799
2233
|
Prompt.ask("\nPress Enter to continue")
|