claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- claude_mpm/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""MCP Service Registry for claude-mpm.
|
|
2
|
+
|
|
3
|
+
This module provides a registry of known MCP services with their
|
|
4
|
+
installation, configuration, and runtime requirements.
|
|
5
|
+
|
|
6
|
+
WHY: Centralizes MCP service definitions to enable enable/disable/list
|
|
7
|
+
operations with automatic configuration generation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InstallMethod(str, Enum):
|
|
16
|
+
"""Installation method for MCP services."""
|
|
17
|
+
|
|
18
|
+
UVX = "uvx"
|
|
19
|
+
PIPX = "pipx"
|
|
20
|
+
NPX = "npx"
|
|
21
|
+
PIP = "pip"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class MCPServiceDefinition:
|
|
26
|
+
"""Definition of an MCP service with all configuration requirements.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
name: Unique service identifier (e.g., "kuzu-memory")
|
|
30
|
+
package: PyPI/npm package name for installation
|
|
31
|
+
install_method: How to install (uvx, pipx, npx, pip)
|
|
32
|
+
command: Command to run the service
|
|
33
|
+
args: Default command arguments
|
|
34
|
+
required_env: Environment variables that must be set
|
|
35
|
+
optional_env: Environment variables that may be set
|
|
36
|
+
description: Human-readable description
|
|
37
|
+
env_defaults: Default values for optional env vars
|
|
38
|
+
enabled_by_default: Whether service is enabled by default
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
package: str
|
|
43
|
+
install_method: InstallMethod
|
|
44
|
+
command: str
|
|
45
|
+
args: list[str] = field(default_factory=list)
|
|
46
|
+
required_env: list[str] = field(default_factory=list)
|
|
47
|
+
optional_env: list[str] = field(default_factory=list)
|
|
48
|
+
description: str = ""
|
|
49
|
+
env_defaults: dict[str, str] = field(default_factory=dict)
|
|
50
|
+
enabled_by_default: bool = False
|
|
51
|
+
oauth_provider: str | None = None # "google", "microsoft", etc.
|
|
52
|
+
oauth_scopes: list[str] = field(default_factory=list) # OAuth scopes if applicable
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MCPServiceRegistry:
|
|
56
|
+
"""Registry of known MCP services.
|
|
57
|
+
|
|
58
|
+
Provides service lookup, configuration generation, and
|
|
59
|
+
enable/disable state management.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# Registry of all known MCP services
|
|
63
|
+
SERVICES: ClassVar[dict[str, MCPServiceDefinition]] = {}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def register(cls, service: MCPServiceDefinition) -> None:
|
|
67
|
+
"""Register a service definition."""
|
|
68
|
+
cls.SERVICES[service.name] = service
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get(cls, name: str) -> MCPServiceDefinition | None:
|
|
72
|
+
"""Get a service definition by name."""
|
|
73
|
+
return cls.SERVICES.get(name)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def list_all(cls) -> list[MCPServiceDefinition]:
|
|
77
|
+
"""List all registered services."""
|
|
78
|
+
return list(cls.SERVICES.values())
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def list_names(cls) -> list[str]:
|
|
82
|
+
"""List all registered service names."""
|
|
83
|
+
return list(cls.SERVICES.keys())
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def exists(cls, name: str) -> bool:
|
|
87
|
+
"""Check if a service exists in the registry."""
|
|
88
|
+
return name in cls.SERVICES
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_default_enabled(cls) -> list[MCPServiceDefinition]:
|
|
92
|
+
"""Get services that are enabled by default."""
|
|
93
|
+
return [s for s in cls.SERVICES.values() if s.enabled_by_default]
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def generate_config(
|
|
97
|
+
cls,
|
|
98
|
+
service: MCPServiceDefinition,
|
|
99
|
+
env_overrides: dict[str, str] | None = None,
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""Generate MCP configuration for a service.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
service: The service definition
|
|
105
|
+
env_overrides: Environment variable overrides
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Configuration dict suitable for .mcp.json or ~/.claude.json
|
|
109
|
+
"""
|
|
110
|
+
env = {}
|
|
111
|
+
|
|
112
|
+
# Add required env vars (must be provided or have defaults)
|
|
113
|
+
for var in service.required_env:
|
|
114
|
+
if env_overrides and var in env_overrides:
|
|
115
|
+
env[var] = env_overrides[var]
|
|
116
|
+
elif var in service.env_defaults:
|
|
117
|
+
env[var] = service.env_defaults[var]
|
|
118
|
+
# If required and not provided, leave it out - caller should validate
|
|
119
|
+
|
|
120
|
+
# Add optional env vars if provided or have defaults
|
|
121
|
+
for var in service.optional_env:
|
|
122
|
+
if env_overrides and var in env_overrides:
|
|
123
|
+
env[var] = env_overrides[var]
|
|
124
|
+
elif var in service.env_defaults:
|
|
125
|
+
env[var] = service.env_defaults[var]
|
|
126
|
+
|
|
127
|
+
config: dict = {
|
|
128
|
+
"command": service.command,
|
|
129
|
+
"args": service.args.copy(),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if env:
|
|
133
|
+
config["env"] = env
|
|
134
|
+
|
|
135
|
+
return config
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def validate_env(
|
|
139
|
+
cls, service: MCPServiceDefinition, env: dict[str, str]
|
|
140
|
+
) -> tuple[bool, list[str]]:
|
|
141
|
+
"""Validate that all required env vars are provided.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
service: The service definition
|
|
145
|
+
env: Environment variables to validate
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Tuple of (is_valid, list of missing required vars)
|
|
149
|
+
"""
|
|
150
|
+
missing = []
|
|
151
|
+
for var in service.required_env:
|
|
152
|
+
if var not in env and var not in service.env_defaults:
|
|
153
|
+
missing.append(var)
|
|
154
|
+
return len(missing) == 0, missing
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ============================================================================
|
|
158
|
+
# Service Definitions
|
|
159
|
+
# ============================================================================
|
|
160
|
+
|
|
161
|
+
# KuzuMemory - Project memory and context management
|
|
162
|
+
KUZU_MEMORY = MCPServiceDefinition(
|
|
163
|
+
name="kuzu-memory",
|
|
164
|
+
package="kuzu-memory",
|
|
165
|
+
install_method=InstallMethod.UVX,
|
|
166
|
+
command="uvx",
|
|
167
|
+
args=["kuzu-memory"],
|
|
168
|
+
required_env=[],
|
|
169
|
+
optional_env=["KUZU_DB_PATH", "KUZU_LOG_LEVEL"],
|
|
170
|
+
description="Project memory and context management with graph database",
|
|
171
|
+
env_defaults={},
|
|
172
|
+
enabled_by_default=True,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# MCP Ticketer - Ticket and project management
|
|
176
|
+
MCP_TICKETER = MCPServiceDefinition(
|
|
177
|
+
name="mcp-ticketer",
|
|
178
|
+
package="mcp-ticketer",
|
|
179
|
+
install_method=InstallMethod.UVX,
|
|
180
|
+
command="uvx",
|
|
181
|
+
args=["mcp-ticketer"],
|
|
182
|
+
required_env=[],
|
|
183
|
+
optional_env=["TICKETER_BACKEND", "GITHUB_TOKEN", "LINEAR_API_KEY"],
|
|
184
|
+
description="Ticket and project management integration",
|
|
185
|
+
env_defaults={},
|
|
186
|
+
enabled_by_default=True,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# MCP Vector Search - Code semantic search
|
|
190
|
+
MCP_VECTOR_SEARCH = MCPServiceDefinition(
|
|
191
|
+
name="mcp-vector-search",
|
|
192
|
+
package="mcp-vector-search",
|
|
193
|
+
install_method=InstallMethod.UVX,
|
|
194
|
+
command="uvx",
|
|
195
|
+
args=["mcp-vector-search"],
|
|
196
|
+
required_env=[],
|
|
197
|
+
optional_env=["VECTOR_SEARCH_INDEX_PATH"],
|
|
198
|
+
description="Semantic code search with vector embeddings",
|
|
199
|
+
env_defaults={},
|
|
200
|
+
enabled_by_default=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Google Workspace MCP - Google Drive, Docs, Sheets integration
|
|
204
|
+
# Package: https://pypi.org/project/workspace-mcp/
|
|
205
|
+
GOOGLE_WORKSPACE_MCP = MCPServiceDefinition(
|
|
206
|
+
name="workspace-mcp",
|
|
207
|
+
package="workspace-mcp",
|
|
208
|
+
install_method=InstallMethod.UVX,
|
|
209
|
+
command="uvx",
|
|
210
|
+
args=["workspace-mcp", "--tool-tier", "core"],
|
|
211
|
+
required_env=["GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_SECRET"],
|
|
212
|
+
optional_env=[
|
|
213
|
+
"OAUTHLIB_INSECURE_TRANSPORT",
|
|
214
|
+
"USER_GOOGLE_EMAIL",
|
|
215
|
+
"GOOGLE_PSE_API_KEY",
|
|
216
|
+
"GOOGLE_PSE_ENGINE_ID",
|
|
217
|
+
],
|
|
218
|
+
description="Google Workspace integration (Gmail, Calendar, Drive, Docs, Sheets, Slides)",
|
|
219
|
+
env_defaults={"OAUTHLIB_INSECURE_TRANSPORT": "1"},
|
|
220
|
+
enabled_by_default=False,
|
|
221
|
+
oauth_provider="google",
|
|
222
|
+
oauth_scopes=[
|
|
223
|
+
"openid",
|
|
224
|
+
"email",
|
|
225
|
+
"profile",
|
|
226
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
227
|
+
"https://www.googleapis.com/auth/calendar",
|
|
228
|
+
"https://www.googleapis.com/auth/drive",
|
|
229
|
+
"https://www.googleapis.com/auth/documents",
|
|
230
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
231
|
+
],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# MCP GitHub - GitHub repository integration (future)
|
|
235
|
+
MCP_GITHUB = MCPServiceDefinition(
|
|
236
|
+
name="mcp-github",
|
|
237
|
+
package="@modelcontextprotocol/server-github",
|
|
238
|
+
install_method=InstallMethod.NPX,
|
|
239
|
+
command="npx",
|
|
240
|
+
args=["-y", "@modelcontextprotocol/server-github"],
|
|
241
|
+
required_env=["GITHUB_PERSONAL_ACCESS_TOKEN"],
|
|
242
|
+
optional_env=[],
|
|
243
|
+
description="GitHub repository integration",
|
|
244
|
+
env_defaults={},
|
|
245
|
+
enabled_by_default=False,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# MCP Filesystem - Local filesystem access (future)
|
|
249
|
+
MCP_FILESYSTEM = MCPServiceDefinition(
|
|
250
|
+
name="mcp-filesystem",
|
|
251
|
+
package="@modelcontextprotocol/server-filesystem",
|
|
252
|
+
install_method=InstallMethod.NPX,
|
|
253
|
+
command="npx",
|
|
254
|
+
args=["-y", "@modelcontextprotocol/server-filesystem"],
|
|
255
|
+
required_env=[],
|
|
256
|
+
optional_env=["FILESYSTEM_ROOT_PATH"],
|
|
257
|
+
description="Local filesystem access and management",
|
|
258
|
+
env_defaults={},
|
|
259
|
+
enabled_by_default=False,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# MCP Skillset - Skills and knowledge management
|
|
263
|
+
MCP_SKILLSET = MCPServiceDefinition(
|
|
264
|
+
name="mcp-skillset",
|
|
265
|
+
package="mcp-skillset",
|
|
266
|
+
install_method=InstallMethod.UVX,
|
|
267
|
+
command="uvx",
|
|
268
|
+
args=["mcp-skillset"],
|
|
269
|
+
required_env=[],
|
|
270
|
+
optional_env=["SKILLSET_PATH", "SKILLSET_LOG_LEVEL"],
|
|
271
|
+
description="Skills and knowledge management for Claude",
|
|
272
|
+
env_defaults={},
|
|
273
|
+
enabled_by_default=True,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# Register all services
|
|
278
|
+
def _register_builtin_services() -> None:
|
|
279
|
+
"""Register all built-in service definitions."""
|
|
280
|
+
services = [
|
|
281
|
+
KUZU_MEMORY,
|
|
282
|
+
MCP_TICKETER,
|
|
283
|
+
MCP_VECTOR_SEARCH,
|
|
284
|
+
GOOGLE_WORKSPACE_MCP,
|
|
285
|
+
MCP_GITHUB,
|
|
286
|
+
MCP_FILESYSTEM,
|
|
287
|
+
MCP_SKILLSET,
|
|
288
|
+
]
|
|
289
|
+
for service in services:
|
|
290
|
+
MCPServiceRegistry.register(service)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# Auto-register on module import
|
|
294
|
+
_register_builtin_services()
|
|
@@ -576,8 +576,13 @@ class UnifiedMonitorServer:
|
|
|
576
576
|
event = data.get("event", "claude_event")
|
|
577
577
|
event_data = data.get("data", {})
|
|
578
578
|
|
|
579
|
+
# Extract actual event name from subtype or type within data
|
|
580
|
+
actual_event = (
|
|
581
|
+
event_data.get("subtype") or event_data.get("type") or event
|
|
582
|
+
)
|
|
583
|
+
|
|
579
584
|
# Categorize event and wrap in expected format
|
|
580
|
-
event_type = self._categorize_event(
|
|
585
|
+
event_type = self._categorize_event(actual_event)
|
|
581
586
|
wrapped_event = {
|
|
582
587
|
"type": event_type,
|
|
583
588
|
"subtype": event,
|
|
@@ -71,6 +71,8 @@ REQUIRED_PM_SKILLS = [
|
|
|
71
71
|
"mpm-circuit-breaker-enforcement",
|
|
72
72
|
"mpm-tool-usage-guide",
|
|
73
73
|
"mpm-session-management",
|
|
74
|
+
"mpm-session-pause",
|
|
75
|
+
"mpm-session-resume",
|
|
74
76
|
]
|
|
75
77
|
|
|
76
78
|
# Tier 2: Recommended skills (deployed with standard install)
|
|
@@ -78,7 +80,6 @@ REQUIRED_PM_SKILLS = [
|
|
|
78
80
|
RECOMMENDED_PM_SKILLS = [
|
|
79
81
|
"mpm-config",
|
|
80
82
|
"mpm-ticket-view",
|
|
81
|
-
"mpm-session-resume",
|
|
82
83
|
"mpm-postmortem",
|
|
83
84
|
]
|
|
84
85
|
|
|
@@ -389,8 +390,9 @@ class PMSkillsDeployerService(LoggerMixin):
|
|
|
389
390
|
if not skill_dir.is_dir() or skill_dir.name.startswith("."):
|
|
390
391
|
continue
|
|
391
392
|
|
|
392
|
-
# Only process mpm
|
|
393
|
-
|
|
393
|
+
# Only process mpm* skills (framework management)
|
|
394
|
+
# Note: Includes both 'mpm' (core skill) and 'mpm-*' (other PM skills)
|
|
395
|
+
if not skill_dir.name.startswith("mpm"):
|
|
394
396
|
self.logger.debug(f"Skipping non-mpm skill: {skill_dir.name}")
|
|
395
397
|
continue
|
|
396
398
|
|
|
@@ -16,6 +16,7 @@ Trade-offs:
|
|
|
16
16
|
- Flexibility: Easy to extend with skills-specific features
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
import os
|
|
19
20
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
20
21
|
from datetime import datetime, timezone
|
|
21
22
|
from pathlib import Path
|
|
@@ -32,6 +33,46 @@ from claude_mpm.services.skills.skill_discovery_service import SkillDiscoverySer
|
|
|
32
33
|
logger = get_logger(__name__)
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
def _get_github_token(source: Optional[SkillSource] = None) -> Optional[str]:
|
|
37
|
+
"""Get GitHub token with source-specific override support.
|
|
38
|
+
|
|
39
|
+
Priority: source.token > GITHUB_TOKEN > GH_TOKEN
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
source: Optional SkillSource to check for per-source token
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
GitHub token if found, None otherwise
|
|
46
|
+
|
|
47
|
+
Token Resolution:
|
|
48
|
+
1. If source has token starting with "$", resolve as env var
|
|
49
|
+
2. If source has direct token, use it (not recommended for security)
|
|
50
|
+
3. Fall back to GITHUB_TOKEN env var
|
|
51
|
+
4. Fall back to GH_TOKEN env var
|
|
52
|
+
5. Return None if no token found
|
|
53
|
+
|
|
54
|
+
Security Note:
|
|
55
|
+
Token is never logged or printed to avoid exposure.
|
|
56
|
+
Direct tokens in config are discouraged - use env var refs ($VAR_NAME).
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> source = SkillSource(..., token="$PRIVATE_TOKEN")
|
|
60
|
+
>>> token = _get_github_token(source) # Resolves $PRIVATE_TOKEN from env
|
|
61
|
+
>>> token = _get_github_token() # Falls back to GITHUB_TOKEN
|
|
62
|
+
"""
|
|
63
|
+
# Priority 1: Per-source token (env var reference or direct)
|
|
64
|
+
if source and source.token:
|
|
65
|
+
if source.token.startswith("$"):
|
|
66
|
+
# Env var reference: $VAR_NAME -> os.environ.get("VAR_NAME")
|
|
67
|
+
env_var_name = source.token[1:]
|
|
68
|
+
return os.environ.get(env_var_name)
|
|
69
|
+
# Direct token (not recommended but supported)
|
|
70
|
+
return source.token
|
|
71
|
+
|
|
72
|
+
# Priority 2-3: Global environment variables
|
|
73
|
+
return os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
74
|
+
|
|
75
|
+
|
|
35
76
|
class GitSkillSourceManager:
|
|
36
77
|
"""Manages multiple Git-based skill sources with priority resolution.
|
|
37
78
|
|
|
@@ -217,9 +258,21 @@ class GitSkillSourceManager:
|
|
|
217
258
|
)
|
|
218
259
|
|
|
219
260
|
# Discover skills in cache
|
|
261
|
+
self.logger.debug(f"Scanning cache path for skills: {cache_path}")
|
|
220
262
|
discovery_service = SkillDiscoveryService(cache_path)
|
|
221
263
|
discovered_skills = discovery_service.discover_skills()
|
|
222
264
|
|
|
265
|
+
# Log discovery results
|
|
266
|
+
if len(discovered_skills) == 0:
|
|
267
|
+
self.logger.info(
|
|
268
|
+
f"No SKILL.md files found in {cache_path}. "
|
|
269
|
+
"Ensure your skill source has SKILL.md files with valid frontmatter."
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
self.logger.debug(
|
|
273
|
+
f"Successfully parsed {len(discovered_skills)} skills from {cache_path}"
|
|
274
|
+
)
|
|
275
|
+
|
|
223
276
|
# Build result
|
|
224
277
|
result = {
|
|
225
278
|
"synced": True,
|
|
@@ -469,7 +522,7 @@ class GitSkillSourceManager:
|
|
|
469
522
|
# Step 1: Discover all files via GitHub Tree API (single request)
|
|
470
523
|
# This discovers the COMPLETE repository structure (272 files for skills)
|
|
471
524
|
all_files = self._discover_repository_files_via_tree_api(
|
|
472
|
-
owner_repo, source.branch
|
|
525
|
+
owner_repo, source.branch, source
|
|
473
526
|
)
|
|
474
527
|
|
|
475
528
|
if not all_files:
|
|
@@ -504,7 +557,7 @@ class GitSkillSourceManager:
|
|
|
504
557
|
raw_url = f"https://raw.githubusercontent.com/{owner_repo}/{source.branch}/{file_path}"
|
|
505
558
|
cache_file = cache_path / file_path
|
|
506
559
|
future = executor.submit(
|
|
507
|
-
self._download_file_with_etag, raw_url, cache_file, force
|
|
560
|
+
self._download_file_with_etag, raw_url, cache_file, force, source
|
|
508
561
|
)
|
|
509
562
|
future_to_file[future] = file_path
|
|
510
563
|
|
|
@@ -533,7 +586,7 @@ class GitSkillSourceManager:
|
|
|
533
586
|
return files_updated, files_cached
|
|
534
587
|
|
|
535
588
|
def _discover_repository_files_via_tree_api(
|
|
536
|
-
self, owner_repo: str, branch: str
|
|
589
|
+
self, owner_repo: str, branch: str, source: Optional[SkillSource] = None
|
|
537
590
|
) -> List[str]:
|
|
538
591
|
"""Discover all files in repository using GitHub Git Tree API.
|
|
539
592
|
|
|
@@ -596,9 +649,17 @@ class GitSkillSourceManager:
|
|
|
596
649
|
)
|
|
597
650
|
self.logger.debug(f"Fetching commit SHA from {refs_url}")
|
|
598
651
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
)
|
|
652
|
+
# Build headers with authentication if token available
|
|
653
|
+
headers = {"Accept": "application/vnd.github+json"}
|
|
654
|
+
token = _get_github_token(source)
|
|
655
|
+
if token:
|
|
656
|
+
headers["Authorization"] = f"token {token}"
|
|
657
|
+
if source and source.token:
|
|
658
|
+
self.logger.debug(f"Using source-specific token for {source.id}")
|
|
659
|
+
else:
|
|
660
|
+
self.logger.debug("Using GitHub token for authentication")
|
|
661
|
+
|
|
662
|
+
refs_response = requests.get(refs_url, headers=headers, timeout=30)
|
|
602
663
|
|
|
603
664
|
# Check for rate limiting
|
|
604
665
|
if refs_response.status_code == 403:
|
|
@@ -621,7 +682,7 @@ class GitSkillSourceManager:
|
|
|
621
682
|
self.logger.debug(f"Fetching recursive tree from {tree_url}")
|
|
622
683
|
tree_response = requests.get(
|
|
623
684
|
tree_url,
|
|
624
|
-
headers=
|
|
685
|
+
headers=headers, # Reuse headers with auth from Step 1
|
|
625
686
|
params=params,
|
|
626
687
|
timeout=30,
|
|
627
688
|
)
|
|
@@ -652,7 +713,11 @@ class GitSkillSourceManager:
|
|
|
652
713
|
return all_files
|
|
653
714
|
|
|
654
715
|
def _download_file_with_etag(
|
|
655
|
-
self,
|
|
716
|
+
self,
|
|
717
|
+
url: str,
|
|
718
|
+
local_path: Path,
|
|
719
|
+
force: bool = False,
|
|
720
|
+
source: Optional[SkillSource] = None,
|
|
656
721
|
) -> bool:
|
|
657
722
|
"""Download file from URL with ETag caching (thread-safe).
|
|
658
723
|
|
|
@@ -660,6 +725,7 @@ class GitSkillSourceManager:
|
|
|
660
725
|
url: Raw GitHub URL
|
|
661
726
|
local_path: Local file path to save to
|
|
662
727
|
force: Force download even if cached
|
|
728
|
+
source: Optional SkillSource for token resolution
|
|
663
729
|
|
|
664
730
|
Returns:
|
|
665
731
|
True if file was updated, False if cached
|
|
@@ -692,6 +758,11 @@ class GitSkillSourceManager:
|
|
|
692
758
|
if cached_etag and not force:
|
|
693
759
|
headers["If-None-Match"] = cached_etag
|
|
694
760
|
|
|
761
|
+
# Add GitHub authentication if token available
|
|
762
|
+
token = _get_github_token(source)
|
|
763
|
+
if token:
|
|
764
|
+
headers["Authorization"] = f"token {token}"
|
|
765
|
+
|
|
695
766
|
try:
|
|
696
767
|
response = requests.get(url, headers=headers, timeout=30)
|
|
697
768
|
|
|
@@ -50,6 +50,22 @@ logger = get_logger(__name__)
|
|
|
50
50
|
# Deployment tracking index file
|
|
51
51
|
DEPLOYED_INDEX_FILE = ".mpm-deployed-skills.json"
|
|
52
52
|
|
|
53
|
+
# Core PM skills that should always be deployed
|
|
54
|
+
# These are referenced in PM_INSTRUCTIONS.md with [SKILL: name] markers
|
|
55
|
+
# Without these skills, PM only sees placeholders, not actual content
|
|
56
|
+
PM_CORE_SKILLS = {
|
|
57
|
+
"mpm-delegation-patterns",
|
|
58
|
+
"mpm-verification-protocols",
|
|
59
|
+
"mpm-tool-usage-guide",
|
|
60
|
+
"mpm-git-file-tracking",
|
|
61
|
+
"mpm-pr-workflow",
|
|
62
|
+
"mpm-ticketing-integration",
|
|
63
|
+
"mpm-teaching-mode",
|
|
64
|
+
"mpm-bug-reporting",
|
|
65
|
+
"mpm-circuit-breaker-enforcement",
|
|
66
|
+
"mpm-session-management",
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
# Core skills that are universally useful across all projects
|
|
54
70
|
# These are deployed when skill mapping returns too many skills (>60)
|
|
55
71
|
# Target: ~25-30 core skills for balanced functionality
|
|
@@ -376,6 +392,18 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
|
|
|
376
392
|
"(converted slashes to dashes)"
|
|
377
393
|
)
|
|
378
394
|
|
|
395
|
+
# Always include PM core skills to ensure PM_INSTRUCTIONS.md markers are resolved
|
|
396
|
+
# These skills are referenced in PM_INSTRUCTIONS.md and must be deployed
|
|
397
|
+
# for PM to see actual content instead of [SKILL: name] placeholders
|
|
398
|
+
before_pm_skills = len(normalized_skills)
|
|
399
|
+
normalized_skills = normalized_skills | PM_CORE_SKILLS
|
|
400
|
+
pm_skills_added = len(normalized_skills) - before_pm_skills
|
|
401
|
+
|
|
402
|
+
if pm_skills_added > 0:
|
|
403
|
+
logger.info(
|
|
404
|
+
f"Added {pm_skills_added} PM core skills to ensure PM_INSTRUCTIONS.md markers resolve"
|
|
405
|
+
)
|
|
406
|
+
|
|
379
407
|
return normalized_skills
|
|
380
408
|
|
|
381
409
|
|
|
@@ -188,6 +188,15 @@ class SkillDiscoveryService:
|
|
|
188
188
|
f"and {len(legacy_md_files)} legacy .md files in {self.skills_dir}"
|
|
189
189
|
)
|
|
190
190
|
|
|
191
|
+
# Log first few file paths for debugging
|
|
192
|
+
if all_skill_files:
|
|
193
|
+
sample_files = [
|
|
194
|
+
str(f.relative_to(self.skills_dir)) for f in all_skill_files[:5]
|
|
195
|
+
]
|
|
196
|
+
self.logger.debug(f"Sample skill files: {sample_files}")
|
|
197
|
+
else:
|
|
198
|
+
self.logger.debug(f"No SKILL.md or .md files found in {self.skills_dir}")
|
|
199
|
+
|
|
191
200
|
# Track deployment names to detect collisions
|
|
192
201
|
deployment_names = {}
|
|
193
202
|
|
|
@@ -226,7 +235,14 @@ class SkillDiscoveryService:
|
|
|
226
235
|
except Exception as e:
|
|
227
236
|
self.logger.warning(f"Failed to parse skill {skill_file}: {e}")
|
|
228
237
|
|
|
229
|
-
|
|
238
|
+
# Summary logging
|
|
239
|
+
parsed_count = len(skills)
|
|
240
|
+
failed_count = len(all_skill_files) - parsed_count
|
|
241
|
+
self.logger.info(
|
|
242
|
+
f"Discovered {parsed_count} skills from {self.skills_dir.name} "
|
|
243
|
+
f"({len(all_skill_files)} files found, {failed_count} failed to parse)"
|
|
244
|
+
)
|
|
245
|
+
|
|
230
246
|
return skills
|
|
231
247
|
|
|
232
248
|
def _parse_skill_file(self, skill_file: Path) -> Optional[Dict[str, Any]]:
|
|
@@ -28,7 +28,7 @@ References:
|
|
|
28
28
|
import json
|
|
29
29
|
import platform
|
|
30
30
|
import shutil
|
|
31
|
-
import subprocess
|
|
31
|
+
import subprocess # nosec B404 - subprocess needed for safe git operations
|
|
32
32
|
from pathlib import Path
|
|
33
33
|
from typing import Any, Dict, List, Optional
|
|
34
34
|
|
|
@@ -653,7 +653,7 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
653
653
|
f"Updating existing collection '{collection_name}' at {target_dir}"
|
|
654
654
|
)
|
|
655
655
|
try:
|
|
656
|
-
result = subprocess.run(
|
|
656
|
+
result = subprocess.run( # nosec B603 B607 - Safe: hardcoded git command
|
|
657
657
|
["git", "pull"],
|
|
658
658
|
cwd=target_dir,
|
|
659
659
|
capture_output=True,
|
|
@@ -684,7 +684,7 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
684
684
|
f"Installing new collection '{collection_name}' to {target_dir}"
|
|
685
685
|
)
|
|
686
686
|
try:
|
|
687
|
-
result = subprocess.run(
|
|
687
|
+
result = subprocess.run( # nosec B603 B607 - Safe: hardcoded git command
|
|
688
688
|
["git", "clone", repo_url, str(target_dir)],
|
|
689
689
|
capture_output=True,
|
|
690
690
|
text=True,
|
|
@@ -773,6 +773,32 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
773
773
|
if isinstance(skills_data, dict):
|
|
774
774
|
flat_skills = []
|
|
775
775
|
|
|
776
|
+
# Define valid top-level categories
|
|
777
|
+
VALID_CATEGORIES = {"universal", "toolchains"}
|
|
778
|
+
|
|
779
|
+
# Check for unknown categories and warn user
|
|
780
|
+
unknown_categories = set(skills_data.keys()) - VALID_CATEGORIES
|
|
781
|
+
if unknown_categories:
|
|
782
|
+
# Count skills in unknown categories
|
|
783
|
+
skipped_count = 0
|
|
784
|
+
for cat in unknown_categories:
|
|
785
|
+
cat_data = skills_data.get(cat, [])
|
|
786
|
+
if isinstance(cat_data, list):
|
|
787
|
+
skipped_count += len(cat_data)
|
|
788
|
+
elif isinstance(cat_data, dict):
|
|
789
|
+
# If it's a dict like toolchains, count nested skills
|
|
790
|
+
for skills_list in cat_data.values():
|
|
791
|
+
if isinstance(skills_list, list):
|
|
792
|
+
skipped_count += len(skills_list)
|
|
793
|
+
|
|
794
|
+
self.logger.warning(
|
|
795
|
+
f"Unknown categories in manifest will be skipped: "
|
|
796
|
+
f"{', '.join(sorted(unknown_categories))} ({skipped_count} skills)"
|
|
797
|
+
)
|
|
798
|
+
self.logger.info(
|
|
799
|
+
f"Valid top-level categories: {', '.join(sorted(VALID_CATEGORIES))}"
|
|
800
|
+
)
|
|
801
|
+
|
|
776
802
|
# Add universal skills
|
|
777
803
|
universal_skills = skills_data.get("universal", [])
|
|
778
804
|
if isinstance(universal_skills, list):
|
|
@@ -1022,12 +1048,12 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
1022
1048
|
"""
|
|
1023
1049
|
try:
|
|
1024
1050
|
if platform.system() == "Windows":
|
|
1025
|
-
result = subprocess.run(
|
|
1051
|
+
result = subprocess.run( # nosec B603 B607 - Safe: hardcoded tasklist command
|
|
1026
1052
|
["tasklist"], check=False, capture_output=True, text=True, timeout=5
|
|
1027
1053
|
)
|
|
1028
1054
|
return "claude" in result.stdout.lower()
|
|
1029
1055
|
# macOS and Linux
|
|
1030
|
-
result = subprocess.run(
|
|
1056
|
+
result = subprocess.run( # nosec B603 B607 - Safe: hardcoded ps command
|
|
1031
1057
|
["ps", "aux"], check=False, capture_output=True, text=True, timeout=5
|
|
1032
1058
|
)
|
|
1033
1059
|
# Look for "Claude Code" or "claude-code" process
|
claude_mpm/skills/__init__.py
CHANGED
|
@@ -24,7 +24,7 @@ Legacy System (maintained for compatibility):
|
|
|
24
24
|
from .agent_skills_injector import AgentSkillsInjector
|
|
25
25
|
|
|
26
26
|
# Legacy System (maintained for compatibility)
|
|
27
|
-
from .registry import Skill, SkillsRegistry, get_registry
|
|
27
|
+
from .registry import Skill, SkillsRegistry, get_registry, validate_agentskills_spec
|
|
28
28
|
from .skill_manager import SkillManager
|
|
29
29
|
from .skills_registry import SkillsRegistry as SkillsRegistryHelper
|
|
30
30
|
from .skills_service import SkillsService
|
|
@@ -39,4 +39,5 @@ __all__ = [
|
|
|
39
39
|
# New Skills Integration System
|
|
40
40
|
"SkillsService",
|
|
41
41
|
"get_registry",
|
|
42
|
+
"validate_agentskills_spec",
|
|
42
43
|
]
|