claude-mpm 3.5.6__py3-none-any.whl → 3.7.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.
Files changed (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  3. claude_mpm/agents/BASE_PM.md +273 -0
  4. claude_mpm/agents/INSTRUCTIONS.md +114 -103
  5. claude_mpm/agents/agent_loader.py +36 -1
  6. claude_mpm/agents/async_agent_loader.py +421 -0
  7. claude_mpm/agents/templates/code_analyzer.json +81 -0
  8. claude_mpm/agents/templates/data_engineer.json +18 -3
  9. claude_mpm/agents/templates/documentation.json +18 -3
  10. claude_mpm/agents/templates/engineer.json +19 -4
  11. claude_mpm/agents/templates/ops.json +18 -3
  12. claude_mpm/agents/templates/qa.json +20 -4
  13. claude_mpm/agents/templates/research.json +20 -4
  14. claude_mpm/agents/templates/security.json +18 -3
  15. claude_mpm/agents/templates/version_control.json +16 -3
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/__init__.py +5 -1
  18. claude_mpm/cli/commands/agents.py +212 -3
  19. claude_mpm/cli/commands/aggregate.py +462 -0
  20. claude_mpm/cli/commands/config.py +277 -0
  21. claude_mpm/cli/commands/run.py +224 -36
  22. claude_mpm/cli/parser.py +176 -1
  23. claude_mpm/constants.py +19 -0
  24. claude_mpm/core/claude_runner.py +320 -44
  25. claude_mpm/core/config.py +161 -4
  26. claude_mpm/core/framework_loader.py +81 -0
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  28. claude_mpm/init.py +40 -5
  29. claude_mpm/models/agent_session.py +511 -0
  30. claude_mpm/scripts/__init__.py +15 -0
  31. claude_mpm/scripts/start_activity_logging.py +86 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
  33. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  34. claude_mpm/services/event_aggregator.py +547 -0
  35. claude_mpm/utils/agent_dependency_loader.py +655 -0
  36. claude_mpm/utils/console.py +11 -0
  37. claude_mpm/utils/dependency_cache.py +376 -0
  38. claude_mpm/utils/dependency_strategies.py +343 -0
  39. claude_mpm/utils/environment_context.py +310 -0
  40. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/METADATA +47 -3
  41. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/RECORD +45 -31
  42. claude_mpm/agents/templates/pm.json +0 -122
  43. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/WHEEL +0 -0
  44. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,655 @@
1
+ """
2
+ Dynamic agent dependency loader for runtime dependency management.
3
+
4
+ This module handles loading and checking dependencies for deployed agents
5
+ at runtime, rather than requiring all possible agent dependencies to be
6
+ installed upfront.
7
+ """
8
+
9
+ import json
10
+ import subprocess
11
+ import sys
12
+ import hashlib
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Dict, List, Set, Tuple, Optional
16
+ import logging
17
+ from packaging.requirements import Requirement, InvalidRequirement
18
+
19
+ from ..core.logger import get_logger
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ class AgentDependencyLoader:
25
+ """
26
+ Dynamically loads and manages dependencies for deployed agents.
27
+
28
+ Only checks/installs dependencies for agents that are actually deployed
29
+ and being used, rather than all possible agents.
30
+ """
31
+
32
+ def __init__(self, auto_install: bool = False):
33
+ """
34
+ Initialize the agent dependency loader.
35
+
36
+ Args:
37
+ auto_install: If True, automatically install missing dependencies.
38
+ If False, only check and report missing dependencies.
39
+ """
40
+ self.auto_install = auto_install
41
+ self.deployed_agents: Dict[str, Path] = {}
42
+ self.agent_dependencies: Dict[str, Dict] = {}
43
+ self.missing_dependencies: Dict[str, List[str]] = {}
44
+ self.checked_packages: Set[str] = set()
45
+ self.deployment_state_file = Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state"
46
+
47
+ def discover_deployed_agents(self) -> Dict[str, Path]:
48
+ """
49
+ Discover which agents are currently deployed in .claude/agents/
50
+
51
+ Returns:
52
+ Dictionary mapping agent IDs to their file paths
53
+ """
54
+ deployed_agents = {}
55
+ claude_agents_dir = Path.cwd() / ".claude" / "agents"
56
+
57
+ if not claude_agents_dir.exists():
58
+ logger.debug("No .claude/agents directory found")
59
+ return deployed_agents
60
+
61
+ # Scan for deployed agent markdown files
62
+ for agent_file in claude_agents_dir.glob("*.md"):
63
+ agent_id = agent_file.stem
64
+ deployed_agents[agent_id] = agent_file
65
+ logger.debug(f"Found deployed agent: {agent_id}")
66
+
67
+ logger.info(f"Discovered {len(deployed_agents)} deployed agents")
68
+ self.deployed_agents = deployed_agents
69
+ return deployed_agents
70
+
71
+ def load_agent_dependencies(self) -> Dict[str, Dict]:
72
+ """
73
+ Load dependency information for deployed agents from their source configs.
74
+
75
+ Returns:
76
+ Dictionary mapping agent IDs to their dependency requirements
77
+ """
78
+ agent_dependencies = {}
79
+
80
+ # Define paths to check for agent configs (in precedence order)
81
+ config_paths = [
82
+ Path.cwd() / ".claude-mpm" / "agents", # PROJECT
83
+ Path.home() / ".claude-mpm" / "agents", # USER
84
+ Path.cwd() / "src" / "claude_mpm" / "agents" / "templates" # SYSTEM
85
+ ]
86
+
87
+ for agent_id in self.deployed_agents:
88
+ # Try to find the agent's JSON config
89
+ for config_dir in config_paths:
90
+ config_file = config_dir / f"{agent_id}.json"
91
+ if config_file.exists():
92
+ try:
93
+ with open(config_file, 'r') as f:
94
+ config = json.load(f)
95
+ if 'dependencies' in config:
96
+ agent_dependencies[agent_id] = config['dependencies']
97
+ logger.debug(f"Loaded dependencies for {agent_id}")
98
+ break
99
+ except Exception as e:
100
+ logger.warning(f"Failed to load config for {agent_id}: {e}")
101
+
102
+ self.agent_dependencies = agent_dependencies
103
+ logger.info(f"Loaded dependencies for {len(agent_dependencies)} agents")
104
+ return agent_dependencies
105
+
106
+ def check_python_dependency(self, package_spec: str) -> Tuple[bool, Optional[str]]:
107
+ """
108
+ Check if a Python package dependency is satisfied.
109
+
110
+ Args:
111
+ package_spec: Package specification (e.g., "pandas>=2.0.0")
112
+
113
+ Returns:
114
+ Tuple of (is_satisfied, installed_version)
115
+ """
116
+ try:
117
+ req = Requirement(package_spec)
118
+ package_name = req.name
119
+
120
+ # Skip if already checked
121
+ if package_name in self.checked_packages:
122
+ return True, None
123
+
124
+ # Try to import and check version
125
+ try:
126
+ import importlib.metadata
127
+ try:
128
+ version = importlib.metadata.version(package_name)
129
+ self.checked_packages.add(package_name)
130
+
131
+ # Check if version satisfies requirement
132
+ if req.specifier.contains(version):
133
+ return True, version
134
+ else:
135
+ logger.debug(f"{package_name} {version} does not satisfy {req.specifier}")
136
+ return False, version
137
+
138
+ except importlib.metadata.PackageNotFoundError:
139
+ return False, None
140
+
141
+ except ImportError:
142
+ # Fallback for older Python versions
143
+ try:
144
+ import pkg_resources
145
+ version = pkg_resources.get_distribution(package_name).version
146
+ self.checked_packages.add(package_name)
147
+
148
+ if req.specifier.contains(version):
149
+ return True, version
150
+ else:
151
+ return False, version
152
+
153
+ except pkg_resources.DistributionNotFound:
154
+ return False, None
155
+
156
+ except InvalidRequirement as e:
157
+ logger.warning(f"Invalid requirement specification: {package_spec}: {e}")
158
+ return False, None
159
+
160
+ def check_system_dependency(self, command: str) -> bool:
161
+ """
162
+ Check if a system command is available in PATH.
163
+
164
+ Args:
165
+ command: System command to check (e.g., "git")
166
+
167
+ Returns:
168
+ True if command is available, False otherwise
169
+ """
170
+ try:
171
+ result = subprocess.run(
172
+ ["which", command],
173
+ capture_output=True,
174
+ text=True,
175
+ timeout=5
176
+ )
177
+ return result.returncode == 0
178
+ except Exception:
179
+ return False
180
+
181
+ def analyze_dependencies(self) -> Dict[str, Dict]:
182
+ """
183
+ Analyze dependencies for all deployed agents.
184
+
185
+ Returns:
186
+ Analysis results including missing and satisfied dependencies
187
+ """
188
+ results = {
189
+ 'agents': {},
190
+ 'summary': {
191
+ 'total_agents': len(self.deployed_agents),
192
+ 'agents_with_deps': 0,
193
+ 'missing_python': [],
194
+ 'missing_system': [],
195
+ 'satisfied_python': [],
196
+ 'satisfied_system': []
197
+ }
198
+ }
199
+
200
+ for agent_id, deps in self.agent_dependencies.items():
201
+ agent_result = {
202
+ 'python': {'satisfied': [], 'missing': [], 'outdated': []},
203
+ 'system': {'satisfied': [], 'missing': []}
204
+ }
205
+
206
+ # Check Python dependencies
207
+ if 'python' in deps:
208
+ for dep_spec in deps['python']:
209
+ is_satisfied, version = self.check_python_dependency(dep_spec)
210
+ if is_satisfied:
211
+ agent_result['python']['satisfied'].append(dep_spec)
212
+ if dep_spec not in results['summary']['satisfied_python']:
213
+ results['summary']['satisfied_python'].append(dep_spec)
214
+ else:
215
+ if version: # Installed but wrong version
216
+ agent_result['python']['outdated'].append(f"{dep_spec} (have {version})")
217
+ else: # Not installed
218
+ agent_result['python']['missing'].append(dep_spec)
219
+ if dep_spec not in results['summary']['missing_python']:
220
+ results['summary']['missing_python'].append(dep_spec)
221
+
222
+ # Check system dependencies
223
+ if 'system' in deps:
224
+ for command in deps['system']:
225
+ if self.check_system_dependency(command):
226
+ agent_result['system']['satisfied'].append(command)
227
+ if command not in results['summary']['satisfied_system']:
228
+ results['summary']['satisfied_system'].append(command)
229
+ else:
230
+ agent_result['system']['missing'].append(command)
231
+ if command not in results['summary']['missing_system']:
232
+ results['summary']['missing_system'].append(command)
233
+
234
+ results['agents'][agent_id] = agent_result
235
+ if 'python' in deps or 'system' in deps:
236
+ results['summary']['agents_with_deps'] += 1
237
+
238
+ return results
239
+
240
+ def check_python_compatibility(self, dependencies: List[str]) -> Tuple[List[str], List[str]]:
241
+ """
242
+ Check which dependencies are compatible with current Python version.
243
+
244
+ Args:
245
+ dependencies: List of package specifications to check
246
+
247
+ Returns:
248
+ Tuple of (compatible_deps, incompatible_deps)
249
+ """
250
+ import sys
251
+ current_version = f"{sys.version_info.major}.{sys.version_info.minor}"
252
+ compatible = []
253
+ incompatible = []
254
+
255
+ for dep in dependencies:
256
+ try:
257
+ # For known problematic packages in Python 3.13
258
+ req = Requirement(dep)
259
+ package_name = req.name.lower()
260
+
261
+ # Known Python 3.13 incompatibilities
262
+ if sys.version_info >= (3, 13):
263
+ if package_name in ['ydata-profiling', 'pandas-profiling']:
264
+ incompatible.append(f"{dep} (requires Python <3.13)")
265
+ continue
266
+ elif package_name == 'apache-airflow':
267
+ incompatible.append(f"{dep} (requires Python <3.13)")
268
+ continue
269
+
270
+ # Default to compatible if we don't know
271
+ compatible.append(dep)
272
+
273
+ except Exception as e:
274
+ logger.warning(f"Could not check compatibility for {dep}: {e}")
275
+ compatible.append(dep) # Assume compatible if we can't check
276
+
277
+ return compatible, incompatible
278
+
279
+ def install_missing_dependencies(self, dependencies: List[str]) -> Tuple[bool, str]:
280
+ """
281
+ Install missing Python dependencies.
282
+
283
+ Args:
284
+ dependencies: List of package specifications to install
285
+
286
+ Returns:
287
+ Tuple of (success, error_message)
288
+ """
289
+ if not dependencies:
290
+ return True, ""
291
+
292
+ # Check Python version compatibility first
293
+ compatible, incompatible = self.check_python_compatibility(dependencies)
294
+
295
+ if incompatible:
296
+ logger.warning(f"Skipping {len(incompatible)} incompatible packages:")
297
+ for dep in incompatible:
298
+ logger.warning(f" - {dep}")
299
+
300
+ if not compatible:
301
+ return True, "No compatible packages to install"
302
+
303
+ try:
304
+ cmd = [sys.executable, "-m", "pip", "install"] + compatible
305
+ logger.info(f"Installing {len(compatible)} compatible dependencies...")
306
+ if incompatible:
307
+ logger.info(f"(Skipping {len(incompatible)} incompatible with Python {sys.version_info.major}.{sys.version_info.minor})")
308
+
309
+ result = subprocess.run(
310
+ cmd,
311
+ capture_output=True,
312
+ text=True,
313
+ timeout=300
314
+ )
315
+
316
+ if result.returncode == 0:
317
+ logger.info("Successfully installed compatible dependencies")
318
+ if incompatible:
319
+ return True, f"Installed {len(compatible)} packages, skipped {len(incompatible)} incompatible"
320
+ return True, ""
321
+ else:
322
+ error_msg = f"Installation failed: {result.stderr}"
323
+ logger.error(error_msg)
324
+ return False, error_msg
325
+
326
+ except Exception as e:
327
+ error_msg = f"Failed to install dependencies: {e}"
328
+ logger.error(error_msg)
329
+ return False, error_msg
330
+
331
+ def load_and_check(self) -> Dict[str, Dict]:
332
+ """
333
+ Complete workflow: discover agents, load dependencies, and check them.
334
+
335
+ Returns:
336
+ Complete analysis results
337
+ """
338
+ # Discover deployed agents
339
+ self.discover_deployed_agents()
340
+
341
+ if not self.deployed_agents:
342
+ logger.info("No deployed agents found")
343
+ return {'agents': {}, 'summary': {'total_agents': 0}}
344
+
345
+ # Load their dependencies
346
+ self.load_agent_dependencies()
347
+
348
+ # Analyze what's missing
349
+ results = self.analyze_dependencies()
350
+
351
+ # Optionally auto-install missing dependencies
352
+ if self.auto_install and results['summary']['missing_python']:
353
+ logger.info(f"Auto-installing {len(results['summary']['missing_python'])} missing dependencies...")
354
+ success, error = self.install_missing_dependencies(results['summary']['missing_python'])
355
+ if success:
356
+ # Re-analyze after installation
357
+ self.checked_packages.clear()
358
+ results = self.analyze_dependencies()
359
+
360
+ return results
361
+
362
+ def format_report(self, results: Dict[str, Dict]) -> str:
363
+ """
364
+ Format a human-readable dependency report.
365
+
366
+ Args:
367
+ results: Analysis results from analyze_dependencies()
368
+
369
+ Returns:
370
+ Formatted report string
371
+ """
372
+ import sys
373
+ lines = []
374
+ lines.append("=" * 80)
375
+ lines.append("AGENT DEPENDENCY ANALYSIS REPORT")
376
+ lines.append("=" * 80)
377
+ lines.append("")
378
+
379
+ # Python version info
380
+ lines.append(f"Python Version: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")
381
+ lines.append("")
382
+
383
+ # Summary
384
+ summary = results['summary']
385
+ lines.append(f"Deployed Agents: {summary['total_agents']}")
386
+ lines.append(f"Agents with Dependencies: {summary['agents_with_deps']}")
387
+ lines.append("")
388
+
389
+ # Missing dependencies summary
390
+ if summary['missing_python'] or summary['missing_system']:
391
+ lines.append("⚠️ MISSING DEPENDENCIES:")
392
+ if summary['missing_python']:
393
+ lines.append(f" Python packages: {len(summary['missing_python'])}")
394
+ for dep in summary['missing_python'][:5]: # Show first 5
395
+ lines.append(f" - {dep}")
396
+ if len(summary['missing_python']) > 5:
397
+ lines.append(f" ... and {len(summary['missing_python']) - 5} more")
398
+
399
+ if summary['missing_system']:
400
+ lines.append(f" System commands: {len(summary['missing_system'])}")
401
+ for cmd in summary['missing_system']:
402
+ lines.append(f" - {cmd}")
403
+ lines.append("")
404
+
405
+ # Per-agent details (only for agents with issues)
406
+ agents_with_issues = {
407
+ agent_id: info for agent_id, info in results['agents'].items()
408
+ if info['python']['missing'] or info['python']['outdated'] or info['system']['missing']
409
+ }
410
+
411
+ if agents_with_issues:
412
+ lines.append("AGENT-SPECIFIC ISSUES:")
413
+ lines.append("-" * 40)
414
+ for agent_id, info in agents_with_issues.items():
415
+ lines.append(f"\n📦 {agent_id}:")
416
+
417
+ if info['python']['missing']:
418
+ lines.append(f" Missing Python: {', '.join(info['python']['missing'])}")
419
+ if info['python']['outdated']:
420
+ lines.append(f" Outdated Python: {', '.join(info['python']['outdated'])}")
421
+ if info['system']['missing']:
422
+ lines.append(f" Missing System: {', '.join(info['system']['missing'])}")
423
+
424
+ else:
425
+ lines.append("✅ All agent dependencies are satisfied!")
426
+
427
+ # Installation instructions
428
+ if summary['missing_python']:
429
+ lines.append("")
430
+ lines.append("TO INSTALL MISSING PYTHON DEPENDENCIES:")
431
+ lines.append("-" * 40)
432
+
433
+ # Check for Python 3.13 compatibility issues
434
+ import sys
435
+ if sys.version_info >= (3, 13):
436
+ compatible, incompatible = self.check_python_compatibility(summary['missing_python'])
437
+ if incompatible:
438
+ lines.append("⚠️ Python 3.13 Compatibility Warning:")
439
+ lines.append(f" {len(incompatible)} packages are not yet compatible with Python 3.13:")
440
+ for dep in incompatible[:3]:
441
+ lines.append(f" - {dep}")
442
+ if len(incompatible) > 3:
443
+ lines.append(f" ... and {len(incompatible) - 3} more")
444
+ lines.append("")
445
+ lines.append(" Consider using Python 3.12 or earlier for full compatibility.")
446
+ lines.append("")
447
+
448
+ lines.append("Option 1: Install all agent dependencies:")
449
+ lines.append(' pip install "claude-mpm[agents]"')
450
+ lines.append("")
451
+ lines.append("Option 2: Install only what's needed:")
452
+ deps_str = ' '.join(f'"{dep}"' for dep in summary['missing_python'][:3])
453
+ lines.append(f" pip install {deps_str}")
454
+ if len(summary['missing_python']) > 3:
455
+ lines.append(f" # ... and {len(summary['missing_python']) - 3} more")
456
+
457
+ if summary['missing_system']:
458
+ lines.append("")
459
+ lines.append("TO INSTALL MISSING SYSTEM DEPENDENCIES:")
460
+ lines.append("-" * 40)
461
+ lines.append("Use your system package manager:")
462
+ lines.append(" # macOS: brew install " + ' '.join(summary['missing_system']))
463
+ lines.append(" # Ubuntu: apt-get install " + ' '.join(summary['missing_system']))
464
+
465
+ lines.append("")
466
+ lines.append("=" * 80)
467
+
468
+ return '\n'.join(lines)
469
+
470
+ def calculate_deployment_hash(self) -> str:
471
+ """
472
+ Calculate a hash of the current agent deployment state.
473
+
474
+ WHY: We use SHA256 hash of agent files to detect when agents have changed.
475
+ This allows us to skip dependency checks when nothing has changed,
476
+ improving startup performance.
477
+
478
+ Returns:
479
+ SHA256 hash of all deployed agent files and their content.
480
+ """
481
+ hash_obj = hashlib.sha256()
482
+
483
+ # Discover current agents if not already done
484
+ if not self.deployed_agents:
485
+ self.discover_deployed_agents()
486
+
487
+ # Sort agent IDs for consistent hashing
488
+ for agent_id in sorted(self.deployed_agents.keys()):
489
+ agent_path = self.deployed_agents[agent_id]
490
+
491
+ # Include agent ID in hash
492
+ hash_obj.update(agent_id.encode('utf-8'))
493
+
494
+ # Include file modification time and size for quick change detection
495
+ try:
496
+ stat = agent_path.stat()
497
+ hash_obj.update(str(stat.st_mtime).encode('utf-8'))
498
+ hash_obj.update(str(stat.st_size).encode('utf-8'))
499
+
500
+ # Include file content for comprehensive change detection
501
+ with open(agent_path, 'rb') as f:
502
+ hash_obj.update(f.read())
503
+ except Exception as e:
504
+ logger.debug(f"Could not hash agent file {agent_path}: {e}")
505
+ # Include error in hash to force recheck on next run
506
+ hash_obj.update(f"error:{agent_id}:{e}".encode('utf-8'))
507
+
508
+ return hash_obj.hexdigest()
509
+
510
+ def load_deployment_state(self) -> Dict:
511
+ """
512
+ Load the saved deployment state.
513
+
514
+ Returns:
515
+ Dictionary with deployment state or empty dict if not found.
516
+ """
517
+ if not self.deployment_state_file.exists():
518
+ return {}
519
+
520
+ try:
521
+ with open(self.deployment_state_file, 'r') as f:
522
+ return json.load(f)
523
+ except Exception as e:
524
+ logger.debug(f"Could not load deployment state: {e}")
525
+ return {}
526
+
527
+ def save_deployment_state(self, state: Dict) -> None:
528
+ """
529
+ Save the deployment state to disk.
530
+
531
+ Args:
532
+ state: Deployment state dictionary to save.
533
+ """
534
+ try:
535
+ # Ensure directory exists
536
+ self.deployment_state_file.parent.mkdir(parents=True, exist_ok=True)
537
+
538
+ with open(self.deployment_state_file, 'w') as f:
539
+ json.dump(state, f, indent=2)
540
+ except Exception as e:
541
+ logger.debug(f"Could not save deployment state: {e}")
542
+
543
+ def has_agents_changed(self) -> Tuple[bool, str]:
544
+ """
545
+ Check if agents have changed since last dependency check.
546
+
547
+ WHY: This is the core of our smart checking system. We only want to
548
+ check dependencies when agents have actually changed, not on every run.
549
+
550
+ Returns:
551
+ Tuple of (has_changed, current_hash)
552
+ """
553
+ current_hash = self.calculate_deployment_hash()
554
+ state = self.load_deployment_state()
555
+
556
+ last_hash = state.get('deployment_hash')
557
+ last_check_time = state.get('last_check_time', 0)
558
+
559
+ # Check if hash has changed
560
+ if last_hash != current_hash:
561
+ logger.info("Agent deployment has changed since last check")
562
+ return True, current_hash
563
+
564
+ # Also check if it's been more than 24 hours (optional staleness check)
565
+ current_time = time.time()
566
+ if current_time - last_check_time > 86400: # 24 hours
567
+ logger.debug("Over 24 hours since last dependency check")
568
+ return True, current_hash
569
+
570
+ logger.debug("No agent changes detected, skipping dependency check")
571
+ return False, current_hash
572
+
573
+ def mark_deployment_checked(self, deployment_hash: str, check_results: Dict) -> None:
574
+ """
575
+ Mark the current deployment as checked.
576
+
577
+ Args:
578
+ deployment_hash: Hash of the current deployment
579
+ check_results: Results of the dependency check
580
+ """
581
+ state = {
582
+ 'deployment_hash': deployment_hash,
583
+ 'last_check_time': time.time(),
584
+ 'last_check_results': check_results,
585
+ 'agent_count': len(self.deployed_agents)
586
+ }
587
+ self.save_deployment_state(state)
588
+
589
+ def get_cached_check_results(self) -> Optional[Dict]:
590
+ """
591
+ Get cached dependency check results if still valid.
592
+
593
+ Returns:
594
+ Cached results or None if not available/valid.
595
+ """
596
+ has_changed, current_hash = self.has_agents_changed()
597
+
598
+ if not has_changed:
599
+ state = self.load_deployment_state()
600
+ cached_results = state.get('last_check_results')
601
+ if cached_results:
602
+ logger.debug("Using cached dependency check results")
603
+ return cached_results
604
+
605
+ return None
606
+
607
+
608
+ def check_deployed_agent_dependencies(auto_install: bool = False, verbose: bool = False) -> None:
609
+ """
610
+ Check dependencies for currently deployed agents.
611
+
612
+ Args:
613
+ auto_install: If True, automatically install missing Python dependencies
614
+ verbose: If True, enable verbose logging
615
+ """
616
+ if verbose:
617
+ logging.getLogger().setLevel(logging.DEBUG)
618
+
619
+ loader = AgentDependencyLoader(auto_install=auto_install)
620
+ results = loader.load_and_check()
621
+
622
+ # Print report
623
+ report = loader.format_report(results)
624
+ print(report)
625
+
626
+ # Return status code based on missing dependencies
627
+ if results['summary']['missing_python'] or results['summary']['missing_system']:
628
+ return 1 # Missing dependencies
629
+ return 0 # All satisfied
630
+
631
+
632
+ if __name__ == "__main__":
633
+ import argparse
634
+
635
+ parser = argparse.ArgumentParser(
636
+ description="Check and manage dependencies for deployed agents"
637
+ )
638
+ parser.add_argument(
639
+ "--auto-install",
640
+ action="store_true",
641
+ help="Automatically install missing Python dependencies"
642
+ )
643
+ parser.add_argument(
644
+ "--verbose",
645
+ action="store_true",
646
+ help="Enable verbose logging"
647
+ )
648
+
649
+ args = parser.parse_args()
650
+
651
+ exit_code = check_deployed_agent_dependencies(
652
+ auto_install=args.auto_install,
653
+ verbose=args.verbose
654
+ )
655
+ sys.exit(exit_code)
@@ -0,0 +1,11 @@
1
+ """
2
+ Console utilities for claude-mpm CLI.
3
+
4
+ WHY: Provides consistent console output formatting across CLI commands.
5
+ Uses rich for enhanced formatting and color support.
6
+ """
7
+
8
+ from rich.console import Console
9
+
10
+ # Global console instance for consistent formatting
11
+ console = Console()