claude-mpm 4.12.4__py3-none-any.whl → 4.13.1__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (29) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/__init__.py +10 -0
  3. claude_mpm/cli/commands/agents.py +31 -0
  4. claude_mpm/cli/commands/agents_detect.py +380 -0
  5. claude_mpm/cli/commands/agents_recommend.py +309 -0
  6. claude_mpm/cli/commands/auto_configure.py +572 -0
  7. claude_mpm/cli/parsers/agents_parser.py +9 -0
  8. claude_mpm/cli/parsers/auto_configure_parser.py +245 -0
  9. claude_mpm/cli/parsers/base_parser.py +7 -0
  10. claude_mpm/services/agents/__init__.py +18 -5
  11. claude_mpm/services/agents/auto_config_manager.py +797 -0
  12. claude_mpm/services/agents/observers.py +547 -0
  13. claude_mpm/services/agents/recommender.py +568 -0
  14. claude_mpm/services/core/__init__.py +33 -1
  15. claude_mpm/services/core/interfaces/__init__.py +16 -1
  16. claude_mpm/services/core/interfaces/agent.py +184 -0
  17. claude_mpm/services/core/interfaces/project.py +121 -0
  18. claude_mpm/services/core/models/__init__.py +46 -0
  19. claude_mpm/services/core/models/agent_config.py +397 -0
  20. claude_mpm/services/core/models/toolchain.py +306 -0
  21. claude_mpm/services/project/__init__.py +23 -0
  22. claude_mpm/services/project/detection_strategies.py +719 -0
  23. claude_mpm/services/project/toolchain_analyzer.py +581 -0
  24. {claude_mpm-4.12.4.dist-info → claude_mpm-4.13.1.dist-info}/METADATA +1 -1
  25. {claude_mpm-4.12.4.dist-info → claude_mpm-4.13.1.dist-info}/RECORD +29 -16
  26. {claude_mpm-4.12.4.dist-info → claude_mpm-4.13.1.dist-info}/WHEEL +0 -0
  27. {claude_mpm-4.12.4.dist-info → claude_mpm-4.13.1.dist-info}/entry_points.txt +0 -0
  28. {claude_mpm-4.12.4.dist-info → claude_mpm-4.13.1.dist-info}/licenses/LICENSE +0 -0
  29. {claude_mpm-4.12.4.dist-info → claude_mpm-4.13.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,572 @@
1
+ """
2
+ Auto-Configuration CLI Command for Claude MPM Framework
3
+ ========================================================
4
+
5
+ WHY: This module provides a user-friendly CLI interface for the auto-configuration
6
+ feature, allowing users to automatically configure agents based on detected toolchain.
7
+
8
+ DESIGN DECISION: Uses rich for beautiful terminal output, implements interactive
9
+ confirmation, and provides comprehensive error handling. Supports both interactive
10
+ and non-interactive modes for flexibility.
11
+
12
+ Part of TSK-0054: Auto-Configuration Feature - Phase 5
13
+ """
14
+
15
+ import json
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ try:
20
+ from rich.console import Console
21
+ from rich.panel import Panel
22
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
23
+ from rich.table import Table
24
+
25
+ RICH_AVAILABLE = True
26
+ except ImportError:
27
+ RICH_AVAILABLE = False
28
+
29
+ from ...services.agents.auto_config_manager import AutoConfigManagerService
30
+ from ...services.agents.observers import NullObserver
31
+ from ...services.core.models.agent_config import (
32
+ ConfigurationResult,
33
+ ConfigurationStatus,
34
+ )
35
+ from ..shared import BaseCommand, CommandResult
36
+
37
+
38
+ class RichProgressObserver(NullObserver):
39
+ """
40
+ Observer that displays deployment progress using Rich.
41
+
42
+ WHY: Extends NullObserver to inherit all required abstract method
43
+ implementations while overriding only the methods needed for
44
+ Rich console output.
45
+ """
46
+
47
+ def __init__(self, console: "Console"):
48
+ """Initialize the observer.
49
+
50
+ Args:
51
+ console: Rich console for output
52
+ """
53
+ self.console = console
54
+ self.progress = None
55
+ self.task_id = None
56
+
57
+ def on_agent_deployment_started(
58
+ self, agent_id: str, agent_name: str, index: int, total: int
59
+ ) -> None:
60
+ """Called when agent deployment starts."""
61
+ if not self.progress:
62
+ self.progress = Progress(
63
+ SpinnerColumn(),
64
+ TextColumn("[progress.description]{task.description}"),
65
+ BarColumn(),
66
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
67
+ console=self.console,
68
+ )
69
+ self.progress.start()
70
+
71
+ self.task_id = self.progress.add_task(f"Deploying {agent_name}...", total=100)
72
+
73
+ def on_agent_deployment_progress(
74
+ self, agent_id: str, progress: int, message: str = ""
75
+ ) -> None:
76
+ """Called when deployment makes progress."""
77
+ if self.progress and self.task_id is not None:
78
+ self.progress.update(self.task_id, completed=progress)
79
+
80
+ def on_agent_deployment_completed(
81
+ self, agent_id: str, agent_name: str, success: bool, error: str | None = None
82
+ ) -> None:
83
+ """Called when agent deployment completes."""
84
+ if self.progress and self.task_id is not None:
85
+ if success:
86
+ self.progress.update(self.task_id, completed=100)
87
+ self.console.print(f"✅ {agent_name} deployed successfully")
88
+ else:
89
+ error_msg = f": {error}" if error else ""
90
+ self.console.print(f"❌ {agent_name} deployment failed{error_msg}")
91
+
92
+ def on_deployment_completed(
93
+ self, success_count: int, failure_count: int, duration_ms: float
94
+ ) -> None:
95
+ """Called when all deployments complete."""
96
+ if self.progress:
97
+ self.progress.stop()
98
+
99
+
100
+ class AutoConfigureCommand(BaseCommand):
101
+ """
102
+ Handle auto-configuration CLI commands.
103
+
104
+ This command provides a user-friendly interface for automatically configuring
105
+ agents based on detected project toolchain.
106
+ """
107
+
108
+ def __init__(self):
109
+ """Initialize the auto-configure command."""
110
+ super().__init__("auto-configure")
111
+ self.console = Console() if RICH_AVAILABLE else None
112
+ self._auto_config_manager = None
113
+
114
+ @property
115
+ def auto_config_manager(self) -> AutoConfigManagerService:
116
+ """Get auto-configuration manager (lazy loaded)."""
117
+ if self._auto_config_manager is None:
118
+ from ...services.agents.auto_config_manager import (
119
+ AutoConfigManagerService,
120
+ )
121
+ from ...services.agents.recommender import AgentRecommenderService
122
+ from ...services.agents.registry import AgentRegistry
123
+ from ...services.project.toolchain_analyzer import (
124
+ ToolchainAnalyzerService,
125
+ )
126
+
127
+ # Initialize services with dependency injection
128
+ toolchain_analyzer = ToolchainAnalyzerService()
129
+ agent_registry = AgentRegistry()
130
+ agent_recommender = AgentRecommenderService()
131
+
132
+ # Get deployment service
133
+ try:
134
+ from ...services.agents.deployment import AgentDeploymentService
135
+
136
+ agent_deployment = AgentDeploymentService()
137
+ except ImportError:
138
+ agent_deployment = None
139
+
140
+ self._auto_config_manager = AutoConfigManagerService(
141
+ toolchain_analyzer=toolchain_analyzer,
142
+ agent_recommender=agent_recommender,
143
+ agent_registry=agent_registry,
144
+ agent_deployment=agent_deployment,
145
+ )
146
+
147
+ return self._auto_config_manager
148
+
149
+ def validate_args(self, args) -> Optional[str]:
150
+ """Validate command arguments."""
151
+ # Validate project path
152
+ project_path = (
153
+ Path(args.project_path)
154
+ if hasattr(args, "project_path") and args.project_path
155
+ else Path.cwd()
156
+ )
157
+ if not project_path.exists():
158
+ return f"Project path does not exist: {project_path}"
159
+
160
+ # Validate min_confidence range
161
+ if hasattr(args, "min_confidence") and args.min_confidence:
162
+ if not 0.0 <= args.min_confidence <= 1.0:
163
+ return "min_confidence must be between 0.0 and 1.0"
164
+
165
+ return None
166
+
167
+ def run(self, args) -> CommandResult:
168
+ """
169
+ Execute auto-configuration command.
170
+
171
+ Returns:
172
+ CommandResult with success status and exit code
173
+ """
174
+ try:
175
+ # Setup logging
176
+ self.setup_logging(args)
177
+
178
+ # Validate arguments
179
+ error = self.validate_args(args)
180
+ if error:
181
+ return CommandResult.error_result(error)
182
+
183
+ # Get configuration options
184
+ project_path = (
185
+ Path(args.project_path)
186
+ if hasattr(args, "project_path") and args.project_path
187
+ else Path.cwd()
188
+ )
189
+ min_confidence = (
190
+ args.min_confidence
191
+ if hasattr(args, "min_confidence") and args.min_confidence
192
+ else 0.8
193
+ )
194
+ dry_run = (
195
+ args.preview or args.dry_run if hasattr(args, "preview") else False
196
+ )
197
+ skip_confirmation = args.yes if hasattr(args, "yes") and args.yes else False
198
+ json_output = args.json if hasattr(args, "json") and args.json else False
199
+
200
+ # Run preview or full configuration
201
+ if dry_run or args.preview if hasattr(args, "preview") else False:
202
+ return self._run_preview(project_path, min_confidence, json_output)
203
+ return self._run_full_configuration(
204
+ project_path, min_confidence, skip_confirmation, json_output
205
+ )
206
+
207
+ except KeyboardInterrupt:
208
+ if self.console:
209
+ self.console.print("\n\n❌ Operation cancelled by user")
210
+ else:
211
+ print("\n\nOperation cancelled by user")
212
+ return CommandResult.error_result("Operation cancelled", exit_code=130)
213
+
214
+ except Exception as e:
215
+ self.logger.exception("Auto-configuration failed")
216
+ error_msg = f"Auto-configuration failed: {e!s}"
217
+ if self.console:
218
+ self.console.print(f"\n❌ {error_msg}")
219
+ else:
220
+ print(f"\n{error_msg}")
221
+ return CommandResult.error_result(error_msg)
222
+
223
+ def _run_preview(
224
+ self, project_path: Path, min_confidence: float, json_output: bool
225
+ ) -> CommandResult:
226
+ """Run configuration preview without deploying."""
227
+ # Show analysis spinner
228
+ if self.console and not json_output:
229
+ with self.console.status("[bold green]Analyzing project toolchain..."):
230
+ preview = self.auto_config_manager.preview_configuration(
231
+ project_path, min_confidence
232
+ )
233
+ else:
234
+ preview = self.auto_config_manager.preview_configuration(
235
+ project_path, min_confidence
236
+ )
237
+
238
+ # Output results
239
+ if json_output:
240
+ return self._output_preview_json(preview)
241
+ return self._display_preview(preview)
242
+
243
+ def _run_full_configuration(
244
+ self,
245
+ project_path: Path,
246
+ min_confidence: float,
247
+ skip_confirmation: bool,
248
+ json_output: bool,
249
+ ) -> CommandResult:
250
+ """Run full auto-configuration with deployment."""
251
+ # Get preview first
252
+ if self.console and not json_output:
253
+ with self.console.status("[bold green]Analyzing project toolchain..."):
254
+ preview = self.auto_config_manager.preview_configuration(
255
+ project_path, min_confidence
256
+ )
257
+ else:
258
+ preview = self.auto_config_manager.preview_configuration(
259
+ project_path, min_confidence
260
+ )
261
+
262
+ # Display preview (unless JSON output)
263
+ if not json_output:
264
+ self._display_preview(preview)
265
+
266
+ # Ask for confirmation (unless skipped)
267
+ if not skip_confirmation and not json_output:
268
+ if not self._confirm_deployment(preview):
269
+ if self.console:
270
+ self.console.print("\n❌ Operation cancelled by user")
271
+ else:
272
+ print("\nOperation cancelled by user")
273
+ return CommandResult.error_result("Operation cancelled", exit_code=0)
274
+
275
+ # Execute configuration
276
+ import asyncio
277
+
278
+ observer = RichProgressObserver(self.console) if self.console else None
279
+ result = asyncio.run(
280
+ self.auto_config_manager.auto_configure(
281
+ project_path,
282
+ confirmation_required=False, # Already confirmed above
283
+ dry_run=False,
284
+ min_confidence=min_confidence,
285
+ observer=observer,
286
+ )
287
+ )
288
+
289
+ # Output results
290
+ if json_output:
291
+ return self._output_result_json(result)
292
+ return self._display_result(result)
293
+
294
+ def _display_preview(self, preview) -> CommandResult:
295
+ """Display configuration preview with Rich formatting."""
296
+ if not self.console:
297
+ # Fallback to plain text
298
+ return self._display_preview_plain(preview)
299
+
300
+ # Display detected toolchain
301
+ self.console.print("\n📊 Detected Toolchain:", style="bold blue")
302
+ if preview.detected_toolchain and preview.detected_toolchain.components:
303
+ toolchain_table = Table(show_header=True, header_style="bold")
304
+ toolchain_table.add_column("Component", style="cyan")
305
+ toolchain_table.add_column("Version", style="yellow")
306
+ toolchain_table.add_column("Confidence", style="green")
307
+
308
+ for component in preview.detected_toolchain.components:
309
+ confidence_pct = int(component.confidence * 100)
310
+ bar = "█" * (confidence_pct // 10) + "░" * (10 - confidence_pct // 10)
311
+ confidence_str = f"{bar} {confidence_pct}%"
312
+
313
+ toolchain_table.add_row(
314
+ (
315
+ component.type.value
316
+ if hasattr(component.type, "value")
317
+ else str(component.type)
318
+ ),
319
+ component.version or "Unknown",
320
+ confidence_str,
321
+ )
322
+
323
+ self.console.print(toolchain_table)
324
+ else:
325
+ self.console.print(" No toolchain detected", style="yellow")
326
+
327
+ # Display recommended agents
328
+ self.console.print("\n🤖 Recommended Agents:", style="bold blue")
329
+ if preview.recommendations:
330
+ for rec in preview.recommendations:
331
+ confidence_pct = int(rec.confidence * 100)
332
+ icon = "✓" if rec.confidence >= 0.8 else "○"
333
+ self.console.print(
334
+ f" {icon} [bold]{rec.agent_id}[/bold] ({confidence_pct}% confidence)"
335
+ )
336
+ self.console.print(f" Reason: {rec.reasoning}", style="dim")
337
+ else:
338
+ self.console.print(" No agents recommended", style="yellow")
339
+
340
+ # Display validation issues
341
+ if preview.validation_result and preview.validation_result.issues:
342
+ self.console.print("\n⚠️ Validation Issues:", style="bold yellow")
343
+ for issue in preview.validation_result.issues:
344
+ severity_icon = {"error": "❌", "warning": "⚠️", "info": "ℹ️"}.get(
345
+ (
346
+ issue.severity.value
347
+ if hasattr(issue.severity, "value")
348
+ else str(issue.severity)
349
+ ),
350
+ "•",
351
+ )
352
+ self.console.print(f" {severity_icon} {issue.message}", style="yellow")
353
+
354
+ return CommandResult.success_result()
355
+
356
+ def _display_preview_plain(self, preview) -> CommandResult:
357
+ """Display preview in plain text (fallback when Rich not available)."""
358
+ print("\nDetected Toolchain:")
359
+ if preview.detected_toolchain and preview.detected_toolchain.components:
360
+ for component in preview.detected_toolchain.components:
361
+ confidence_pct = int(component.confidence * 100)
362
+ print(f" - {component.type}: {component.version} ({confidence_pct}%)")
363
+ else:
364
+ print(" No toolchain detected")
365
+
366
+ print("\nRecommended Agents:")
367
+ if preview.recommendations:
368
+ for rec in preview.recommendations:
369
+ confidence_pct = int(rec.confidence * 100)
370
+ print(f" - {rec.agent_id} ({confidence_pct}%)")
371
+ print(f" Reason: {rec.reasoning}")
372
+ else:
373
+ print(" No agents recommended")
374
+
375
+ if preview.validation_result and preview.validation_result.issues:
376
+ print("\nValidation Issues:")
377
+ for issue in preview.validation_result.issues:
378
+ print(f" - {issue.severity}: {issue.message}")
379
+
380
+ return CommandResult.success_result()
381
+
382
+ def _confirm_deployment(self, preview) -> bool:
383
+ """Ask user to confirm deployment."""
384
+ if not preview.recommendations:
385
+ return False
386
+
387
+ if self.console:
388
+ self.console.print("\n" + "=" * 60)
389
+ self.console.print("Deploy these agents?", style="bold yellow")
390
+ self.console.print("=" * 60)
391
+ response = (
392
+ self.console.input("\n[bold]Proceed? (y/n/s for select):[/bold] ")
393
+ .strip()
394
+ .lower()
395
+ )
396
+ else:
397
+ print("\n" + "=" * 60)
398
+ print("Deploy these agents?")
399
+ print("=" * 60)
400
+ response = input("\nProceed? (y/n/s for select): ").strip().lower()
401
+
402
+ if response in ["y", "yes"]:
403
+ return True
404
+ if response in ["s", "select"]:
405
+ # TODO: Implement interactive selection
406
+ if self.console:
407
+ self.console.print(
408
+ "\n⚠️ Interactive selection not yet implemented",
409
+ style="yellow",
410
+ )
411
+ else:
412
+ print("\nInteractive selection not yet implemented")
413
+ return False
414
+ return False
415
+
416
+ def _display_result(self, result: ConfigurationResult) -> CommandResult:
417
+ """Display configuration result."""
418
+ if not self.console:
419
+ return self._display_result_plain(result)
420
+
421
+ # Display summary
422
+ if result.status == ConfigurationStatus.SUCCESS:
423
+ panel = Panel(
424
+ f"✅ Auto-configuration completed successfully!\n\n"
425
+ f"Deployed {len(result.deployed_agents)} agent(s)",
426
+ title="Success",
427
+ border_style="green",
428
+ )
429
+ self.console.print(panel)
430
+
431
+ # Show deployed agents
432
+ if result.deployed_agents:
433
+ self.console.print("\n📦 Deployed Agents:", style="bold green")
434
+ for agent_id in result.deployed_agents:
435
+ self.console.print(f" ✓ {agent_id}")
436
+
437
+ return CommandResult.success_result()
438
+
439
+ if result.status == ConfigurationStatus.PARTIAL_SUCCESS:
440
+ panel = Panel(
441
+ f"⚠️ Auto-configuration partially completed\n\n"
442
+ f"Deployed: {len(result.deployed_agents)}\n"
443
+ f"Failed: {len(result.failed_agents)}",
444
+ title="Partial Success",
445
+ border_style="yellow",
446
+ )
447
+ self.console.print(panel)
448
+
449
+ if result.failed_agents:
450
+ self.console.print("\n❌ Failed Agents:", style="bold red")
451
+ for agent_id in result.failed_agents:
452
+ error = result.errors.get(agent_id, "Unknown error")
453
+ self.console.print(f" ✗ {agent_id}: {error}")
454
+
455
+ return CommandResult.error_result("Partial configuration", exit_code=1)
456
+
457
+ panel = Panel(
458
+ f"❌ Auto-configuration failed\n\n{result.errors.get('general', 'Unknown error')}",
459
+ title="Error",
460
+ border_style="red",
461
+ )
462
+ self.console.print(panel)
463
+
464
+ return CommandResult.error_result("Configuration failed", exit_code=1)
465
+
466
+ def _display_result_plain(self, result: ConfigurationResult) -> CommandResult:
467
+ """Display result in plain text (fallback)."""
468
+ if result.status == ConfigurationStatus.SUCCESS:
469
+ print("\n✅ Auto-configuration completed successfully!")
470
+ print(f"Deployed {len(result.deployed_agents)} agent(s)")
471
+
472
+ if result.deployed_agents:
473
+ print("\nDeployed Agents:")
474
+ for agent_id in result.deployed_agents:
475
+ print(f" - {agent_id}")
476
+
477
+ return CommandResult.success_result()
478
+
479
+ if result.status == ConfigurationStatus.PARTIAL_SUCCESS:
480
+ print("\n⚠️ Auto-configuration partially completed")
481
+ print(f"Deployed: {len(result.deployed_agents)}")
482
+ print(f"Failed: {len(result.failed_agents)}")
483
+
484
+ if result.failed_agents:
485
+ print("\nFailed Agents:")
486
+ for agent_id in result.failed_agents:
487
+ error = result.errors.get(agent_id, "Unknown error")
488
+ print(f" - {agent_id}: {error}")
489
+
490
+ return CommandResult.error_result("Partial configuration", exit_code=1)
491
+
492
+ print("\n❌ Auto-configuration failed")
493
+ print(result.errors.get("general", "Unknown error"))
494
+
495
+ return CommandResult.error_result("Configuration failed", exit_code=1)
496
+
497
+ def _output_preview_json(self, preview) -> CommandResult:
498
+ """Output preview as JSON."""
499
+ output = {
500
+ "detected_toolchain": {
501
+ "components": (
502
+ [
503
+ {
504
+ "type": (
505
+ c.type.value
506
+ if hasattr(c.type, "value")
507
+ else str(c.type)
508
+ ),
509
+ "version": c.version,
510
+ "confidence": c.confidence,
511
+ }
512
+ for c in preview.detected_toolchain.components
513
+ ]
514
+ if preview.detected_toolchain
515
+ else []
516
+ )
517
+ },
518
+ "recommendations": [
519
+ {
520
+ "agent_id": r.agent_id,
521
+ "confidence": r.confidence,
522
+ "reasoning": r.reasoning,
523
+ }
524
+ for r in preview.recommendations
525
+ ],
526
+ "validation": {
527
+ "is_valid": (
528
+ preview.validation_result.is_valid
529
+ if preview.validation_result
530
+ else True
531
+ ),
532
+ "issues": (
533
+ [
534
+ {
535
+ "severity": (
536
+ i.severity.value
537
+ if hasattr(i.severity, "value")
538
+ else str(i.severity)
539
+ ),
540
+ "message": i.message,
541
+ }
542
+ for i in preview.validation_result.issues
543
+ ]
544
+ if preview.validation_result
545
+ else []
546
+ ),
547
+ },
548
+ }
549
+
550
+ print(json.dumps(output, indent=2))
551
+ return CommandResult.success_result(data=output)
552
+
553
+ def _output_result_json(self, result: ConfigurationResult) -> CommandResult:
554
+ """Output result as JSON."""
555
+ output = {
556
+ "status": (
557
+ result.status.value
558
+ if hasattr(result.status, "value")
559
+ else str(result.status)
560
+ ),
561
+ "deployed_agents": result.deployed_agents,
562
+ "failed_agents": result.failed_agents,
563
+ "errors": result.errors,
564
+ }
565
+
566
+ print(json.dumps(output, indent=2))
567
+
568
+ if result.status == ConfigurationStatus.SUCCESS:
569
+ return CommandResult.success_result(data=output)
570
+ return CommandResult.error_result(
571
+ "Configuration failed or partial", exit_code=1, data=output
572
+ )
@@ -266,4 +266,13 @@ def add_agents_subparser(subparsers) -> argparse.ArgumentParser:
266
266
  help="Only show summary, not individual agents",
267
267
  )
268
268
 
269
+ # Auto-configuration commands (TSK-0054 Phase 5)
270
+ from .auto_configure_parser import (
271
+ add_agents_detect_subparser,
272
+ add_agents_recommend_subparser,
273
+ )
274
+
275
+ add_agents_detect_subparser(agents_subparsers)
276
+ add_agents_recommend_subparser(agents_subparsers)
277
+
269
278
  return agents_parser