claude-mpm 5.4.71__py3-none-any.whl → 5.4.74__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/cli/commands/agents.py +2 -4
- claude_mpm/cli/commands/agents_reconcile.py +197 -0
- claude_mpm/cli/commands/configure.py +179 -14
- claude_mpm/cli/commands/skills.py +166 -14
- claude_mpm/cli/interactive/__init__.py +10 -0
- claude_mpm/cli/interactive/agent_wizard.py +30 -50
- claude_mpm/cli/interactive/questionary_styles.py +65 -0
- claude_mpm/cli/interactive/skill_selector.py +481 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/unified_config.py +33 -8
- claude_mpm/services/agents/deployment/deployment_reconciler.py +539 -0
- claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
- claude_mpm/utils/agent_dependency_loader.py +4 -2
- claude_mpm/utils/robust_installer.py +10 -6
- {claude_mpm-5.4.71.dist-info → claude_mpm-5.4.74.dist-info}/METADATA +1 -1
- {claude_mpm-5.4.71.dist-info → claude_mpm-5.4.74.dist-info}/RECORD +21 -16
- {claude_mpm-5.4.71.dist-info → claude_mpm-5.4.74.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.71.dist-info → claude_mpm-5.4.74.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.71.dist-info → claude_mpm-5.4.74.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.71.dist-info → claude_mpm-5.4.74.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.71.dist-info → claude_mpm-5.4.74.dist-info}/top_level.txt +0 -0
|
@@ -1379,7 +1379,7 @@ class AgentsCommand(AgentCommand):
|
|
|
1379
1379
|
return CommandResult.error_result("agent_id is required")
|
|
1380
1380
|
|
|
1381
1381
|
import os
|
|
1382
|
-
import subprocess
|
|
1382
|
+
import subprocess # nosec B404
|
|
1383
1383
|
|
|
1384
1384
|
from ...services.agents.local_template_manager import (
|
|
1385
1385
|
LocalAgentTemplateManager,
|
|
@@ -1415,7 +1415,7 @@ class AgentsCommand(AgentCommand):
|
|
|
1415
1415
|
|
|
1416
1416
|
# Use system editor
|
|
1417
1417
|
editor = getattr(args, "editor", None) or os.environ.get("EDITOR", "nano")
|
|
1418
|
-
subprocess.run([editor, str(template_file)], check=True)
|
|
1418
|
+
subprocess.run([editor, str(template_file)], check=True) # nosec B603
|
|
1419
1419
|
return CommandResult.success_result(
|
|
1420
1420
|
f"Agent '{agent_id}' edited successfully"
|
|
1421
1421
|
)
|
|
@@ -1519,8 +1519,6 @@ class AgentsCommand(AgentCommand):
|
|
|
1519
1519
|
console.print("For a better experience with integrated configuration:")
|
|
1520
1520
|
console.print(" • Agent management")
|
|
1521
1521
|
console.print(" • Skills management")
|
|
1522
|
-
console.print(" • Template editing")
|
|
1523
|
-
console.print(" • Behavior configuration")
|
|
1524
1522
|
console.print(" • Startup settings\n")
|
|
1525
1523
|
|
|
1526
1524
|
console.print("Please use: [bold green]claude-mpm config[/bold green]\n")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent/Skill Reconciliation CLI Command
|
|
3
|
+
|
|
4
|
+
Shows the reconciliation view between configured and deployed agents/skills,
|
|
5
|
+
and performs reconciliation (deploy missing, remove unneeded).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
claude-mpm agents reconcile [--dry-run] [--show-only]
|
|
9
|
+
claude-mpm skills reconcile [--dry-run] [--show-only]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from ...core.unified_config import UnifiedConfig
|
|
18
|
+
from ...services.agents.deployment.deployment_reconciler import (
|
|
19
|
+
DeploymentReconciler,
|
|
20
|
+
ReconciliationState,
|
|
21
|
+
)
|
|
22
|
+
from ..shared import BaseCommand, CommandResult
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentsReconcileCommand(BaseCommand):
|
|
26
|
+
"""CLI command for agent reconciliation."""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
super().__init__("agents-reconcile")
|
|
30
|
+
self.console = Console()
|
|
31
|
+
|
|
32
|
+
def run(self, args) -> CommandResult:
|
|
33
|
+
"""Execute reconciliation."""
|
|
34
|
+
# Load config
|
|
35
|
+
config = UnifiedConfig()
|
|
36
|
+
reconciler = DeploymentReconciler(config)
|
|
37
|
+
|
|
38
|
+
# Get project path
|
|
39
|
+
project_path = Path(getattr(args, "project_path", "."))
|
|
40
|
+
|
|
41
|
+
# Show current state
|
|
42
|
+
if getattr(args, "show_only", False) or getattr(args, "dry_run", False):
|
|
43
|
+
return self._show_reconciliation_view(reconciler, project_path)
|
|
44
|
+
|
|
45
|
+
# Perform reconciliation
|
|
46
|
+
return self._reconcile_agents(reconciler, project_path)
|
|
47
|
+
|
|
48
|
+
def _show_reconciliation_view(
|
|
49
|
+
self, reconciler: DeploymentReconciler, project_path: Path
|
|
50
|
+
) -> CommandResult:
|
|
51
|
+
"""Show reconciliation view without making changes."""
|
|
52
|
+
view = reconciler.get_reconciliation_view(project_path)
|
|
53
|
+
agent_state = view["agents"]
|
|
54
|
+
skill_state = view["skills"]
|
|
55
|
+
|
|
56
|
+
# Display agents table
|
|
57
|
+
self.console.print(
|
|
58
|
+
"\n[bold blue]═══ Agent Reconciliation View ═══[/bold blue]\n"
|
|
59
|
+
)
|
|
60
|
+
agent_table = self._build_reconciliation_table(agent_state, "Agent")
|
|
61
|
+
self.console.print(agent_table)
|
|
62
|
+
|
|
63
|
+
# Display skills table
|
|
64
|
+
self.console.print(
|
|
65
|
+
"\n[bold blue]═══ Skill Reconciliation View ═══[/bold blue]\n"
|
|
66
|
+
)
|
|
67
|
+
skill_table = self._build_reconciliation_table(skill_state, "Skill")
|
|
68
|
+
self.console.print(skill_table)
|
|
69
|
+
|
|
70
|
+
# Show summary
|
|
71
|
+
self._show_summary(agent_state, skill_state)
|
|
72
|
+
|
|
73
|
+
return CommandResult.success_result("Reconciliation view displayed")
|
|
74
|
+
|
|
75
|
+
def _build_reconciliation_table(
|
|
76
|
+
self, state: ReconciliationState, item_type: str
|
|
77
|
+
) -> Table:
|
|
78
|
+
"""Build Rich table for reconciliation state."""
|
|
79
|
+
table = Table(title=f"{item_type} Deployment Status")
|
|
80
|
+
|
|
81
|
+
table.add_column(f"{item_type}", style="cyan", no_wrap=True)
|
|
82
|
+
table.add_column("Configured", style="green")
|
|
83
|
+
table.add_column("Deployed", style="yellow")
|
|
84
|
+
table.add_column("Action", style="magenta")
|
|
85
|
+
|
|
86
|
+
# All items to consider
|
|
87
|
+
all_items = state.configured | state.deployed | state.cached
|
|
88
|
+
|
|
89
|
+
for item_id in sorted(all_items):
|
|
90
|
+
configured = "✓" if item_id in state.configured else "✗"
|
|
91
|
+
deployed = "✓" if item_id in state.deployed else "✗"
|
|
92
|
+
|
|
93
|
+
# Determine action
|
|
94
|
+
if item_id in state.to_deploy:
|
|
95
|
+
if item_id in state.cached:
|
|
96
|
+
action = "Will deploy"
|
|
97
|
+
else:
|
|
98
|
+
action = "[red]Missing in cache![/red]"
|
|
99
|
+
elif item_id in state.to_remove:
|
|
100
|
+
action = "Will remove"
|
|
101
|
+
elif item_id in state.unchanged:
|
|
102
|
+
action = "-"
|
|
103
|
+
elif item_id in state.cached and item_id not in state.configured:
|
|
104
|
+
action = "[dim]Available (not configured)[/dim]"
|
|
105
|
+
else:
|
|
106
|
+
action = "-"
|
|
107
|
+
|
|
108
|
+
table.add_row(item_id, configured, deployed, action)
|
|
109
|
+
|
|
110
|
+
return table
|
|
111
|
+
|
|
112
|
+
def _show_summary(
|
|
113
|
+
self, agent_state: ReconciliationState, skill_state: ReconciliationState
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Show reconciliation summary."""
|
|
116
|
+
self.console.print("\n[bold]Summary:[/bold]")
|
|
117
|
+
|
|
118
|
+
# Agents
|
|
119
|
+
self.console.print("\nAgents:")
|
|
120
|
+
self.console.print(f" Configured: {len(agent_state.configured)}")
|
|
121
|
+
self.console.print(f" Deployed: {len(agent_state.deployed)}")
|
|
122
|
+
self.console.print(f" To deploy: {len(agent_state.to_deploy)}")
|
|
123
|
+
self.console.print(f" To remove: {len(agent_state.to_remove)}")
|
|
124
|
+
self.console.print(f" Unchanged: {len(agent_state.unchanged)}")
|
|
125
|
+
|
|
126
|
+
# Skills
|
|
127
|
+
self.console.print("\nSkills:")
|
|
128
|
+
self.console.print(f" Configured: {len(skill_state.configured)}")
|
|
129
|
+
self.console.print(f" Deployed: {len(skill_state.deployed)}")
|
|
130
|
+
self.console.print(f" To deploy: {len(skill_state.to_deploy)}")
|
|
131
|
+
self.console.print(f" To remove: {len(skill_state.to_remove)}")
|
|
132
|
+
self.console.print(f" Unchanged: {len(skill_state.unchanged)}")
|
|
133
|
+
|
|
134
|
+
# Show next steps
|
|
135
|
+
if agent_state.to_deploy or skill_state.to_deploy:
|
|
136
|
+
self.console.print(
|
|
137
|
+
"\n[yellow]Run without --show-only to perform deployment[/yellow]"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _reconcile_agents(
|
|
141
|
+
self, reconciler: DeploymentReconciler, project_path: Path
|
|
142
|
+
) -> CommandResult:
|
|
143
|
+
"""Perform agent and skill reconciliation."""
|
|
144
|
+
# Show current state first
|
|
145
|
+
self._show_reconciliation_view(reconciler, project_path)
|
|
146
|
+
|
|
147
|
+
self.console.print("\n[bold blue]Performing reconciliation...[/bold blue]\n")
|
|
148
|
+
|
|
149
|
+
# Reconcile agents
|
|
150
|
+
self.console.print("[cyan]Reconciling agents...[/cyan]")
|
|
151
|
+
agent_result = reconciler.reconcile_agents(project_path)
|
|
152
|
+
|
|
153
|
+
if agent_result.deployed:
|
|
154
|
+
self.console.print(
|
|
155
|
+
f" [green]✓ Deployed: {', '.join(agent_result.deployed)}[/green]"
|
|
156
|
+
)
|
|
157
|
+
if agent_result.removed:
|
|
158
|
+
self.console.print(
|
|
159
|
+
f" [yellow]✓ Removed: {', '.join(agent_result.removed)}[/yellow]"
|
|
160
|
+
)
|
|
161
|
+
if agent_result.errors:
|
|
162
|
+
for error in agent_result.errors:
|
|
163
|
+
self.console.print(f" [red]✗ {error}[/red]")
|
|
164
|
+
|
|
165
|
+
# Reconcile skills
|
|
166
|
+
self.console.print("\n[cyan]Reconciling skills...[/cyan]")
|
|
167
|
+
skill_result = reconciler.reconcile_skills(project_path)
|
|
168
|
+
|
|
169
|
+
if skill_result.deployed:
|
|
170
|
+
self.console.print(
|
|
171
|
+
f" [green]✓ Deployed: {', '.join(skill_result.deployed)}[/green]"
|
|
172
|
+
)
|
|
173
|
+
if skill_result.removed:
|
|
174
|
+
self.console.print(
|
|
175
|
+
f" [yellow]✓ Removed: {', '.join(skill_result.removed)}[/yellow]"
|
|
176
|
+
)
|
|
177
|
+
if skill_result.errors:
|
|
178
|
+
for error in skill_result.errors:
|
|
179
|
+
self.console.print(f" [red]✗ {error}[/red]")
|
|
180
|
+
|
|
181
|
+
# Final summary
|
|
182
|
+
total_errors = len(agent_result.errors) + len(skill_result.errors)
|
|
183
|
+
if total_errors == 0:
|
|
184
|
+
self.console.print("\n[bold green]✓ Reconciliation complete![/bold green]")
|
|
185
|
+
return CommandResult.success_result("Reconciliation successful")
|
|
186
|
+
self.console.print(
|
|
187
|
+
f"\n[bold yellow]⚠ Reconciliation complete with {total_errors} errors[/bold yellow]"
|
|
188
|
+
)
|
|
189
|
+
return CommandResult.error_result(f"Reconciliation had {total_errors} errors")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SkillsReconcileCommand(AgentsReconcileCommand):
|
|
193
|
+
"""CLI command for skill reconciliation (alias to agents reconcile)."""
|
|
194
|
+
|
|
195
|
+
def __init__(self):
|
|
196
|
+
BaseCommand.__init__(self, "skills-reconcile")
|
|
197
|
+
self.console = Console()
|
|
@@ -656,32 +656,84 @@ 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 table display."""
|
|
660
660
|
from ...cli.interactive.skills_wizard import SkillsWizard
|
|
661
|
+
from ...skills.registry import get_registry
|
|
661
662
|
from ...skills.skill_manager import get_manager
|
|
662
663
|
|
|
663
664
|
wizard = SkillsWizard()
|
|
664
665
|
manager = get_manager()
|
|
666
|
+
registry = get_registry()
|
|
665
667
|
|
|
666
668
|
while True:
|
|
667
669
|
self.console.clear()
|
|
668
670
|
self._display_header()
|
|
669
671
|
|
|
670
|
-
|
|
671
|
-
self.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
self.console.print("
|
|
675
|
-
self.console.print(" [
|
|
672
|
+
# Display skills table
|
|
673
|
+
self._display_skills_table(registry)
|
|
674
|
+
|
|
675
|
+
# Show action options
|
|
676
|
+
self.console.print("\n[bold]Actions:[/bold]")
|
|
677
|
+
self.console.print(" [1] Toggle skill installation")
|
|
678
|
+
self.console.print(" [2] Configure skills for agents")
|
|
679
|
+
self.console.print(" [3] View current skill mappings")
|
|
680
|
+
self.console.print(" [4] Auto-link skills to agents")
|
|
681
|
+
self.console.print(" [b] Back to main menu")
|
|
676
682
|
self.console.print()
|
|
677
683
|
|
|
678
684
|
choice = Prompt.ask("[bold blue]Select an option[/bold blue]", default="b")
|
|
679
685
|
|
|
680
686
|
if choice == "1":
|
|
681
|
-
#
|
|
687
|
+
# Toggle skill installation
|
|
682
688
|
self.console.clear()
|
|
683
689
|
self._display_header()
|
|
684
|
-
|
|
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
|
+
|
|
685
737
|
Prompt.ask("\nPress Enter to continue")
|
|
686
738
|
|
|
687
739
|
elif choice == "2":
|
|
@@ -805,6 +857,116 @@ class ConfigureCommand(BaseCommand):
|
|
|
805
857
|
self.console.print("[red]Invalid choice. Please try again.[/red]")
|
|
806
858
|
Prompt.ask("\nPress Enter to continue")
|
|
807
859
|
|
|
860
|
+
def _display_skills_table(self, registry) -> None:
|
|
861
|
+
"""Display skills in a table format like agents."""
|
|
862
|
+
from rich import box
|
|
863
|
+
from rich.table import Table
|
|
864
|
+
|
|
865
|
+
# Get all skills and deployed skill IDs
|
|
866
|
+
all_skills = self._get_all_skills_sorted(registry)
|
|
867
|
+
deployed_ids = self._get_deployed_skill_ids()
|
|
868
|
+
|
|
869
|
+
# Create table with same styling as agents table
|
|
870
|
+
table = Table(show_header=True, header_style="bold cyan", box=box.ROUNDED)
|
|
871
|
+
table.add_column("#", style="bright_black", width=6)
|
|
872
|
+
table.add_column("Skill ID", style="bright_black", overflow="ellipsis")
|
|
873
|
+
table.add_column("Name", style="bright_cyan", overflow="ellipsis")
|
|
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"
|
|
884
|
+
|
|
885
|
+
# Determine source label
|
|
886
|
+
if skill.source == "bundled":
|
|
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()
|
|
894
|
+
|
|
895
|
+
# Get display name (fallback to skill_id with formatting)
|
|
896
|
+
name = skill.name or skill.skill_id.replace("-", " ").title()
|
|
897
|
+
|
|
898
|
+
table.add_row(str(i), skill.skill_id, name, source, status)
|
|
899
|
+
|
|
900
|
+
self.console.print(table)
|
|
901
|
+
|
|
902
|
+
# Show summary
|
|
903
|
+
installed_count = len([s for s in all_skills if s.skill_id in deployed_ids])
|
|
904
|
+
self.console.print(
|
|
905
|
+
f"\nShowing {len(all_skills)} skills ({installed_count} installed)"
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
def _get_all_skills_sorted(self, registry):
|
|
909
|
+
"""Get all skills from registry, sorted by source and name."""
|
|
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")
|
|
914
|
+
|
|
915
|
+
# Combine and sort: bundled first, then user, then project
|
|
916
|
+
# Within each group, sort by name
|
|
917
|
+
all_skills = []
|
|
918
|
+
all_skills.extend(sorted(bundled, key=lambda s: s.name.lower()))
|
|
919
|
+
all_skills.extend(sorted(user, key=lambda s: s.name.lower()))
|
|
920
|
+
all_skills.extend(sorted(project, key=lambda s: s.name.lower()))
|
|
921
|
+
|
|
922
|
+
return all_skills
|
|
923
|
+
|
|
924
|
+
def _get_deployed_skill_ids(self) -> set:
|
|
925
|
+
"""Get set of deployed skill IDs from .claude/skills/ directory."""
|
|
926
|
+
from pathlib import Path
|
|
927
|
+
|
|
928
|
+
skills_dir = Path.cwd() / ".claude" / "skills"
|
|
929
|
+
if not skills_dir.exists():
|
|
930
|
+
return set()
|
|
931
|
+
|
|
932
|
+
# Each deployed skill is a directory in .claude/skills/
|
|
933
|
+
deployed_ids = set()
|
|
934
|
+
for skill_dir in skills_dir.iterdir():
|
|
935
|
+
if skill_dir.is_dir() and not skill_dir.name.startswith("."):
|
|
936
|
+
deployed_ids.add(skill_dir.name)
|
|
937
|
+
|
|
938
|
+
return deployed_ids
|
|
939
|
+
|
|
940
|
+
def _install_skill(self, skill) -> None:
|
|
941
|
+
"""Install a skill to .claude/skills/ directory."""
|
|
942
|
+
import shutil
|
|
943
|
+
from pathlib import Path
|
|
944
|
+
|
|
945
|
+
# Target directory
|
|
946
|
+
target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
|
|
947
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
948
|
+
|
|
949
|
+
# Copy skill file(s)
|
|
950
|
+
if skill.path.is_file():
|
|
951
|
+
# Single file skill - copy to skill.md in target directory
|
|
952
|
+
shutil.copy2(skill.path, target_dir / "skill.md")
|
|
953
|
+
elif skill.path.is_dir():
|
|
954
|
+
# Directory-based skill - copy all contents
|
|
955
|
+
for item in skill.path.iterdir():
|
|
956
|
+
if item.is_file():
|
|
957
|
+
shutil.copy2(item, target_dir / item.name)
|
|
958
|
+
elif item.is_dir():
|
|
959
|
+
shutil.copytree(item, target_dir / item.name, dirs_exist_ok=True)
|
|
960
|
+
|
|
961
|
+
def _uninstall_skill(self, skill) -> None:
|
|
962
|
+
"""Uninstall a skill from .claude/skills/ directory."""
|
|
963
|
+
import shutil
|
|
964
|
+
from pathlib import Path
|
|
965
|
+
|
|
966
|
+
target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
|
|
967
|
+
if target_dir.exists():
|
|
968
|
+
shutil.rmtree(target_dir)
|
|
969
|
+
|
|
808
970
|
def _display_behavior_files(self) -> None:
|
|
809
971
|
"""Display current behavior files."""
|
|
810
972
|
self.behavior_manager.display_behavior_files()
|
|
@@ -1437,18 +1599,20 @@ class ConfigureCommand(BaseCommand):
|
|
|
1437
1599
|
|
|
1438
1600
|
# Add inline control: Select/Deselect all from this collection
|
|
1439
1601
|
if all_selected:
|
|
1602
|
+
deselect_value = f"__DESELECT_ALL_{collection_id}__"
|
|
1440
1603
|
choices.append(
|
|
1441
1604
|
Choice(
|
|
1442
|
-
f" [Deselect all from {collection_id}]",
|
|
1443
|
-
value=
|
|
1605
|
+
f" [Deselect all from {collection_id}]", # nosec B608
|
|
1606
|
+
value=deselect_value,
|
|
1444
1607
|
checked=False,
|
|
1445
1608
|
)
|
|
1446
1609
|
)
|
|
1447
1610
|
else:
|
|
1611
|
+
select_value = f"__SELECT_ALL_{collection_id}__"
|
|
1448
1612
|
choices.append(
|
|
1449
1613
|
Choice(
|
|
1450
|
-
f" [Select all from {collection_id}]",
|
|
1451
|
-
value=
|
|
1614
|
+
f" [Select all from {collection_id}]", # nosec B608
|
|
1615
|
+
value=select_value,
|
|
1452
1616
|
checked=False,
|
|
1453
1617
|
)
|
|
1454
1618
|
)
|
|
@@ -2329,7 +2493,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
2329
2493
|
f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
|
|
2330
2494
|
)
|
|
2331
2495
|
self.console.print()
|
|
2332
|
-
except Exception:
|
|
2496
|
+
except Exception: # nosec B110 - Suppress broad except for failed safety check
|
|
2497
|
+
# Silent failure on safety check - non-critical feature
|
|
2333
2498
|
pass
|
|
2334
2499
|
|
|
2335
2500
|
# Build mapping: agent_id -> AgentConfig
|