claude-mpm 4.13.2__py3-none-any.whl → 4.18.2__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 (250) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +286 -0
  3. claude_mpm/agents/BASE_PM.md +48 -17
  4. claude_mpm/agents/OUTPUT_STYLE.md +329 -11
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +227 -8
  6. claude_mpm/agents/agent_loader.py +17 -5
  7. claude_mpm/agents/frontmatter_validator.py +284 -253
  8. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  9. claude_mpm/agents/templates/api_qa.json +7 -1
  10. claude_mpm/agents/templates/clerk-ops.json +8 -1
  11. claude_mpm/agents/templates/code_analyzer.json +4 -1
  12. claude_mpm/agents/templates/dart_engineer.json +11 -1
  13. claude_mpm/agents/templates/data_engineer.json +11 -1
  14. claude_mpm/agents/templates/documentation.json +6 -1
  15. claude_mpm/agents/templates/engineer.json +18 -1
  16. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  17. claude_mpm/agents/templates/golang_engineer.json +11 -1
  18. claude_mpm/agents/templates/java_engineer.json +12 -2
  19. claude_mpm/agents/templates/local_ops_agent.json +1217 -6
  20. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  21. claude_mpm/agents/templates/ops.json +8 -1
  22. claude_mpm/agents/templates/php-engineer.json +11 -1
  23. claude_mpm/agents/templates/project_organizer.json +10 -3
  24. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  25. claude_mpm/agents/templates/python_engineer.json +11 -1
  26. claude_mpm/agents/templates/qa.json +7 -1
  27. claude_mpm/agents/templates/react_engineer.json +11 -1
  28. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  29. claude_mpm/agents/templates/research.json +4 -1
  30. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  31. claude_mpm/agents/templates/rust_engineer.json +11 -1
  32. claude_mpm/agents/templates/security.json +6 -1
  33. claude_mpm/agents/templates/svelte-engineer.json +225 -0
  34. claude_mpm/agents/templates/ticketing.json +6 -1
  35. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  36. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  37. claude_mpm/agents/templates/version_control.json +8 -1
  38. claude_mpm/agents/templates/web_qa.json +7 -1
  39. claude_mpm/agents/templates/web_ui.json +11 -1
  40. claude_mpm/cli/__init__.py +34 -706
  41. claude_mpm/cli/commands/agent_manager.py +25 -12
  42. claude_mpm/cli/commands/agent_state_manager.py +186 -0
  43. claude_mpm/cli/commands/agents.py +204 -148
  44. claude_mpm/cli/commands/aggregate.py +7 -3
  45. claude_mpm/cli/commands/analyze.py +9 -4
  46. claude_mpm/cli/commands/analyze_code.py +7 -2
  47. claude_mpm/cli/commands/auto_configure.py +7 -9
  48. claude_mpm/cli/commands/config.py +47 -13
  49. claude_mpm/cli/commands/configure.py +294 -1788
  50. claude_mpm/cli/commands/configure_agent_display.py +261 -0
  51. claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
  52. claude_mpm/cli/commands/configure_hook_manager.py +225 -0
  53. claude_mpm/cli/commands/configure_models.py +18 -0
  54. claude_mpm/cli/commands/configure_navigation.py +167 -0
  55. claude_mpm/cli/commands/configure_paths.py +104 -0
  56. claude_mpm/cli/commands/configure_persistence.py +254 -0
  57. claude_mpm/cli/commands/configure_startup_manager.py +646 -0
  58. claude_mpm/cli/commands/configure_template_editor.py +497 -0
  59. claude_mpm/cli/commands/configure_validators.py +73 -0
  60. claude_mpm/cli/commands/local_deploy.py +537 -0
  61. claude_mpm/cli/commands/memory.py +54 -20
  62. claude_mpm/cli/commands/mpm_init.py +39 -25
  63. claude_mpm/cli/commands/mpm_init_handler.py +8 -3
  64. claude_mpm/cli/executor.py +202 -0
  65. claude_mpm/cli/helpers.py +105 -0
  66. claude_mpm/cli/interactive/__init__.py +3 -0
  67. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  68. claude_mpm/cli/parsers/__init__.py +7 -1
  69. claude_mpm/cli/parsers/base_parser.py +98 -3
  70. claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
  71. claude_mpm/cli/shared/output_formatters.py +28 -19
  72. claude_mpm/cli/startup.py +481 -0
  73. claude_mpm/cli/utils.py +52 -1
  74. claude_mpm/commands/mpm-help.md +3 -0
  75. claude_mpm/commands/mpm-version.md +113 -0
  76. claude_mpm/commands/mpm.md +1 -0
  77. claude_mpm/config/agent_config.py +2 -2
  78. claude_mpm/config/model_config.py +428 -0
  79. claude_mpm/core/base_service.py +13 -12
  80. claude_mpm/core/enums.py +452 -0
  81. claude_mpm/core/factories.py +1 -1
  82. claude_mpm/core/instruction_reinforcement_hook.py +2 -1
  83. claude_mpm/core/interactive_session.py +9 -3
  84. claude_mpm/core/logging_config.py +6 -2
  85. claude_mpm/core/oneshot_session.py +8 -4
  86. claude_mpm/core/optimized_agent_loader.py +3 -3
  87. claude_mpm/core/output_style_manager.py +12 -192
  88. claude_mpm/core/service_registry.py +5 -1
  89. claude_mpm/core/types.py +2 -9
  90. claude_mpm/core/typing_utils.py +7 -6
  91. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  92. claude_mpm/dashboard/templates/index.html +3 -41
  93. claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
  94. claude_mpm/hooks/instruction_reinforcement.py +7 -2
  95. claude_mpm/models/resume_log.py +340 -0
  96. claude_mpm/services/agents/auto_config_manager.py +10 -11
  97. claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
  98. claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
  99. claude_mpm/services/agents/deployment/agent_validator.py +17 -1
  100. claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
  101. claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
  102. claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
  103. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
  104. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
  105. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
  106. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
  107. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
  108. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
  109. claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
  110. claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
  111. claude_mpm/services/agents/local_template_manager.py +1 -1
  112. claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
  113. claude_mpm/services/agents/registry/modification_tracker.py +5 -2
  114. claude_mpm/services/command_handler_service.py +11 -5
  115. claude_mpm/services/core/interfaces/__init__.py +74 -2
  116. claude_mpm/services/core/interfaces/health.py +172 -0
  117. claude_mpm/services/core/interfaces/model.py +281 -0
  118. claude_mpm/services/core/interfaces/process.py +372 -0
  119. claude_mpm/services/core/interfaces/restart.py +307 -0
  120. claude_mpm/services/core/interfaces/stability.py +260 -0
  121. claude_mpm/services/core/models/__init__.py +33 -0
  122. claude_mpm/services/core/models/agent_config.py +12 -28
  123. claude_mpm/services/core/models/health.py +162 -0
  124. claude_mpm/services/core/models/process.py +235 -0
  125. claude_mpm/services/core/models/restart.py +302 -0
  126. claude_mpm/services/core/models/stability.py +264 -0
  127. claude_mpm/services/core/path_resolver.py +23 -7
  128. claude_mpm/services/diagnostics/__init__.py +2 -2
  129. claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
  130. claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
  131. claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
  132. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
  133. claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
  134. claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
  135. claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
  136. claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
  137. claude_mpm/services/diagnostics/checks/mcp_services_check.py +36 -31
  138. claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
  139. claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
  140. claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
  141. claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
  142. claude_mpm/services/diagnostics/models.py +19 -24
  143. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
  144. claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
  145. claude_mpm/services/infrastructure/monitoring/base.py +5 -13
  146. claude_mpm/services/infrastructure/monitoring/network.py +7 -6
  147. claude_mpm/services/infrastructure/monitoring/process.py +13 -12
  148. claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
  149. claude_mpm/services/infrastructure/monitoring/service.py +16 -15
  150. claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
  151. claude_mpm/services/local_ops/__init__.py +163 -0
  152. claude_mpm/services/local_ops/crash_detector.py +257 -0
  153. claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
  154. claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
  155. claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
  156. claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
  157. claude_mpm/services/local_ops/health_manager.py +430 -0
  158. claude_mpm/services/local_ops/log_monitor.py +396 -0
  159. claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
  160. claude_mpm/services/local_ops/process_manager.py +595 -0
  161. claude_mpm/services/local_ops/resource_monitor.py +331 -0
  162. claude_mpm/services/local_ops/restart_manager.py +401 -0
  163. claude_mpm/services/local_ops/restart_policy.py +387 -0
  164. claude_mpm/services/local_ops/state_manager.py +372 -0
  165. claude_mpm/services/local_ops/unified_manager.py +600 -0
  166. claude_mpm/services/mcp_config_manager.py +9 -4
  167. claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
  168. claude_mpm/services/mcp_gateway/core/base.py +18 -31
  169. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +71 -24
  170. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
  171. claude_mpm/services/memory_hook_service.py +4 -1
  172. claude_mpm/services/model/__init__.py +147 -0
  173. claude_mpm/services/model/base_provider.py +365 -0
  174. claude_mpm/services/model/claude_provider.py +412 -0
  175. claude_mpm/services/model/model_router.py +453 -0
  176. claude_mpm/services/model/ollama_provider.py +415 -0
  177. claude_mpm/services/monitor/daemon_manager.py +3 -2
  178. claude_mpm/services/monitor/handlers/dashboard.py +2 -1
  179. claude_mpm/services/monitor/handlers/hooks.py +2 -1
  180. claude_mpm/services/monitor/management/lifecycle.py +3 -2
  181. claude_mpm/services/monitor/server.py +2 -1
  182. claude_mpm/services/session_management_service.py +3 -2
  183. claude_mpm/services/session_manager.py +205 -1
  184. claude_mpm/services/shared/async_service_base.py +16 -27
  185. claude_mpm/services/shared/lifecycle_service_base.py +1 -14
  186. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  187. claude_mpm/services/socketio/handlers/hook.py +13 -2
  188. claude_mpm/services/socketio/handlers/registry.py +4 -2
  189. claude_mpm/services/socketio/server/main.py +10 -8
  190. claude_mpm/services/subprocess_launcher_service.py +14 -5
  191. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
  192. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
  193. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
  194. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
  195. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
  196. claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
  197. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
  198. claude_mpm/services/unified/deployment_strategies/local.py +6 -5
  199. claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
  200. claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
  201. claude_mpm/services/unified/interfaces.py +3 -1
  202. claude_mpm/services/unified/unified_analyzer.py +14 -10
  203. claude_mpm/services/unified/unified_config.py +2 -1
  204. claude_mpm/services/unified/unified_deployment.py +9 -4
  205. claude_mpm/services/version_service.py +104 -1
  206. claude_mpm/skills/__init__.py +21 -0
  207. claude_mpm/skills/bundled/__init__.py +6 -0
  208. claude_mpm/skills/bundled/api-documentation.md +393 -0
  209. claude_mpm/skills/bundled/async-testing.md +571 -0
  210. claude_mpm/skills/bundled/code-review.md +143 -0
  211. claude_mpm/skills/bundled/database-migration.md +199 -0
  212. claude_mpm/skills/bundled/docker-containerization.md +194 -0
  213. claude_mpm/skills/bundled/express-local-dev.md +1429 -0
  214. claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
  215. claude_mpm/skills/bundled/git-workflow.md +414 -0
  216. claude_mpm/skills/bundled/imagemagick.md +204 -0
  217. claude_mpm/skills/bundled/json-data-handling.md +223 -0
  218. claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
  219. claude_mpm/skills/bundled/pdf.md +141 -0
  220. claude_mpm/skills/bundled/performance-profiling.md +567 -0
  221. claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
  222. claude_mpm/skills/bundled/security-scanning.md +327 -0
  223. claude_mpm/skills/bundled/systematic-debugging.md +473 -0
  224. claude_mpm/skills/bundled/test-driven-development.md +378 -0
  225. claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
  226. claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
  227. claude_mpm/skills/bundled/xlsx.md +157 -0
  228. claude_mpm/skills/registry.py +286 -0
  229. claude_mpm/skills/skill_manager.py +310 -0
  230. claude_mpm/tools/code_tree_analyzer.py +177 -141
  231. claude_mpm/tools/code_tree_events.py +4 -2
  232. claude_mpm/utils/agent_dependency_loader.py +2 -2
  233. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +117 -8
  234. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +238 -174
  235. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  236. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  237. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  238. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  239. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  240. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  241. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  242. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
  243. claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
  244. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
  245. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
  246. claude_mpm/services/project/analyzer_refactored.py +0 -450
  247. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
  248. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
  249. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
  250. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,310 @@
1
+ """Skills manager - integrates skills with agents."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ from claude_mpm.core.logging_utils import get_logger
8
+
9
+ from .registry import Skill, get_registry
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class SkillManager:
15
+ """Manages skills and their integration with agents."""
16
+
17
+ def __init__(self):
18
+ """Initialize the skill manager."""
19
+ self.registry = get_registry()
20
+ self.agent_skill_mapping: Dict[str, List[str]] = {}
21
+ self._load_agent_mappings()
22
+
23
+ def _load_agent_mappings(self):
24
+ """Load skill mappings from agent templates."""
25
+ # Load mappings from agent JSON templates that have 'skills' field
26
+ agent_templates_dir = Path(__file__).parent.parent / "agents" / "templates"
27
+
28
+ if not agent_templates_dir.exists():
29
+ logger.warning(
30
+ f"Agent templates directory not found: {agent_templates_dir}"
31
+ )
32
+ return
33
+
34
+ mapping_count = 0
35
+ for template_file in agent_templates_dir.glob("*.json"):
36
+ try:
37
+ with open(template_file, encoding="utf-8") as f:
38
+ agent_data = json.load(f)
39
+
40
+ agent_id = agent_data.get("agent_id") or agent_data.get("agent_type")
41
+ if not agent_id:
42
+ continue
43
+
44
+ # Extract skills list if present
45
+ skills = agent_data.get("skills", [])
46
+ if skills:
47
+ self.agent_skill_mapping[agent_id] = skills
48
+ mapping_count += 1
49
+ logger.debug(
50
+ f"Agent '{agent_id}' mapped to {len(skills)} skills: {', '.join(skills)}"
51
+ )
52
+
53
+ except Exception as e:
54
+ logger.error(f"Error loading agent mapping from {template_file}: {e}")
55
+
56
+ if mapping_count > 0:
57
+ logger.info(f"Loaded skill mappings for {mapping_count} agents")
58
+
59
+ def get_agent_skills(self, agent_type: str) -> List[Skill]:
60
+ """
61
+ Get all skills for an agent (bundled + discovered).
62
+
63
+ Args:
64
+ agent_type: Agent type/ID (e.g., 'engineer', 'python_engineer')
65
+
66
+ Returns:
67
+ List of Skill objects for this agent
68
+ """
69
+ skill_names = self.agent_skill_mapping.get(agent_type, [])
70
+
71
+ # Get skills from registry
72
+ skills = []
73
+ for name in skill_names:
74
+ skill = self.registry.get_skill(name)
75
+ if skill:
76
+ skills.append(skill)
77
+ else:
78
+ logger.warning(
79
+ f"Skill '{name}' referenced by agent '{agent_type}' not found"
80
+ )
81
+
82
+ # Also include skills that have no agent restriction
83
+ # or explicitly list this agent type
84
+ additional_skills = self.registry.get_skills_for_agent(agent_type)
85
+ for skill in additional_skills:
86
+ if skill not in skills:
87
+ skills.append(skill)
88
+
89
+ return skills
90
+
91
+ def enhance_agent_prompt(
92
+ self, agent_type: str, base_prompt: str, include_all: bool = False
93
+ ) -> str:
94
+ """
95
+ Enhance agent prompt with available skills.
96
+
97
+ Args:
98
+ agent_type: Agent type/ID
99
+ base_prompt: Original agent prompt
100
+ include_all: If True, include all available skills regardless of mapping
101
+
102
+ Returns:
103
+ Enhanced prompt with skills section appended
104
+ """
105
+ if include_all:
106
+ skills = self.registry.list_skills()
107
+ else:
108
+ skills = self.get_agent_skills(agent_type)
109
+
110
+ if not skills:
111
+ return base_prompt
112
+
113
+ # Build skills section
114
+ skills_section = "\n\n" + "=" * 80 + "\n"
115
+ skills_section += "## 🎯 Available Skills\n\n"
116
+ skills_section += f"You have access to {len(skills)} specialized skills:\n\n"
117
+
118
+ for skill in skills:
119
+ skills_section += f"### 📚 {skill.name.replace('-', ' ').title()}\n\n"
120
+ skills_section += f"**Source:** {skill.source}\n"
121
+ if skill.description:
122
+ skills_section += f"**Description:** {skill.description}\n"
123
+ skills_section += "\n```\n"
124
+ skills_section += skill.content
125
+ skills_section += "\n```\n\n"
126
+
127
+ skills_section += "=" * 80 + "\n"
128
+
129
+ return base_prompt + skills_section
130
+
131
+ def list_agent_skill_mappings(self) -> Dict[str, List[str]]:
132
+ """
133
+ Get all agent-to-skill mappings.
134
+
135
+ Returns:
136
+ Dictionary mapping agent IDs to lists of skill names
137
+ """
138
+ return self.agent_skill_mapping.copy()
139
+
140
+ def add_skill_to_agent(self, agent_type: str, skill_name: str) -> bool:
141
+ """
142
+ Add a skill to an agent's mapping.
143
+
144
+ Args:
145
+ agent_type: Agent type/ID
146
+ skill_name: Name of the skill to add
147
+
148
+ Returns:
149
+ True if successful, False if skill not found
150
+ """
151
+ skill = self.registry.get_skill(skill_name)
152
+ if not skill:
153
+ logger.error(f"Cannot add skill '{skill_name}': skill not found")
154
+ return False
155
+
156
+ if agent_type not in self.agent_skill_mapping:
157
+ self.agent_skill_mapping[agent_type] = []
158
+
159
+ if skill_name not in self.agent_skill_mapping[agent_type]:
160
+ self.agent_skill_mapping[agent_type].append(skill_name)
161
+ logger.info(f"Added skill '{skill_name}' to agent '{agent_type}'")
162
+
163
+ return True
164
+
165
+ def remove_skill_from_agent(self, agent_type: str, skill_name: str) -> bool:
166
+ """
167
+ Remove a skill from an agent's mapping.
168
+
169
+ Args:
170
+ agent_type: Agent type/ID
171
+ skill_name: Name of the skill to remove
172
+
173
+ Returns:
174
+ True if successful, False if not found
175
+ """
176
+ if agent_type not in self.agent_skill_mapping:
177
+ return False
178
+
179
+ if skill_name in self.agent_skill_mapping[agent_type]:
180
+ self.agent_skill_mapping[agent_type].remove(skill_name)
181
+ logger.info(f"Removed skill '{skill_name}' from agent '{agent_type}'")
182
+ return True
183
+
184
+ return False
185
+
186
+ def reload(self):
187
+ """Reload skills and agent mappings."""
188
+ logger.info("Reloading skill manager...")
189
+ self.registry.reload()
190
+ self.agent_skill_mapping.clear()
191
+ self._load_agent_mappings()
192
+ logger.info("Skill manager reloaded")
193
+
194
+ def infer_agents_for_skill(self, skill) -> List[str]:
195
+ """Infer which agents should have this skill based on tags/name.
196
+
197
+ Args:
198
+ skill: Skill object to analyze
199
+
200
+ Returns:
201
+ List of agent IDs that should have this skill
202
+ """
203
+ agents = []
204
+ content_lower = skill.content.lower()
205
+ name_lower = skill.name.lower()
206
+
207
+ # Python-related
208
+ if any(
209
+ tag in content_lower or tag in name_lower
210
+ for tag in ["python", "django", "flask", "fastapi"]
211
+ ):
212
+ agents.append("python-engineer")
213
+
214
+ # TypeScript/JavaScript-related
215
+ if any(
216
+ tag in content_lower or tag in name_lower
217
+ for tag in ["typescript", "javascript", "react", "next", "vue", "node"]
218
+ ):
219
+ agents.extend(["typescript-engineer", "react-engineer", "nextjs-engineer"])
220
+
221
+ # Go-related
222
+ if any(tag in content_lower or tag in name_lower for tag in ["golang", "go "]):
223
+ agents.append("golang-engineer")
224
+
225
+ # Ops-related
226
+ if any(
227
+ tag in content_lower or tag in name_lower
228
+ for tag in ["docker", "kubernetes", "deploy", "devops", "ops"]
229
+ ):
230
+ agents.extend(["ops", "devops", "local-ops"])
231
+
232
+ # Testing/QA-related
233
+ if any(
234
+ tag in content_lower or tag in name_lower
235
+ for tag in ["test", "qa", "quality", "assert"]
236
+ ):
237
+ agents.extend(["qa", "web-qa", "api-qa"])
238
+
239
+ # Documentation-related
240
+ if any(
241
+ tag in content_lower or tag in name_lower
242
+ for tag in ["documentation", "docs", "api doc", "openapi"]
243
+ ):
244
+ agents.extend(["docs", "documentation", "technical-writer"])
245
+
246
+ # Remove duplicates
247
+ return list(set(agents))
248
+
249
+ def save_mappings_to_config(self, config_path: Optional[Path] = None):
250
+ """Save current agent-skill mappings to configuration file.
251
+
252
+ Args:
253
+ config_path: Path to configuration file. If None, uses default.
254
+ """
255
+ import json
256
+
257
+ if config_path is None:
258
+ config_path = Path.cwd() / ".claude-mpm" / "skills_config.json"
259
+
260
+ # Ensure directory exists
261
+ config_path.parent.mkdir(parents=True, exist_ok=True)
262
+
263
+ # Save mappings
264
+ with open(config_path, "w", encoding="utf-8") as f:
265
+ json.dump(self.agent_skill_mapping, f, indent=2)
266
+
267
+ logger.info(f"Saved skill mappings to {config_path}")
268
+
269
+ def load_mappings_from_config(self, config_path: Optional[Path] = None):
270
+ """Load agent-skill mappings from configuration file.
271
+
272
+ Args:
273
+ config_path: Path to configuration file. If None, uses default.
274
+ """
275
+ import json
276
+
277
+ if config_path is None:
278
+ config_path = Path.cwd() / ".claude-mpm" / "skills_config.json"
279
+
280
+ if not config_path.exists():
281
+ logger.debug(f"No skill mappings config found at {config_path}")
282
+ return
283
+
284
+ try:
285
+ with open(config_path, encoding="utf-8") as f:
286
+ loaded_mappings = json.load(f)
287
+
288
+ # Merge with existing mappings
289
+ for agent_id, skills in loaded_mappings.items():
290
+ if agent_id not in self.agent_skill_mapping:
291
+ self.agent_skill_mapping[agent_id] = []
292
+ for skill in skills:
293
+ if skill not in self.agent_skill_mapping[agent_id]:
294
+ self.agent_skill_mapping[agent_id].append(skill)
295
+
296
+ logger.info(f"Loaded skill mappings from {config_path}")
297
+ except Exception as e:
298
+ logger.error(f"Error loading skill mappings from {config_path}: {e}")
299
+
300
+
301
+ # Global manager instance (singleton pattern)
302
+ _manager: Optional[SkillManager] = None
303
+
304
+
305
+ def get_manager() -> SkillManager:
306
+ """Get the global skill manager (singleton)."""
307
+ global _manager
308
+ if _manager is None:
309
+ _manager = SkillManager()
310
+ return _manager
@@ -1528,10 +1528,31 @@ class CodeTreeAnalyzer:
1528
1528
  if not path.exists() or not path.is_file():
1529
1529
  return {"error": f"Invalid file: {file_path}"}
1530
1530
 
1531
- # Get language first (needed for return statement)
1532
1531
  language = self._get_language(path)
1532
+ self._emit_analysis_start(path, language)
1533
1533
 
1534
- # Emit analysis start event
1534
+ # Check cache
1535
+ file_hash = self._get_file_hash(path)
1536
+ cache_key = f"{file_path}:{file_hash}"
1537
+
1538
+ if cache_key in self.cache:
1539
+ nodes = self.cache[cache_key]
1540
+ self._emit_cache_hit(path)
1541
+ filtered_nodes = self._filter_nodes(nodes)
1542
+ else:
1543
+ nodes, filtered_nodes, duration = self._analyze_and_cache_file(
1544
+ path, language, cache_key
1545
+ )
1546
+ self._emit_analysis_complete(path, filtered_nodes, duration)
1547
+
1548
+ # Prepare final data structures
1549
+ final_nodes = self._prepare_final_nodes(nodes, filtered_nodes)
1550
+ elements = self._convert_nodes_to_elements(final_nodes)
1551
+
1552
+ return self._build_result(file_path, language, final_nodes, elements)
1553
+
1554
+ def _emit_analysis_start(self, path: Path, language: str) -> None:
1555
+ """Emit analysis start event."""
1535
1556
  if self.emitter:
1536
1557
  from datetime import datetime
1537
1558
 
@@ -1546,179 +1567,194 @@ class CodeTreeAnalyzer:
1546
1567
  },
1547
1568
  )
1548
1569
 
1549
- # Check cache
1550
- file_hash = self._get_file_hash(path)
1551
- cache_key = f"{file_path}:{file_hash}"
1570
+ def _emit_cache_hit(self, path: Path) -> None:
1571
+ """Emit cache hit event."""
1572
+ if self.emitter:
1573
+ from datetime import datetime
1552
1574
 
1553
- if cache_key in self.cache:
1554
- nodes = self.cache[cache_key]
1555
- if self.emitter:
1556
- from datetime import datetime
1575
+ self.emitter.emit(
1576
+ "info",
1577
+ {
1578
+ "type": "cache.hit",
1579
+ "file": str(path),
1580
+ "message": f"Using cached analysis for {path.name}",
1581
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1582
+ },
1583
+ )
1557
1584
 
1558
- self.emitter.emit(
1559
- "info",
1560
- {
1561
- "type": "cache.hit",
1562
- "file": str(path),
1563
- "message": f"Using cached analysis for {path.name}",
1564
- "timestamp": datetime.now(timezone.utc).isoformat(),
1565
- },
1566
- )
1567
- else:
1568
- # Analyze file
1569
- if self.emitter:
1570
- from datetime import datetime
1585
+ def _emit_cache_miss(self, path: Path) -> None:
1586
+ """Emit cache miss event."""
1587
+ if self.emitter:
1588
+ from datetime import datetime
1571
1589
 
1572
- self.emitter.emit(
1573
- "info",
1574
- {
1575
- "type": "cache.miss",
1576
- "file": str(path),
1577
- "message": f"Cache miss, analyzing fresh: {path.name}",
1578
- "timestamp": datetime.now(timezone.utc).isoformat(),
1579
- },
1580
- )
1590
+ self.emitter.emit(
1591
+ "info",
1592
+ {
1593
+ "type": "cache.miss",
1594
+ "file": str(path),
1595
+ "message": f"Cache miss, analyzing fresh: {path.name}",
1596
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1597
+ },
1598
+ )
1581
1599
 
1582
- if language == "python":
1583
- analyzer = self.python_analyzer
1584
- elif language in {"javascript", "typescript"}:
1585
- analyzer = self.javascript_analyzer
1586
- else:
1587
- analyzer = self.generic_analyzer
1600
+ def _emit_parsing_start(self, path: Path) -> None:
1601
+ """Emit parsing start event."""
1602
+ if self.emitter:
1603
+ from datetime import datetime
1604
+
1605
+ self.emitter.emit(
1606
+ "info",
1607
+ {
1608
+ "type": "analysis.parse",
1609
+ "file": str(path),
1610
+ "message": f"Parsing file content: {path.name}",
1611
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1612
+ },
1613
+ )
1588
1614
 
1589
- start_time = time.time()
1615
+ def _emit_node_found(self, node: CodeNode, path: Path) -> None:
1616
+ """Emit node found event."""
1617
+ if self.emitter:
1618
+ from datetime import datetime
1590
1619
 
1591
- # Emit parsing event
1592
- if self.emitter:
1593
- from datetime import datetime
1620
+ self.emitter.emit(
1621
+ "info",
1622
+ {
1623
+ "type": f"analysis.{node.node_type}",
1624
+ "name": node.name,
1625
+ "file": str(path),
1626
+ "line_start": node.line_start,
1627
+ "complexity": node.complexity,
1628
+ "message": f"Found {node.node_type}: {node.name}",
1629
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1630
+ },
1631
+ )
1594
1632
 
1595
- self.emitter.emit(
1596
- "info",
1597
- {
1598
- "type": "analysis.parse",
1599
- "file": str(path),
1600
- "message": f"Parsing file content: {path.name}",
1601
- "timestamp": datetime.now(timezone.utc).isoformat(),
1602
- },
1603
- )
1633
+ def _emit_analysis_complete(
1634
+ self, path: Path, filtered_nodes: list, duration: float
1635
+ ) -> None:
1636
+ """Emit analysis complete event."""
1637
+ if not self.emitter:
1638
+ return
1604
1639
 
1605
- nodes = analyzer.analyze_file(path) if analyzer else []
1606
- duration = time.time() - start_time
1640
+ from datetime import datetime
1641
+
1642
+ stats = self._calculate_node_stats(filtered_nodes)
1643
+ self.emitter.emit(
1644
+ "info",
1645
+ {
1646
+ "type": "analysis.complete",
1647
+ "file": str(path),
1648
+ "stats": stats,
1649
+ "duration": duration,
1650
+ "message": f"Analysis complete: {stats['classes']} classes, {stats['functions']} functions, {stats['methods']} methods",
1651
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1652
+ },
1653
+ )
1654
+ self.emitter.emit_file_analyzed(str(path), filtered_nodes, duration)
1607
1655
 
1608
- # Cache results
1609
- self.cache[cache_key] = nodes
1656
+ def _analyze_and_cache_file(
1657
+ self, path: Path, language: str, cache_key: str
1658
+ ) -> tuple:
1659
+ """Analyze file content and cache results."""
1660
+ self._emit_cache_miss(path)
1661
+ self._emit_parsing_start(path)
1610
1662
 
1611
- # Filter internal functions before emitting
1612
- filtered_nodes = []
1613
- classes_count = 0
1614
- functions_count = 0
1615
- methods_count = 0
1663
+ # Select analyzer based on language
1664
+ analyzer = self._select_analyzer(language)
1616
1665
 
1617
- for node in nodes:
1618
- # Only include main structural elements
1619
- if not self._is_internal_node(node):
1620
- # Emit found element event
1621
- if self.emitter:
1622
- from datetime import datetime
1666
+ # Perform analysis
1667
+ start_time = time.time()
1668
+ nodes = analyzer.analyze_file(path) if analyzer else []
1669
+ duration = time.time() - start_time
1623
1670
 
1624
- self.emitter.emit(
1625
- "info",
1626
- {
1627
- "type": f"analysis.{node.node_type}",
1628
- "name": node.name,
1629
- "file": str(path),
1630
- "line_start": node.line_start,
1631
- "complexity": node.complexity,
1632
- "message": f"Found {node.node_type}: {node.name}",
1633
- "timestamp": datetime.now(timezone.utc).isoformat(),
1634
- },
1635
- )
1671
+ # Cache results
1672
+ self.cache[cache_key] = nodes
1636
1673
 
1637
- # Count node types
1638
- if node.node_type == "class":
1639
- classes_count += 1
1640
- elif node.node_type == "function":
1641
- functions_count += 1
1642
- elif node.node_type == "method":
1643
- methods_count += 1
1644
-
1645
- filtered_nodes.append(
1646
- {
1647
- "name": node.name,
1648
- "type": node.node_type,
1649
- "line_start": node.line_start,
1650
- "line_end": node.line_end,
1651
- "complexity": node.complexity,
1652
- "has_docstring": node.has_docstring,
1653
- "signature": node.signature,
1654
- }
1655
- )
1674
+ # Filter and process nodes
1675
+ filtered_nodes = self._filter_and_emit_nodes(nodes, path)
1656
1676
 
1657
- # Emit analysis complete event with stats
1658
- if self.emitter:
1659
- from datetime import datetime
1677
+ return nodes, filtered_nodes, duration
1660
1678
 
1661
- self.emitter.emit(
1662
- "info",
1663
- {
1664
- "type": "analysis.complete",
1665
- "file": str(path),
1666
- "stats": {
1667
- "classes": classes_count,
1668
- "functions": functions_count,
1669
- "methods": methods_count,
1670
- "total_nodes": len(filtered_nodes),
1671
- },
1672
- "duration": duration,
1673
- "message": f"Analysis complete: {classes_count} classes, {functions_count} functions, {methods_count} methods",
1674
- "timestamp": datetime.now(timezone.utc).isoformat(),
1675
- },
1676
- )
1679
+ def _select_analyzer(self, language: str):
1680
+ """Select appropriate analyzer for language."""
1681
+ if language == "python":
1682
+ return self.python_analyzer
1683
+ if language in {"javascript", "typescript"}:
1684
+ return self.javascript_analyzer
1685
+ return self.generic_analyzer
1677
1686
 
1678
- self.emitter.emit_file_analyzed(file_path, filtered_nodes, duration)
1687
+ def _filter_nodes(self, nodes: list) -> list:
1688
+ """Filter nodes without emitting events."""
1689
+ return [self._node_to_dict(n) for n in nodes if not self._is_internal_node(n)]
1679
1690
 
1680
- # Prepare the nodes data
1681
- final_nodes = (
1682
- filtered_nodes
1683
- if "filtered_nodes" in locals()
1684
- else [
1685
- {
1686
- "name": n.name,
1687
- "type": n.node_type,
1688
- "line_start": n.line_start,
1689
- "line_end": n.line_end,
1690
- "complexity": n.complexity,
1691
- "has_docstring": n.has_docstring,
1692
- "signature": n.signature,
1693
- }
1694
- for n in nodes
1695
- if not self._is_internal_node(n)
1696
- ]
1697
- )
1691
+ def _filter_and_emit_nodes(self, nodes: list, path: Path) -> list:
1692
+ """Filter nodes and emit events for each."""
1693
+ filtered_nodes = []
1694
+ for node in nodes:
1695
+ if not self._is_internal_node(node):
1696
+ self._emit_node_found(node, path)
1697
+ filtered_nodes.append(self._node_to_dict(node))
1698
+ return filtered_nodes
1699
+
1700
+ def _node_to_dict(self, node: CodeNode) -> dict:
1701
+ """Convert CodeNode to dictionary."""
1702
+ return {
1703
+ "name": node.name,
1704
+ "type": node.node_type,
1705
+ "line_start": node.line_start,
1706
+ "line_end": node.line_end,
1707
+ "complexity": node.complexity,
1708
+ "has_docstring": node.has_docstring,
1709
+ "signature": node.signature,
1710
+ }
1711
+
1712
+ def _calculate_node_stats(self, filtered_nodes: list) -> dict:
1713
+ """Calculate statistics from filtered nodes."""
1714
+ classes_count = sum(1 for n in filtered_nodes if n["type"] == "class")
1715
+ functions_count = sum(1 for n in filtered_nodes if n["type"] == "function")
1716
+ methods_count = sum(1 for n in filtered_nodes if n["type"] == "method")
1717
+ return {
1718
+ "classes": classes_count,
1719
+ "functions": functions_count,
1720
+ "methods": methods_count,
1721
+ "total_nodes": len(filtered_nodes),
1722
+ }
1723
+
1724
+ def _prepare_final_nodes(self, nodes: list, filtered_nodes: list) -> list:
1725
+ """Prepare final nodes data structure."""
1726
+ if filtered_nodes:
1727
+ return filtered_nodes
1728
+ return [self._node_to_dict(n) for n in nodes if not self._is_internal_node(n)]
1698
1729
 
1699
- # Convert nodes to elements format for dashboard compatibility
1730
+ def _convert_nodes_to_elements(self, final_nodes: list) -> list:
1731
+ """Convert nodes to elements format for dashboard."""
1700
1732
  elements = []
1701
1733
  for node in final_nodes:
1702
1734
  element = {
1703
1735
  "name": node["name"],
1704
1736
  "type": node["type"],
1705
- "line": node["line_start"], # Dashboard expects 'line' not 'line_start'
1737
+ "line": node["line_start"],
1706
1738
  "complexity": node["complexity"],
1707
1739
  "signature": node.get("signature", ""),
1708
1740
  "has_docstring": node.get("has_docstring", False),
1709
1741
  }
1710
- # Add methods if it's a class (for expandable tree)
1711
1742
  if node["type"] == "class":
1712
- element["methods"] = [] # Could be populated with class methods
1743
+ element["methods"] = []
1713
1744
  elements.append(element)
1745
+ return elements
1714
1746
 
1747
+ def _build_result(
1748
+ self, file_path: str, language: str, final_nodes: list, elements: list
1749
+ ) -> dict:
1750
+ """Build final result dictionary."""
1715
1751
  return {
1716
1752
  "path": file_path,
1717
1753
  "language": language,
1718
- "nodes": final_nodes, # Keep for backward compatibility
1719
- "elements": elements, # Add for dashboard compatibility
1754
+ "nodes": final_nodes,
1755
+ "elements": elements,
1720
1756
  "complexity": sum(e["complexity"] for e in elements),
1721
- "lines": len(elements), # Simple line count approximation
1757
+ "lines": len(elements),
1722
1758
  "stats": {
1723
1759
  "classes": len([e for e in elements if e["type"] == "class"]),
1724
1760
  "functions": len([e for e in elements if e["type"] == "function"]),