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.
@@ -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
- 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")
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
- # View available skills
687
+ # Toggle skill installation
682
688
  self.console.clear()
683
689
  self._display_header()
684
- wizard.list_available_skills()
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=f"__DESELECT_ALL_{collection_id}__",
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=f"__SELECT_ALL_{collection_id}__",
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