claude-mpm 4.15.2__py3-none-any.whl → 4.15.6__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/agents/templates/agentic-coder-optimizer.json +9 -2
- claude_mpm/agents/templates/api_qa.json +7 -1
- claude_mpm/agents/templates/clerk-ops.json +8 -1
- claude_mpm/agents/templates/code_analyzer.json +4 -1
- claude_mpm/agents/templates/dart_engineer.json +11 -1
- claude_mpm/agents/templates/data_engineer.json +11 -1
- claude_mpm/agents/templates/documentation.json +6 -1
- claude_mpm/agents/templates/engineer.json +13 -0
- claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
- claude_mpm/agents/templates/golang_engineer.json +11 -1
- claude_mpm/agents/templates/java_engineer.json +12 -2
- claude_mpm/agents/templates/local_ops_agent.json +216 -37
- claude_mpm/agents/templates/nextjs_engineer.json +11 -1
- claude_mpm/agents/templates/ops.json +8 -1
- claude_mpm/agents/templates/php-engineer.json +11 -1
- claude_mpm/agents/templates/project_organizer.json +9 -2
- claude_mpm/agents/templates/prompt-engineer.json +5 -1
- claude_mpm/agents/templates/python_engineer.json +11 -1
- claude_mpm/agents/templates/qa.json +7 -1
- claude_mpm/agents/templates/react_engineer.json +11 -1
- claude_mpm/agents/templates/refactoring_engineer.json +8 -1
- claude_mpm/agents/templates/research.json +4 -1
- claude_mpm/agents/templates/ruby-engineer.json +11 -1
- claude_mpm/agents/templates/rust_engineer.json +11 -1
- claude_mpm/agents/templates/security.json +6 -1
- claude_mpm/agents/templates/ticketing.json +6 -1
- claude_mpm/agents/templates/typescript_engineer.json +11 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
- claude_mpm/agents/templates/version_control.json +8 -1
- claude_mpm/agents/templates/web_qa.json +7 -1
- claude_mpm/agents/templates/web_ui.json +11 -1
- claude_mpm/cli/commands/configure.py +164 -16
- claude_mpm/cli/commands/configure_agent_display.py +6 -6
- claude_mpm/cli/commands/configure_behavior_manager.py +8 -8
- claude_mpm/cli/commands/configure_navigation.py +20 -18
- claude_mpm/cli/commands/configure_startup_manager.py +14 -14
- claude_mpm/cli/commands/configure_template_editor.py +8 -8
- claude_mpm/cli/interactive/__init__.py +3 -0
- claude_mpm/cli/interactive/skills_wizard.py +491 -0
- claude_mpm/cli/startup.py +26 -0
- claude_mpm/core/enums.py +18 -0
- claude_mpm/core/types.py +2 -9
- claude_mpm/dashboard/static/js/dashboard.js +0 -14
- claude_mpm/dashboard/templates/index.html +3 -41
- claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
- claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
- claude_mpm/services/core/models/health.py +1 -28
- claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
- claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
- claude_mpm/services/infrastructure/monitoring/base.py +5 -13
- claude_mpm/services/infrastructure/monitoring/network.py +7 -6
- claude_mpm/services/infrastructure/monitoring/process.py +13 -12
- claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
- claude_mpm/services/infrastructure/monitoring/service.py +16 -15
- claude_mpm/services/local_ops/__init__.py +1 -1
- claude_mpm/services/local_ops/crash_detector.py +1 -1
- claude_mpm/services/local_ops/health_checks/http_check.py +2 -1
- claude_mpm/services/local_ops/health_checks/process_check.py +2 -1
- claude_mpm/services/local_ops/health_checks/resource_check.py +2 -1
- claude_mpm/services/local_ops/health_manager.py +1 -1
- claude_mpm/services/local_ops/restart_manager.py +1 -1
- claude_mpm/services/shared/async_service_base.py +16 -27
- claude_mpm/services/shared/lifecycle_service_base.py +1 -14
- claude_mpm/services/socketio/handlers/__init__.py +5 -2
- claude_mpm/services/socketio/handlers/hook.py +10 -0
- claude_mpm/services/socketio/handlers/registry.py +4 -2
- claude_mpm/services/socketio/server/main.py +7 -7
- claude_mpm/skills/__init__.py +21 -0
- claude_mpm/skills/bundled/__init__.py +6 -0
- claude_mpm/skills/registry.py +198 -0
- claude_mpm/skills/skill_manager.py +310 -0
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/METADATA +1 -1
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/RECORD +78 -80
- claude_mpm/dashboard/static/css/code-tree.css +0 -1639
- claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
- claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
- claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
- claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
- claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
- claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/WHEEL +0 -0
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/top_level.txt +0 -0
|
@@ -4,23 +4,12 @@ Base class for asynchronous services to reduce duplication.
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
-
from enum import Enum
|
|
8
7
|
from typing import Any, Dict, Optional
|
|
9
8
|
|
|
9
|
+
from ...core.enums import ServiceState
|
|
10
10
|
from ...core.mixins import LoggerMixin
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class AsyncServiceState(Enum):
|
|
14
|
-
"""Standard states for async services."""
|
|
15
|
-
|
|
16
|
-
UNINITIALIZED = "uninitialized"
|
|
17
|
-
INITIALIZING = "initializing"
|
|
18
|
-
RUNNING = "running"
|
|
19
|
-
STOPPING = "stopping"
|
|
20
|
-
STOPPED = "stopped"
|
|
21
|
-
ERROR = "error"
|
|
22
|
-
|
|
23
|
-
|
|
24
13
|
class AsyncServiceBase(LoggerMixin, ABC):
|
|
25
14
|
"""
|
|
26
15
|
Base class for asynchronous services.
|
|
@@ -45,7 +34,7 @@ class AsyncServiceBase(LoggerMixin, ABC):
|
|
|
45
34
|
self.config = config or {}
|
|
46
35
|
|
|
47
36
|
# State management
|
|
48
|
-
self._state =
|
|
37
|
+
self._state = ServiceState.UNINITIALIZED
|
|
49
38
|
self._state_lock = asyncio.Lock()
|
|
50
39
|
|
|
51
40
|
# Background tasks
|
|
@@ -57,19 +46,19 @@ class AsyncServiceBase(LoggerMixin, ABC):
|
|
|
57
46
|
self._error_count = 0
|
|
58
47
|
|
|
59
48
|
@property
|
|
60
|
-
def state(self) ->
|
|
49
|
+
def state(self) -> ServiceState:
|
|
61
50
|
"""Get current service state."""
|
|
62
51
|
return self._state
|
|
63
52
|
|
|
64
53
|
@property
|
|
65
54
|
def is_running(self) -> bool:
|
|
66
55
|
"""Check if service is running."""
|
|
67
|
-
return self._state ==
|
|
56
|
+
return self._state == ServiceState.RUNNING
|
|
68
57
|
|
|
69
58
|
@property
|
|
70
59
|
def is_healthy(self) -> bool:
|
|
71
60
|
"""Check if service is healthy."""
|
|
72
|
-
return self._state ==
|
|
61
|
+
return self._state == ServiceState.RUNNING and self._last_error is None
|
|
73
62
|
|
|
74
63
|
async def initialize(self) -> bool:
|
|
75
64
|
"""
|
|
@@ -79,22 +68,22 @@ class AsyncServiceBase(LoggerMixin, ABC):
|
|
|
79
68
|
True if initialization successful
|
|
80
69
|
"""
|
|
81
70
|
async with self._state_lock:
|
|
82
|
-
if self._state !=
|
|
71
|
+
if self._state != ServiceState.UNINITIALIZED:
|
|
83
72
|
self.logger.warning(f"Service {self.service_name} already initialized")
|
|
84
|
-
return self._state ==
|
|
73
|
+
return self._state == ServiceState.RUNNING
|
|
85
74
|
|
|
86
|
-
self._state =
|
|
75
|
+
self._state = ServiceState.INITIALIZING
|
|
87
76
|
self.logger.info(f"Initializing service: {self.service_name}")
|
|
88
77
|
|
|
89
78
|
try:
|
|
90
79
|
success = await self._do_initialize()
|
|
91
80
|
if success:
|
|
92
|
-
self._state =
|
|
81
|
+
self._state = ServiceState.RUNNING
|
|
93
82
|
self.logger.info(
|
|
94
83
|
f"Service {self.service_name} initialized successfully"
|
|
95
84
|
)
|
|
96
85
|
else:
|
|
97
|
-
self._state =
|
|
86
|
+
self._state = ServiceState.ERROR
|
|
98
87
|
self.logger.error(
|
|
99
88
|
f"Service {self.service_name} initialization failed"
|
|
100
89
|
)
|
|
@@ -102,7 +91,7 @@ class AsyncServiceBase(LoggerMixin, ABC):
|
|
|
102
91
|
return success
|
|
103
92
|
|
|
104
93
|
except Exception as e:
|
|
105
|
-
self._state =
|
|
94
|
+
self._state = ServiceState.ERROR
|
|
106
95
|
self._last_error = e
|
|
107
96
|
self._error_count += 1
|
|
108
97
|
self.logger.error(
|
|
@@ -114,10 +103,10 @@ class AsyncServiceBase(LoggerMixin, ABC):
|
|
|
114
103
|
async def shutdown(self) -> None:
|
|
115
104
|
"""Shutdown the service gracefully."""
|
|
116
105
|
async with self._state_lock:
|
|
117
|
-
if self._state in (
|
|
106
|
+
if self._state in (ServiceState.STOPPED, ServiceState.STOPPING):
|
|
118
107
|
return
|
|
119
108
|
|
|
120
|
-
self._state =
|
|
109
|
+
self._state = ServiceState.STOPPING
|
|
121
110
|
self.logger.info(f"Shutting down service: {self.service_name}")
|
|
122
111
|
|
|
123
112
|
try:
|
|
@@ -130,11 +119,11 @@ class AsyncServiceBase(LoggerMixin, ABC):
|
|
|
130
119
|
# Service-specific shutdown
|
|
131
120
|
await self._do_shutdown()
|
|
132
121
|
|
|
133
|
-
self._state =
|
|
122
|
+
self._state = ServiceState.STOPPED
|
|
134
123
|
self.logger.info(f"Service {self.service_name} shut down successfully")
|
|
135
124
|
|
|
136
125
|
except Exception as e:
|
|
137
|
-
self._state =
|
|
126
|
+
self._state = ServiceState.ERROR
|
|
138
127
|
self._last_error = e
|
|
139
128
|
self.logger.error(
|
|
140
129
|
f"Service {self.service_name} shutdown error: {e}", exc_info=True
|
|
@@ -146,7 +135,7 @@ class AsyncServiceBase(LoggerMixin, ABC):
|
|
|
146
135
|
await self.shutdown()
|
|
147
136
|
|
|
148
137
|
# Reset state for restart
|
|
149
|
-
self._state =
|
|
138
|
+
self._state = ServiceState.UNINITIALIZED
|
|
150
139
|
self._shutdown_event.clear()
|
|
151
140
|
self._last_error = None
|
|
152
141
|
|
|
@@ -4,25 +4,12 @@ Base class for services with complex lifecycle management.
|
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
-
from enum import Enum
|
|
8
7
|
from typing import Any, Dict, List, Optional
|
|
9
8
|
|
|
9
|
+
from ...core.enums import ServiceState
|
|
10
10
|
from ...core.mixins import LoggerMixin
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class ServiceState(Enum):
|
|
14
|
-
"""Standard service states."""
|
|
15
|
-
|
|
16
|
-
UNINITIALIZED = "uninitialized"
|
|
17
|
-
INITIALIZING = "initializing"
|
|
18
|
-
INITIALIZED = "initialized"
|
|
19
|
-
STARTING = "starting"
|
|
20
|
-
RUNNING = "running"
|
|
21
|
-
STOPPING = "stopping"
|
|
22
|
-
STOPPED = "stopped"
|
|
23
|
-
ERROR = "error"
|
|
24
|
-
|
|
25
|
-
|
|
26
13
|
class LifecycleServiceBase(LoggerMixin, ABC):
|
|
27
14
|
"""
|
|
28
15
|
Base class for services with complex lifecycle management.
|
|
@@ -7,7 +7,9 @@ and maintainability.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from .base import BaseEventHandler
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
# DISABLED: File Tree interface removed from dashboard
|
|
12
|
+
# from .code_analysis import CodeAnalysisEventHandler
|
|
11
13
|
from .connection import ConnectionEventHandler
|
|
12
14
|
from .file import FileEventHandler
|
|
13
15
|
from .git import GitEventHandler
|
|
@@ -17,7 +19,8 @@ from .registry import EventHandlerRegistry
|
|
|
17
19
|
|
|
18
20
|
__all__ = [
|
|
19
21
|
"BaseEventHandler",
|
|
20
|
-
|
|
22
|
+
# DISABLED: File Tree interface removed from dashboard
|
|
23
|
+
# "CodeAnalysisEventHandler",
|
|
21
24
|
"ConnectionEventHandler",
|
|
22
25
|
"EventHandlerRegistry",
|
|
23
26
|
"FileEventHandler",
|
|
@@ -61,6 +61,10 @@ class HookEventHandler(BaseEventHandler):
|
|
|
61
61
|
|
|
62
62
|
hook_data = data.get("data", {})
|
|
63
63
|
|
|
64
|
+
# Log hook event processing
|
|
65
|
+
tool_name = hook_data.get("tool_name", "N/A")
|
|
66
|
+
self.logger.info(f"Processing hook event: {hook_event} - tool: {tool_name}")
|
|
67
|
+
|
|
64
68
|
# Create properly formatted event for history
|
|
65
69
|
# Note: add_to_history expects the event data directly, not wrapped
|
|
66
70
|
history_event = {
|
|
@@ -77,6 +81,12 @@ class HookEventHandler(BaseEventHandler):
|
|
|
77
81
|
|
|
78
82
|
# Broadcast the original event to all connected clients
|
|
79
83
|
# (preserves all original fields)
|
|
84
|
+
connected_clients = (
|
|
85
|
+
len(self.server.clients) if hasattr(self.server, "clients") else 0
|
|
86
|
+
)
|
|
87
|
+
self.logger.info(
|
|
88
|
+
f"Broadcasting claude_event to {connected_clients} clients: {hook_event}"
|
|
89
|
+
)
|
|
80
90
|
await self.broadcast_event("claude_event", data)
|
|
81
91
|
|
|
82
92
|
# Track sessions based on hook events
|
|
@@ -15,7 +15,8 @@ if TYPE_CHECKING:
|
|
|
15
15
|
|
|
16
16
|
from ..server import SocketIOServer
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# DISABLED: File Tree interface removed from dashboard
|
|
19
|
+
# from .code_analysis import CodeAnalysisEventHandler
|
|
19
20
|
from .connection import ConnectionEventHandler
|
|
20
21
|
from .file import FileEventHandler
|
|
21
22
|
from .git import GitEventHandler
|
|
@@ -38,7 +39,8 @@ class EventHandlerRegistry:
|
|
|
38
39
|
HookEventHandler, # Hook events for session tracking
|
|
39
40
|
GitEventHandler, # Git operations
|
|
40
41
|
FileEventHandler, # File operations
|
|
41
|
-
|
|
42
|
+
# DISABLED: File Tree interface removed from dashboard
|
|
43
|
+
# CodeAnalysisEventHandler, # Code analysis for dashboard
|
|
42
44
|
ProjectEventHandler, # Project management (future)
|
|
43
45
|
MemoryEventHandler, # Memory management (future)
|
|
44
46
|
]
|
|
@@ -267,15 +267,15 @@ class SocketIOServer(SocketIOServiceInterface):
|
|
|
267
267
|
except Exception as e:
|
|
268
268
|
self.logger.error(f"Error during EventBus teardown: {e}")
|
|
269
269
|
|
|
270
|
-
# Stop
|
|
270
|
+
# Stop event handlers
|
|
271
271
|
if self.event_registry:
|
|
272
|
-
from ..handlers import
|
|
273
|
-
|
|
274
|
-
# Stop analysis runner
|
|
275
|
-
analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
|
|
276
|
-
if analysis_handler and hasattr(analysis_handler, "cleanup"):
|
|
277
|
-
analysis_handler.cleanup()
|
|
272
|
+
from ..handlers import ConnectionEventHandler
|
|
278
273
|
|
|
274
|
+
# DISABLED: File Tree interface removed from dashboard
|
|
275
|
+
# Stop analysis runner (code analysis handler is disabled)
|
|
276
|
+
# analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
|
|
277
|
+
# if analysis_handler and hasattr(analysis_handler, "cleanup"):
|
|
278
|
+
# analysis_handler.cleanup()
|
|
279
279
|
# Stop health monitoring in connection handler
|
|
280
280
|
conn_handler = self.event_registry.get_handler(ConnectionEventHandler)
|
|
281
281
|
if conn_handler and hasattr(conn_handler, "stop_health_monitoring"):
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude MPM Skills Package
|
|
3
|
+
|
|
4
|
+
Skills system for sharing common capabilities across agents.
|
|
5
|
+
This reduces redundancy by extracting shared patterns into reusable skills.
|
|
6
|
+
|
|
7
|
+
Skills can be:
|
|
8
|
+
- Bundled with MPM (in skills/bundled/)
|
|
9
|
+
- User-installed (in ~/.claude/skills/)
|
|
10
|
+
- Project-specific (in .claude/skills/)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .registry import Skill, SkillsRegistry, get_registry
|
|
14
|
+
from .skill_manager import SkillManager
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Skill",
|
|
18
|
+
"SkillManager",
|
|
19
|
+
"SkillsRegistry",
|
|
20
|
+
"get_registry",
|
|
21
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Skills registry - manages bundled and discovered skills."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
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
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Skill:
|
|
14
|
+
"""Represents a skill that can be used by agents."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
path: Path
|
|
18
|
+
content: str
|
|
19
|
+
source: str # 'bundled', 'user', or 'project'
|
|
20
|
+
description: str = ""
|
|
21
|
+
agent_types: List[str] = None # Which agent types can use this skill
|
|
22
|
+
|
|
23
|
+
def __post_init__(self):
|
|
24
|
+
"""Initialize agent_types list if not provided."""
|
|
25
|
+
if self.agent_types is None:
|
|
26
|
+
self.agent_types = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SkillsRegistry:
|
|
30
|
+
"""Registry for managing skills across all tiers."""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
"""Initialize the skills registry."""
|
|
34
|
+
self.skills: Dict[str, Skill] = {}
|
|
35
|
+
self._load_bundled_skills()
|
|
36
|
+
self._load_user_skills()
|
|
37
|
+
self._load_project_skills()
|
|
38
|
+
|
|
39
|
+
def _load_bundled_skills(self):
|
|
40
|
+
"""Load skills bundled with MPM."""
|
|
41
|
+
bundled_dir = Path(__file__).parent / "bundled"
|
|
42
|
+
if not bundled_dir.exists():
|
|
43
|
+
logger.warning(f"Bundled skills directory not found: {bundled_dir}")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
skill_count = 0
|
|
47
|
+
for skill_file in bundled_dir.glob("*.md"):
|
|
48
|
+
try:
|
|
49
|
+
skill_name = skill_file.stem
|
|
50
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
# Extract description from first paragraph if available
|
|
53
|
+
description = self._extract_description(content)
|
|
54
|
+
|
|
55
|
+
self.skills[skill_name] = Skill(
|
|
56
|
+
name=skill_name,
|
|
57
|
+
path=skill_file,
|
|
58
|
+
content=content,
|
|
59
|
+
source="bundled",
|
|
60
|
+
description=description,
|
|
61
|
+
)
|
|
62
|
+
skill_count += 1
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Error loading bundled skill {skill_file}: {e}")
|
|
65
|
+
|
|
66
|
+
logger.info(f"Loaded {skill_count} bundled skills")
|
|
67
|
+
|
|
68
|
+
def _load_user_skills(self):
|
|
69
|
+
"""Load user-installed skills from ~/.claude/skills/"""
|
|
70
|
+
user_skills_dir = Path.home() / ".claude" / "skills"
|
|
71
|
+
if not user_skills_dir.exists():
|
|
72
|
+
logger.debug("User skills directory not found, skipping")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
skill_count = 0
|
|
76
|
+
for skill_file in user_skills_dir.glob("*.md"):
|
|
77
|
+
try:
|
|
78
|
+
skill_name = skill_file.stem
|
|
79
|
+
# User skills override bundled skills
|
|
80
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
81
|
+
description = self._extract_description(content)
|
|
82
|
+
|
|
83
|
+
self.skills[skill_name] = Skill(
|
|
84
|
+
name=skill_name,
|
|
85
|
+
path=skill_file,
|
|
86
|
+
content=content,
|
|
87
|
+
source="user",
|
|
88
|
+
description=description,
|
|
89
|
+
)
|
|
90
|
+
skill_count += 1
|
|
91
|
+
logger.debug(f"User skill '{skill_name}' overrides bundled version")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Error loading user skill {skill_file}: {e}")
|
|
94
|
+
|
|
95
|
+
if skill_count > 0:
|
|
96
|
+
logger.info(f"Loaded {skill_count} user skills")
|
|
97
|
+
|
|
98
|
+
def _load_project_skills(self):
|
|
99
|
+
"""Load project-specific skills from .claude/skills/"""
|
|
100
|
+
project_skills_dir = Path.cwd() / ".claude" / "skills"
|
|
101
|
+
if not project_skills_dir.exists():
|
|
102
|
+
logger.debug("Project skills directory not found, skipping")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
skill_count = 0
|
|
106
|
+
for skill_file in project_skills_dir.glob("*.md"):
|
|
107
|
+
try:
|
|
108
|
+
skill_name = skill_file.stem
|
|
109
|
+
# Project skills override both user and bundled skills
|
|
110
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
111
|
+
description = self._extract_description(content)
|
|
112
|
+
|
|
113
|
+
self.skills[skill_name] = Skill(
|
|
114
|
+
name=skill_name,
|
|
115
|
+
path=skill_file,
|
|
116
|
+
content=content,
|
|
117
|
+
source="project",
|
|
118
|
+
description=description,
|
|
119
|
+
)
|
|
120
|
+
skill_count += 1
|
|
121
|
+
logger.debug(f"Project skill '{skill_name}' overrides other versions")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Error loading project skill {skill_file}: {e}")
|
|
124
|
+
|
|
125
|
+
if skill_count > 0:
|
|
126
|
+
logger.info(f"Loaded {skill_count} project skills")
|
|
127
|
+
|
|
128
|
+
def _extract_description(self, content: str) -> str:
|
|
129
|
+
"""Extract description from skill content (first paragraph or summary)."""
|
|
130
|
+
lines = content.strip().split("\n")
|
|
131
|
+
description_lines = []
|
|
132
|
+
|
|
133
|
+
# Skip title (first line starting with #)
|
|
134
|
+
start_idx = 0
|
|
135
|
+
if lines and lines[0].startswith("#"):
|
|
136
|
+
start_idx = 1
|
|
137
|
+
|
|
138
|
+
# Find first non-empty paragraph
|
|
139
|
+
for line in lines[start_idx:]:
|
|
140
|
+
line = line.strip()
|
|
141
|
+
if not line:
|
|
142
|
+
if description_lines:
|
|
143
|
+
break
|
|
144
|
+
continue
|
|
145
|
+
if line.startswith("#"):
|
|
146
|
+
break
|
|
147
|
+
description_lines.append(line)
|
|
148
|
+
|
|
149
|
+
return " ".join(description_lines)[:200] # Limit to 200 chars
|
|
150
|
+
|
|
151
|
+
def get_skill(self, name: str) -> Optional[Skill]:
|
|
152
|
+
"""Get a skill by name."""
|
|
153
|
+
return self.skills.get(name)
|
|
154
|
+
|
|
155
|
+
def list_skills(self, source: Optional[str] = None) -> List[Skill]:
|
|
156
|
+
"""List all skills, optionally filtered by source."""
|
|
157
|
+
if source:
|
|
158
|
+
return [s for s in self.skills.values() if s.source == source]
|
|
159
|
+
return list(self.skills.values())
|
|
160
|
+
|
|
161
|
+
def get_skills_for_agent(self, agent_type: str) -> List[Skill]:
|
|
162
|
+
"""
|
|
163
|
+
Get skills mapped to a specific agent type.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
agent_type: Agent type/ID (e.g., 'engineer', 'python_engineer')
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of skills applicable to this agent type
|
|
170
|
+
"""
|
|
171
|
+
# Filter skills that explicitly list this agent type
|
|
172
|
+
# If a skill has no agent_types specified, it's available to all agents
|
|
173
|
+
return [
|
|
174
|
+
skill
|
|
175
|
+
for skill in self.skills.values()
|
|
176
|
+
if not skill.agent_types or agent_type in skill.agent_types
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
def reload(self):
|
|
180
|
+
"""Reload all skills from disk."""
|
|
181
|
+
logger.info("Reloading skills registry...")
|
|
182
|
+
self.skills.clear()
|
|
183
|
+
self._load_bundled_skills()
|
|
184
|
+
self._load_user_skills()
|
|
185
|
+
self._load_project_skills()
|
|
186
|
+
logger.info(f"Skills registry reloaded with {len(self.skills)} skills")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Global registry instance (singleton pattern)
|
|
190
|
+
_registry: Optional[SkillsRegistry] = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_registry() -> SkillsRegistry:
|
|
194
|
+
"""Get the global skills registry (singleton)."""
|
|
195
|
+
global _registry
|
|
196
|
+
if _registry is None:
|
|
197
|
+
_registry = SkillsRegistry()
|
|
198
|
+
return _registry
|