claude-mpm 5.1.8__py3-none-any.whl → 5.4.22__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/__init__.py +4 -0
- claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
- claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
- claude_mpm/agents/agent_loader.py +13 -44
- claude_mpm/agents/frontmatter_validator.py +68 -0
- claude_mpm/agents/templates/circuit-breakers.md +138 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agent_state_manager.py +8 -17
- claude_mpm/cli/commands/agents.py +169 -31
- claude_mpm/cli/commands/auto_configure.py +210 -25
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +1111 -161
- claude_mpm/cli/commands/configure_agent_display.py +15 -6
- claude_mpm/cli/commands/mpm_init/core.py +160 -46
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +11 -3
- claude_mpm/cli/parsers/agents_parser.py +54 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +550 -94
- claude_mpm/commands/mpm-config.md +265 -0
- claude_mpm/commands/mpm-help.md +14 -95
- claude_mpm/commands/mpm-organize.md +500 -0
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
- claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +0 -19
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
- claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +188 -12
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +531 -55
- claude_mpm/services/agents/git_source_manager.py +34 -0
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
- claude_mpm/services/agents/toolchain_detector.py +10 -6
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +81 -10
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +93 -8
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +32 -2
- claude_mpm/services/skills/selective_skill_deployer.py +704 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +126 -9
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +17 -44
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +86 -176
- claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
- claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_ENGINEER.md +0 -658
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent.json +0 -31
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/commands/mpm-ticket-organize.md +0 -304
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- claude_mpm-5.1.8.dist-info/entry_points.txt +0 -10
- claude_mpm-5.1.8.dist-info/licenses/LICENSE +0 -21
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Agent review service for comparing project agents with managed agents.
|
|
2
|
+
|
|
3
|
+
WHY: This service helps users maintain a clean agent directory by:
|
|
4
|
+
1. Identifying which agents are managed vs custom
|
|
5
|
+
2. Detecting outdated versions of managed agents
|
|
6
|
+
3. Finding unused agents that don't match the detected toolchain
|
|
7
|
+
4. Safely archiving unnecessary agents instead of deleting them
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Archive to .claude/agents/unused/ instead of deleting (safe, recoverable)
|
|
11
|
+
- Add timestamps to archived files to prevent conflicts
|
|
12
|
+
- Preserve custom user agents (not in managed set)
|
|
13
|
+
- Compare versions to detect outdated managed agents
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Set
|
|
20
|
+
|
|
21
|
+
from claude_mpm.core.logging_config import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentReviewService:
|
|
27
|
+
"""Service for reviewing and managing project agents.
|
|
28
|
+
|
|
29
|
+
This service analyzes the relationship between project agents and managed
|
|
30
|
+
agents from the claude-mpm-agents repository, categorizing them as:
|
|
31
|
+
- Managed: In sync with managed agents
|
|
32
|
+
- Outdated: Older version of managed agent exists
|
|
33
|
+
- Custom: User-created agents not in managed set
|
|
34
|
+
- Unused: Not recommended for this project's toolchain
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""Initialize the agent review service."""
|
|
39
|
+
self.logger = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
def review_project_agents(
|
|
42
|
+
self,
|
|
43
|
+
project_agents_dir: Path,
|
|
44
|
+
managed_agents: List[Dict[str, Any]],
|
|
45
|
+
recommended_agent_ids: Set[str],
|
|
46
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
47
|
+
"""Review existing project agents and categorize them.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
project_agents_dir: Directory containing project agents (.claude/agents/)
|
|
51
|
+
managed_agents: List of managed agent dicts from cache
|
|
52
|
+
recommended_agent_ids: Set of agent IDs recommended for this toolchain
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary with categorized agents:
|
|
56
|
+
{
|
|
57
|
+
"managed": [...], # In sync with managed
|
|
58
|
+
"outdated": [...], # Older version exists
|
|
59
|
+
"custom": [...], # User-created
|
|
60
|
+
"unused": [...], # Not needed for this toolchain
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
results = {
|
|
64
|
+
"managed": [],
|
|
65
|
+
"outdated": [],
|
|
66
|
+
"custom": [],
|
|
67
|
+
"unused": [],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if not project_agents_dir.exists():
|
|
71
|
+
self.logger.debug(
|
|
72
|
+
f"Project agents directory does not exist: {project_agents_dir}"
|
|
73
|
+
)
|
|
74
|
+
return results
|
|
75
|
+
|
|
76
|
+
# Build lookup map of managed agents by ID
|
|
77
|
+
managed_by_id = {agent["agent_id"]: agent for agent in managed_agents}
|
|
78
|
+
|
|
79
|
+
# Scan project agents
|
|
80
|
+
for agent_file in project_agents_dir.glob("*.md"):
|
|
81
|
+
# Skip the unused directory itself
|
|
82
|
+
if agent_file.name == "unused":
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
agent_name = agent_file.stem
|
|
86
|
+
|
|
87
|
+
# Parse agent to get version and metadata
|
|
88
|
+
project_agent_info = self._parse_project_agent(agent_file)
|
|
89
|
+
|
|
90
|
+
# Check if this is a managed agent
|
|
91
|
+
if agent_name in managed_by_id:
|
|
92
|
+
managed_agent = managed_by_id[agent_name]
|
|
93
|
+
|
|
94
|
+
# Compare versions
|
|
95
|
+
project_version = project_agent_info.get("version", "unknown")
|
|
96
|
+
managed_version = managed_agent.get("version", "unknown")
|
|
97
|
+
|
|
98
|
+
if self._is_outdated(project_version, managed_version):
|
|
99
|
+
# Outdated version of managed agent
|
|
100
|
+
results["outdated"].append(
|
|
101
|
+
{
|
|
102
|
+
"name": agent_name,
|
|
103
|
+
"path": agent_file,
|
|
104
|
+
"current_version": project_version,
|
|
105
|
+
"available_version": managed_version,
|
|
106
|
+
"recommended": agent_name in recommended_agent_ids,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
# Up-to-date managed agent
|
|
111
|
+
results["managed"].append(
|
|
112
|
+
{
|
|
113
|
+
"name": agent_name,
|
|
114
|
+
"path": agent_file,
|
|
115
|
+
"version": project_version,
|
|
116
|
+
"recommended": agent_name in recommended_agent_ids,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
# Custom user agent (not in managed set)
|
|
121
|
+
results["custom"].append(
|
|
122
|
+
{
|
|
123
|
+
"name": agent_name,
|
|
124
|
+
"path": agent_file,
|
|
125
|
+
"version": project_agent_info.get("version", "unknown"),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Identify unused agents (managed or outdated but not recommended)
|
|
130
|
+
for category in ["managed", "outdated"]:
|
|
131
|
+
for agent in results[category][:]: # Copy list to modify during iteration
|
|
132
|
+
if not agent.get("recommended", False):
|
|
133
|
+
# This managed/outdated agent is not recommended for this toolchain
|
|
134
|
+
results["unused"].append(agent)
|
|
135
|
+
results[category].remove(agent)
|
|
136
|
+
|
|
137
|
+
self.logger.info(
|
|
138
|
+
f"Agent review complete: "
|
|
139
|
+
f"{len(results['managed'])} managed, "
|
|
140
|
+
f"{len(results['outdated'])} outdated, "
|
|
141
|
+
f"{len(results['custom'])} custom, "
|
|
142
|
+
f"{len(results['unused'])} unused"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return results
|
|
146
|
+
|
|
147
|
+
def archive_agents(
|
|
148
|
+
self, agents_to_archive: List[Dict[str, Any]], project_agents_dir: Path
|
|
149
|
+
) -> Dict[str, Any]:
|
|
150
|
+
"""Archive agents by moving them to .claude/agents/unused/.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
agents_to_archive: List of agent dicts with 'name' and 'path' keys
|
|
154
|
+
project_agents_dir: Base agents directory (.claude/agents/)
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dictionary with archival results:
|
|
158
|
+
{
|
|
159
|
+
"archived": [...], # Successfully archived
|
|
160
|
+
"errors": [...], # Archival errors
|
|
161
|
+
}
|
|
162
|
+
"""
|
|
163
|
+
results = {"archived": [], "errors": []}
|
|
164
|
+
|
|
165
|
+
if not agents_to_archive:
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
# Create unused directory
|
|
169
|
+
unused_dir = project_agents_dir / "unused"
|
|
170
|
+
unused_dir.mkdir(exist_ok=True)
|
|
171
|
+
|
|
172
|
+
for agent in agents_to_archive:
|
|
173
|
+
agent_path = agent["path"]
|
|
174
|
+
agent_name = agent["name"]
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Generate timestamped filename to avoid conflicts
|
|
178
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
179
|
+
archived_name = f"{agent_name}_{timestamp}.md"
|
|
180
|
+
archived_path = unused_dir / archived_name
|
|
181
|
+
|
|
182
|
+
# Move the file
|
|
183
|
+
shutil.move(str(agent_path), str(archived_path))
|
|
184
|
+
|
|
185
|
+
results["archived"].append(
|
|
186
|
+
{
|
|
187
|
+
"name": agent_name,
|
|
188
|
+
"original_path": str(agent_path),
|
|
189
|
+
"archived_path": str(archived_path),
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self.logger.debug(f"Archived {agent_name} to {archived_path}")
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
error_msg = f"Failed to archive {agent_name}: {e}"
|
|
197
|
+
self.logger.error(error_msg)
|
|
198
|
+
results["errors"].append(error_msg)
|
|
199
|
+
|
|
200
|
+
self.logger.info(
|
|
201
|
+
f"Archived {len(results['archived'])} agents, "
|
|
202
|
+
f"{len(results['errors'])} errors"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return results
|
|
206
|
+
|
|
207
|
+
def _parse_project_agent(self, agent_file: Path) -> Dict[str, Any]:
|
|
208
|
+
"""Parse a project agent file to extract metadata.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
agent_file: Path to agent Markdown file
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dictionary with agent metadata (version, name, etc.)
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
content = agent_file.read_text(encoding="utf-8")
|
|
218
|
+
|
|
219
|
+
# Extract version from YAML frontmatter
|
|
220
|
+
import re
|
|
221
|
+
|
|
222
|
+
version_match = re.search(
|
|
223
|
+
r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE
|
|
224
|
+
)
|
|
225
|
+
version = version_match.group(1) if version_match else "unknown"
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"version": version,
|
|
229
|
+
"name": agent_file.stem,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.logger.warning(f"Failed to parse agent {agent_file.name}: {e}")
|
|
234
|
+
return {"version": "unknown", "name": agent_file.stem}
|
|
235
|
+
|
|
236
|
+
def _is_outdated(self, current_version: str, available_version: str) -> bool:
|
|
237
|
+
"""Check if current version is outdated compared to available version.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
current_version: Currently deployed version
|
|
241
|
+
available_version: Available version from managed agents
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if current version is outdated
|
|
245
|
+
"""
|
|
246
|
+
# Handle unknown versions
|
|
247
|
+
if current_version == "unknown" or available_version == "unknown":
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
# Simple string comparison for now
|
|
251
|
+
# TODO: Implement semantic version comparison (1.2.3 vs 1.2.4)
|
|
252
|
+
return current_version != available_version
|
|
253
|
+
|
|
254
|
+
def get_archive_summary(self, project_agents_dir: Path) -> Dict[str, Any]:
|
|
255
|
+
"""Get summary of archived agents.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
project_agents_dir: Base agents directory (.claude/agents/)
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Dictionary with archive statistics
|
|
262
|
+
"""
|
|
263
|
+
unused_dir = project_agents_dir / "unused"
|
|
264
|
+
|
|
265
|
+
if not unused_dir.exists():
|
|
266
|
+
return {"count": 0, "agents": []}
|
|
267
|
+
|
|
268
|
+
archived_files = list(unused_dir.glob("*.md"))
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"count": len(archived_files),
|
|
272
|
+
"agents": [
|
|
273
|
+
{
|
|
274
|
+
"name": f.stem,
|
|
275
|
+
"path": str(f),
|
|
276
|
+
"size_bytes": f.stat().st_size,
|
|
277
|
+
}
|
|
278
|
+
for f in archived_files
|
|
279
|
+
],
|
|
280
|
+
}
|
|
@@ -215,9 +215,8 @@ class AgentDiscoveryService:
|
|
|
215
215
|
# Extract YAML frontmatter
|
|
216
216
|
frontmatter = self._extract_yaml_frontmatter(template_content)
|
|
217
217
|
if not frontmatter:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
)
|
|
218
|
+
# Silently return None for files without frontmatter
|
|
219
|
+
# (e.g., PM instruction templates in templates/ directory)
|
|
221
220
|
return None
|
|
222
221
|
|
|
223
222
|
# Extract metadata directly from frontmatter (flat structure)
|
|
@@ -129,12 +129,13 @@ class AgentTemplateBuilder:
|
|
|
129
129
|
base_templates.append(base_agent_file)
|
|
130
130
|
self.logger.debug(f"Found BASE-AGENT.md at: {base_agent_file}")
|
|
131
131
|
|
|
132
|
-
# Stop at git repository root if detected
|
|
132
|
+
# Stop at git repository root if detected (check AFTER finding BASE-AGENT.md)
|
|
133
133
|
if (current_dir / ".git").exists():
|
|
134
134
|
self.logger.debug(f"Reached git repository root at: {current_dir}")
|
|
135
135
|
break
|
|
136
136
|
|
|
137
|
-
# Stop at common repository root indicators
|
|
137
|
+
# Stop at common repository root indicators (check AFTER finding BASE-AGENT.md)
|
|
138
|
+
# This ensures we check the 'agents' directory before stopping at 'remote-agents'
|
|
138
139
|
if current_dir.name in [".claude-mpm", "remote-agents", "cache"]:
|
|
139
140
|
self.logger.debug(
|
|
140
141
|
f"Reached repository root indicator at: {current_dir}"
|
|
@@ -675,6 +676,7 @@ Only include memories that are:
|
|
|
675
676
|
"""
|
|
676
677
|
content = content + memory_instructions
|
|
677
678
|
|
|
679
|
+
# Combine frontmatter and content
|
|
678
680
|
return frontmatter + content
|
|
679
681
|
|
|
680
682
|
def build_agent_yaml(
|
|
@@ -16,6 +16,8 @@ import os
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Dict, List, Optional, Tuple
|
|
18
18
|
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
19
21
|
from claude_mpm.core.config import Config
|
|
20
22
|
from claude_mpm.core.logging_config import get_logger
|
|
21
23
|
|
|
@@ -51,6 +53,127 @@ class MultiSourceAgentDeploymentService:
|
|
|
51
53
|
self.logger = get_logger(__name__)
|
|
52
54
|
self.version_manager = AgentVersionManager()
|
|
53
55
|
|
|
56
|
+
def _read_template_version(self, template_path: Path) -> Optional[str]:
|
|
57
|
+
"""Read version from template file (supports both .md and .json formats).
|
|
58
|
+
|
|
59
|
+
For .md files: Extract version from YAML frontmatter
|
|
60
|
+
For .json files: Extract version from JSON structure
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
template_path: Path to template file
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Version string or None if version cannot be extracted
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
if template_path.suffix == ".md":
|
|
70
|
+
# Parse markdown with YAML frontmatter
|
|
71
|
+
content = template_path.read_text()
|
|
72
|
+
|
|
73
|
+
# Extract YAML frontmatter (between --- markers)
|
|
74
|
+
if not content.strip().startswith("---"):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
parts = content.split("---", 2)
|
|
78
|
+
if len(parts) < 3:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# Parse YAML frontmatter
|
|
82
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
83
|
+
if not frontmatter:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Extract version from frontmatter
|
|
87
|
+
version = frontmatter.get("version")
|
|
88
|
+
return version if version else None
|
|
89
|
+
|
|
90
|
+
if template_path.suffix == ".json":
|
|
91
|
+
# Parse JSON template
|
|
92
|
+
template_data = json.loads(template_path.read_text())
|
|
93
|
+
metadata = template_data.get("metadata", {})
|
|
94
|
+
version = (
|
|
95
|
+
template_data.get("agent_version")
|
|
96
|
+
or template_data.get("version")
|
|
97
|
+
or metadata.get("version")
|
|
98
|
+
)
|
|
99
|
+
return version if version else None
|
|
100
|
+
|
|
101
|
+
self.logger.warning(
|
|
102
|
+
f"Unknown template format: {template_path.suffix} for {template_path.name}"
|
|
103
|
+
)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
except yaml.YAMLError as e:
|
|
107
|
+
self.logger.warning(
|
|
108
|
+
f"Invalid YAML frontmatter in {template_path.name}: {e}"
|
|
109
|
+
)
|
|
110
|
+
return None
|
|
111
|
+
except json.JSONDecodeError as e:
|
|
112
|
+
self.logger.warning(f"Invalid JSON in {template_path.name}: {e}")
|
|
113
|
+
return None
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.logger.warning(
|
|
116
|
+
f"Error reading template version from {template_path.name}: {e}"
|
|
117
|
+
)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def _build_canonical_id_for_agent(self, agent_info: Dict[str, Any]) -> str:
|
|
121
|
+
"""Build or retrieve canonical_id for an agent.
|
|
122
|
+
|
|
123
|
+
NEW: Supports enhanced agent matching via canonical_id.
|
|
124
|
+
|
|
125
|
+
Priority:
|
|
126
|
+
1. Use existing canonical_id from agent_info if present
|
|
127
|
+
2. Generate from collection_id + agent_id if available
|
|
128
|
+
3. Fallback to legacy:{filename} for backward compatibility
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
agent_info: Agent dictionary with metadata
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Canonical ID string for matching
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
Remote agent: "bobmatnyc/claude-mpm-agents:pm"
|
|
138
|
+
Legacy agent: "legacy:custom-agent"
|
|
139
|
+
"""
|
|
140
|
+
# Priority 1: Existing canonical_id
|
|
141
|
+
if "canonical_id" in agent_info:
|
|
142
|
+
return agent_info["canonical_id"]
|
|
143
|
+
|
|
144
|
+
# Priority 2: Generate from collection_id + agent_id
|
|
145
|
+
collection_id = agent_info.get("collection_id")
|
|
146
|
+
agent_id = agent_info.get("agent_id")
|
|
147
|
+
|
|
148
|
+
if collection_id and agent_id:
|
|
149
|
+
canonical_id = f"{collection_id}:{agent_id}"
|
|
150
|
+
# Cache it in agent_info for future use
|
|
151
|
+
agent_info["canonical_id"] = canonical_id
|
|
152
|
+
return canonical_id
|
|
153
|
+
|
|
154
|
+
# Priority 3: Fallback to legacy format
|
|
155
|
+
# Use filename or agent name
|
|
156
|
+
agent_name = agent_info.get("name") or agent_info.get("metadata", {}).get(
|
|
157
|
+
"name", "unknown"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Extract filename from path
|
|
161
|
+
path_str = (
|
|
162
|
+
agent_info.get("path")
|
|
163
|
+
or agent_info.get("file_path")
|
|
164
|
+
or agent_info.get("source_file")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if path_str:
|
|
168
|
+
filename = Path(path_str).stem
|
|
169
|
+
canonical_id = f"legacy:{filename}"
|
|
170
|
+
else:
|
|
171
|
+
canonical_id = f"legacy:{agent_name}"
|
|
172
|
+
|
|
173
|
+
# Cache it
|
|
174
|
+
agent_info["canonical_id"] = canonical_id
|
|
175
|
+
return canonical_id
|
|
176
|
+
|
|
54
177
|
def discover_agents_from_all_sources(
|
|
55
178
|
self,
|
|
56
179
|
system_templates_dir: Optional[Path] = None,
|
|
@@ -156,11 +279,19 @@ class MultiSourceAgentDeploymentService:
|
|
|
156
279
|
agent_info["source"] = source_name
|
|
157
280
|
agent_info["source_dir"] = str(source_dir)
|
|
158
281
|
|
|
282
|
+
# NEW: Build canonical_id for enhanced matching
|
|
283
|
+
canonical_id = self._build_canonical_id_for_agent(agent_info)
|
|
284
|
+
|
|
285
|
+
# Group by canonical_id (PRIMARY) for enhanced matching
|
|
286
|
+
# This allows matching agents from different sources with same canonical_id
|
|
287
|
+
# while maintaining backward compatibility with name-based matching
|
|
288
|
+
matching_key = canonical_id
|
|
289
|
+
|
|
159
290
|
# Initialize list if this is the first occurrence of this agent
|
|
160
|
-
if
|
|
161
|
-
agents_by_name[
|
|
291
|
+
if matching_key not in agents_by_name:
|
|
292
|
+
agents_by_name[matching_key] = []
|
|
162
293
|
|
|
163
|
-
agents_by_name[
|
|
294
|
+
agents_by_name[matching_key].append(agent_info)
|
|
164
295
|
|
|
165
296
|
# Use more specific log message
|
|
166
297
|
self.logger.info(
|
|
@@ -189,6 +320,48 @@ class MultiSourceAgentDeploymentService:
|
|
|
189
320
|
|
|
190
321
|
return agents_by_name
|
|
191
322
|
|
|
323
|
+
def get_agents_by_collection(
|
|
324
|
+
self,
|
|
325
|
+
collection_id: str,
|
|
326
|
+
remote_agents_dir: Optional[Path] = None,
|
|
327
|
+
) -> List[Dict[str, Any]]:
|
|
328
|
+
"""Get all agents from a specific collection.
|
|
329
|
+
|
|
330
|
+
NEW: Enables collection-based agent selection.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
collection_id: Collection identifier (e.g., "bobmatnyc/claude-mpm-agents")
|
|
334
|
+
remote_agents_dir: Directory containing remote agents cache
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of agent dictionaries from the specified collection
|
|
338
|
+
|
|
339
|
+
Example:
|
|
340
|
+
>>> service = MultiSourceAgentDeploymentService()
|
|
341
|
+
>>> agents = service.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
|
|
342
|
+
>>> len(agents)
|
|
343
|
+
45
|
|
344
|
+
"""
|
|
345
|
+
if not remote_agents_dir:
|
|
346
|
+
cache_dir = Path.home() / ".claude-mpm" / "cache"
|
|
347
|
+
remote_agents_dir = cache_dir / "remote-agents"
|
|
348
|
+
|
|
349
|
+
if not remote_agents_dir.exists():
|
|
350
|
+
self.logger.warning(
|
|
351
|
+
f"Remote agents directory not found: {remote_agents_dir}"
|
|
352
|
+
)
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
# Use RemoteAgentDiscoveryService to get collection agents
|
|
356
|
+
remote_service = RemoteAgentDiscoveryService(remote_agents_dir)
|
|
357
|
+
collection_agents = remote_service.get_agents_by_collection(collection_id)
|
|
358
|
+
|
|
359
|
+
self.logger.info(
|
|
360
|
+
f"Retrieved {len(collection_agents)} agents from collection '{collection_id}'"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return collection_agents
|
|
364
|
+
|
|
192
365
|
def select_highest_version_agents(
|
|
193
366
|
self, agents_by_name: Dict[str, List[Dict[str, Any]]]
|
|
194
367
|
) -> Dict[str, Dict[str, Any]]:
|
|
@@ -720,17 +893,20 @@ class MultiSourceAgentDeploymentService:
|
|
|
720
893
|
comparison_results["needs_update"].append(agent_name)
|
|
721
894
|
continue
|
|
722
895
|
|
|
723
|
-
# Read template version
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
template_data.get("agent_version")
|
|
729
|
-
or template_data.get("version")
|
|
730
|
-
or metadata.get("version", "0.0.0")
|
|
896
|
+
# Read template version using format-aware helper
|
|
897
|
+
version_string = self._read_template_version(template_path)
|
|
898
|
+
if not version_string:
|
|
899
|
+
self.logger.warning(
|
|
900
|
+
f"Could not extract version from template for '{agent_name}', skipping"
|
|
731
901
|
)
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
try:
|
|
905
|
+
template_version = self.version_manager.parse_version(version_string)
|
|
732
906
|
except Exception as e:
|
|
733
|
-
self.logger.warning(
|
|
907
|
+
self.logger.warning(
|
|
908
|
+
f"Error parsing version '{version_string}' for '{agent_name}': {e}"
|
|
909
|
+
)
|
|
734
910
|
continue
|
|
735
911
|
|
|
736
912
|
# Read deployed version
|