claude-mpm 5.4.70__py3-none-any.whl → 5.4.73__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 CHANGED
@@ -1 +1 @@
1
- 5.4.70
1
+ 5.4.71
@@ -91,7 +91,11 @@ def main(argv: Optional[list] = None):
91
91
  )
92
92
 
93
93
  try:
94
- run_background_services()
94
+ # Check for --force-sync flag or environment variable
95
+ force_sync = getattr(args, "force_sync", False) or os.environ.get(
96
+ "CLAUDE_MPM_FORCE_SYNC", "0"
97
+ ) in ("1", "true", "True", "yes")
98
+ run_background_services(force_sync=force_sync)
95
99
  launch_progress.finish(message="Ready")
96
100
 
97
101
  # Inform user about Claude Code initialization delay (3-5 seconds)
@@ -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()
@@ -18,7 +18,7 @@ ARCHITECTURE:
18
18
  """
19
19
 
20
20
  import os
21
- import subprocess
21
+ import subprocess # nosec B404
22
22
  from typing import Optional
23
23
 
24
24
  from rich.console import Console
@@ -84,6 +84,7 @@ class SkillsManagementCommand(BaseCommand):
84
84
  SkillsCommands.INFO.value: self._show_skill_info,
85
85
  SkillsCommands.CONFIG.value: self._manage_config,
86
86
  SkillsCommands.CONFIGURE.value: self._configure_skills,
87
+ SkillsCommands.SELECT.value: self._select_skills_interactive,
87
88
  # GitHub deployment commands
88
89
  SkillsCommands.DEPLOY_FROM_GITHUB.value: self._deploy_from_github,
89
90
  SkillsCommands.LIST_AVAILABLE.value: self._list_available_github_skills,
@@ -504,7 +505,7 @@ class SkillsManagementCommand(BaseCommand):
504
505
  # Open in editor
505
506
  editor = os.environ.get("EDITOR", "nano")
506
507
  try:
507
- subprocess.run([editor, str(config_path)], check=True)
508
+ subprocess.run([editor, str(config_path)], check=True) # nosec B603
508
509
  console.print(
509
510
  f"\n[green]Configuration saved to {config_path}[/green]\n"
510
511
  )
@@ -673,33 +674,91 @@ class SkillsManagementCommand(BaseCommand):
673
674
  def _check_deployed_skills(self, args) -> CommandResult:
674
675
  """Check currently deployed skills in ~/.claude/skills/."""
675
676
  try:
676
- result = self.skills_deployer.check_deployed_skills()
677
+ # Get deployed skills
678
+ deployed_result = self.skills_deployer.check_deployed_skills()
679
+ deployed_names = {skill["name"] for skill in deployed_result["skills"]}
677
680
 
678
681
  console.print("\n[bold cyan]Claude Code Skills Status:[/bold cyan]\n")
679
- console.print(f"[dim]Directory: {result['claude_skills_dir']}[/dim]\n")
682
+ console.print(
683
+ f"[dim]Directory: {deployed_result['claude_skills_dir']}[/dim]\n"
684
+ )
685
+
686
+ # Fetch available skills from GitHub to get full list
687
+ try:
688
+ available_result = self.skills_deployer.list_available_skills()
689
+ all_skills = available_result.get("skills", [])
690
+ except Exception as e:
691
+ console.print(
692
+ f"[yellow]Warning: Could not fetch available skills: {e}[/yellow]\n"
693
+ )
694
+ all_skills = []
680
695
 
681
- if result["deployed_count"] == 0:
682
- console.print("[yellow]No skills currently deployed.[/yellow]")
696
+ # Combine deployed and available skills
697
+ skill_map = {}
698
+
699
+ # Add available skills
700
+ for skill in all_skills:
701
+ skill_name = skill.get("name", "")
702
+ if skill_name:
703
+ skill_map[skill_name] = {
704
+ "name": skill.get("display_name")
705
+ or skill_name.replace("-", " ").title(),
706
+ "skill_id": skill_name,
707
+ "source": "MPM Skills",
708
+ "is_deployed": skill_name in deployed_names,
709
+ }
710
+
711
+ # Add any deployed skills not in available list (local/custom skills)
712
+ for skill in deployed_result["skills"]:
713
+ skill_name = skill["name"]
714
+ if skill_name not in skill_map:
715
+ skill_map[skill_name] = {
716
+ "name": skill_name.replace("-", " ").title(),
717
+ "skill_id": skill_name,
718
+ "source": "Local",
719
+ "is_deployed": True,
720
+ }
721
+
722
+ if not skill_map:
723
+ console.print("[yellow]No skills available.[/yellow]")
683
724
  console.print(
684
725
  "[dim]Use 'claude-mpm skills deploy-github' to deploy skills.[/dim]\n"
685
726
  )
686
727
  return CommandResult(success=True, exit_code=0)
687
728
 
688
- console.print(
689
- f"[green]{result['deployed_count']} skill(s) deployed:[/green]\n"
729
+ # Create table matching agent management format
730
+ table = Table(show_header=True, header_style="bold cyan")
731
+ table.add_column("#", style="bright_black", width=6, no_wrap=True)
732
+ table.add_column(
733
+ "Skill ID", style="bright_black", no_wrap=True, overflow="ellipsis"
690
734
  )
735
+ table.add_column(
736
+ "Name", style="bright_cyan", no_wrap=True, overflow="ellipsis"
737
+ )
738
+ table.add_column("Source", style="bright_yellow", no_wrap=True)
739
+ table.add_column("Status", style="bright_black", no_wrap=True)
691
740
 
692
- # Create table for deployed skills
693
- table = Table(show_header=True, header_style="bold cyan")
694
- table.add_column("Skill Name", style="green")
695
- table.add_column("Path", style="dim")
741
+ # Sort skills by name for consistent display
742
+ sorted_skills = sorted(skill_map.values(), key=lambda s: s["skill_id"])
696
743
 
697
- for skill in sorted(result["skills"], key=lambda s: s["name"]):
698
- table.add_row(skill["name"], skill["path"])
744
+ for idx, skill in enumerate(sorted_skills, 1):
745
+ status = (
746
+ "[green]Installed[/green]" if skill["is_deployed"] else "Available"
747
+ )
748
+ table.add_row(
749
+ str(idx), skill["skill_id"], skill["name"], skill["source"], status
750
+ )
699
751
 
700
752
  console.print(table)
701
753
  console.print()
702
754
 
755
+ # Show summary
756
+ deployed_count = sum(1 for s in skill_map.values() if s["is_deployed"])
757
+ console.print(
758
+ f"[dim]Showing {len(skill_map)} skills ({deployed_count} installed, "
759
+ f"{len(skill_map) - deployed_count} available)[/dim]\n"
760
+ )
761
+
703
762
  return CommandResult(success=True, exit_code=0)
704
763
 
705
764
  except Exception as e:
@@ -1222,6 +1281,99 @@ class SkillsManagementCommand(BaseCommand):
1222
1281
  console.print(f"[dim]{traceback.format_exc()}[/dim]")
1223
1282
  return CommandResult(success=False, message=str(e), exit_code=1)
1224
1283
 
1284
+ def _select_skills_interactive(self, args) -> CommandResult:
1285
+ """Interactive skill selection with topic grouping.
1286
+
1287
+ This command provides a two-tier selection interface:
1288
+ 1. Select topic groups (toolchains) to explore
1289
+ 2. Multi-select skills within each topic group
1290
+
1291
+ Features:
1292
+ - Groups skills by toolchain (universal, python, typescript, etc.)
1293
+ - Shows skills auto-included by agent dependencies
1294
+ - Displays token counts for each skill
1295
+ - Updates config and runs reconciliation
1296
+
1297
+ Returns:
1298
+ CommandResult with success/failure status
1299
+ """
1300
+ try:
1301
+ from ...cli.interactive.skill_selector import run_skill_selector
1302
+ from ...core.unified_config import UnifiedConfig
1303
+ from ...services.agents.deployment.deployment_reconciler import (
1304
+ DeploymentReconciler,
1305
+ )
1306
+
1307
+ console.print("\n[bold cyan]Interactive Skill Selector[/bold cyan]\n")
1308
+
1309
+ # Run skill selector
1310
+ selected_skills = run_skill_selector()
1311
+
1312
+ if selected_skills is None:
1313
+ console.print("\n[yellow]Skill selection cancelled[/yellow]")
1314
+ return CommandResult(success=True, exit_code=0)
1315
+
1316
+ # Update config with selected skills
1317
+ config = UnifiedConfig()
1318
+ config.skills.enabled = selected_skills
1319
+
1320
+ # Save config
1321
+ try:
1322
+ config.save()
1323
+ console.print(
1324
+ f"\n[green]✓ Saved {len(selected_skills)} skills to configuration[/green]"
1325
+ )
1326
+ except Exception as e:
1327
+ console.print(f"\n[red]Failed to save configuration: {e}[/red]")
1328
+ return CommandResult(success=False, message=str(e), exit_code=1)
1329
+
1330
+ # Run reconciliation to deploy skills
1331
+ console.print("\n[cyan]Running skill reconciliation...[/cyan]")
1332
+ reconciler = DeploymentReconciler(config)
1333
+
1334
+ try:
1335
+ from pathlib import Path
1336
+
1337
+ project_path = Path.cwd()
1338
+ result = reconciler.reconcile_skills(project_path)
1339
+
1340
+ if result.deployed:
1341
+ console.print(
1342
+ f" [green]✓ Deployed: {', '.join(result.deployed)}[/green]"
1343
+ )
1344
+ if result.removed:
1345
+ console.print(
1346
+ f" [yellow]✓ Removed: {', '.join(result.removed)}[/yellow]"
1347
+ )
1348
+ if result.errors:
1349
+ for error in result.errors:
1350
+ console.print(f" [red]✗ {error}[/red]")
1351
+
1352
+ if result.success:
1353
+ console.print(
1354
+ "\n[bold green]✓ Skill deployment complete![/bold green]"
1355
+ )
1356
+ return CommandResult(success=True, exit_code=0)
1357
+ console.print(
1358
+ f"\n[yellow]⚠ Deployment had {len(result.errors)} errors[/yellow]"
1359
+ )
1360
+ return CommandResult(
1361
+ success=False,
1362
+ message=f"{len(result.errors)} deployment errors",
1363
+ exit_code=1,
1364
+ )
1365
+
1366
+ except Exception as e:
1367
+ console.print(f"\n[red]Reconciliation failed: {e}[/red]")
1368
+ return CommandResult(success=False, message=str(e), exit_code=1)
1369
+
1370
+ except Exception as e:
1371
+ console.print(f"[red]Skill selection error: {e}[/red]")
1372
+ import traceback
1373
+
1374
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
1375
+ return CommandResult(success=False, message=str(e), exit_code=1)
1376
+
1225
1377
 
1226
1378
  def manage_skills(args) -> int:
1227
1379
  """
@@ -66,6 +66,7 @@ def ensure_run_attributes(args):
66
66
  args.monitor = getattr(args, "monitor", False)
67
67
  args.force = getattr(args, "force", False)
68
68
  args.reload_agents = getattr(args, "reload_agents", False)
69
+ args.force_sync = getattr(args, "force_sync", False)
69
70
  # Include dependency checking attributes
70
71
  args.check_dependencies = getattr(args, "check_dependencies", True)
71
72
  args.force_check_dependencies = getattr(args, "force_check_dependencies", False)
@@ -10,12 +10,22 @@ from .agent_wizard import (
10
10
  run_interactive_agent_manager,
11
11
  run_interactive_agent_wizard,
12
12
  )
13
+ from .questionary_styles import (
14
+ BANNER_WIDTH,
15
+ MPM_STYLE,
16
+ print_banner,
17
+ print_section_header,
18
+ )
13
19
  from .skills_wizard import SkillsWizard, discover_and_link_runtime_skills
14
20
 
15
21
  __all__ = [
22
+ "BANNER_WIDTH",
23
+ "MPM_STYLE",
16
24
  "AgentWizard",
17
25
  "SkillsWizard",
18
26
  "discover_and_link_runtime_skills",
27
+ "print_banner",
28
+ "print_section_header",
19
29
  "run_interactive_agent_manager",
20
30
  "run_interactive_agent_wizard",
21
31
  ]