claude-mpm 3.9.2__py3-none-any.whl → 3.9.4__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/config/paths.py +56 -6
- claude_mpm/hooks/claude_hooks/hook_handler.py +167 -105
- claude_mpm/utils/paths.py +112 -8
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/METADATA +1 -1
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/RECORD +10 -10
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.9.
|
|
1
|
+
3.9.4
|
claude_mpm/config/paths.py
CHANGED
|
@@ -33,6 +33,7 @@ class ClaudeMPMPaths:
|
|
|
33
33
|
|
|
34
34
|
_instance: Optional['ClaudeMPMPaths'] = None
|
|
35
35
|
_project_root: Optional[Path] = None
|
|
36
|
+
_is_installed: bool = False
|
|
36
37
|
|
|
37
38
|
def __new__(cls) -> 'ClaudeMPMPaths':
|
|
38
39
|
"""Singleton pattern to ensure single instance."""
|
|
@@ -43,6 +44,7 @@ class ClaudeMPMPaths:
|
|
|
43
44
|
def __init__(self):
|
|
44
45
|
"""Initialize paths if not already done."""
|
|
45
46
|
if self._project_root is None:
|
|
47
|
+
self._is_installed = False
|
|
46
48
|
self._detect_project_root()
|
|
47
49
|
|
|
48
50
|
def _detect_project_root(self) -> None:
|
|
@@ -53,16 +55,29 @@ class ClaudeMPMPaths:
|
|
|
53
55
|
1. Look for definitive project markers (pyproject.toml, setup.py)
|
|
54
56
|
2. Look for combination of markers to ensure we're at the right level
|
|
55
57
|
3. Walk up from current file location
|
|
58
|
+
4. Handle both development and installed environments
|
|
56
59
|
"""
|
|
57
60
|
# Start from this file's location
|
|
58
61
|
current = Path(__file__).resolve()
|
|
59
62
|
|
|
60
|
-
#
|
|
63
|
+
# Check if we're in an installed environment (site-packages)
|
|
64
|
+
# In pip/pipx installs, the package is directly in site-packages
|
|
65
|
+
if 'site-packages' in str(current) or 'dist-packages' in str(current):
|
|
66
|
+
# We're in an installed environment
|
|
67
|
+
# The claude_mpm package directory itself is the "root" for resources
|
|
68
|
+
import claude_mpm
|
|
69
|
+
self._project_root = Path(claude_mpm.__file__).parent
|
|
70
|
+
self._is_installed = True
|
|
71
|
+
logger.debug(f"Installed environment detected, using package dir: {self._project_root}")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# We're in a development environment, look for project markers
|
|
61
75
|
for parent in current.parents:
|
|
62
76
|
# Check for definitive project root indicators
|
|
63
77
|
# Prioritize pyproject.toml and setup.py as they're only at root
|
|
64
78
|
if (parent / 'pyproject.toml').exists() or (parent / 'setup.py').exists():
|
|
65
79
|
self._project_root = parent
|
|
80
|
+
self._is_installed = False
|
|
66
81
|
logger.debug(f"Project root detected at: {parent} (found pyproject.toml or setup.py)")
|
|
67
82
|
return
|
|
68
83
|
|
|
@@ -70,6 +85,7 @@ class ClaudeMPMPaths:
|
|
|
70
85
|
# This combination is more likely to be the real project root
|
|
71
86
|
if (parent / '.git').exists() and (parent / 'VERSION').exists():
|
|
72
87
|
self._project_root = parent
|
|
88
|
+
self._is_installed = False
|
|
73
89
|
logger.debug(f"Project root detected at: {parent} (found .git and VERSION)")
|
|
74
90
|
return
|
|
75
91
|
|
|
@@ -77,12 +93,14 @@ class ClaudeMPMPaths:
|
|
|
77
93
|
for parent in current.parents:
|
|
78
94
|
if parent.name == 'claude-mpm':
|
|
79
95
|
self._project_root = parent
|
|
96
|
+
self._is_installed = False
|
|
80
97
|
logger.debug(f"Project root detected at: {parent} (by directory name)")
|
|
81
98
|
return
|
|
82
99
|
|
|
83
100
|
# Last resort fallback: 3 levels up from this file
|
|
84
101
|
# paths.py is in src/claude_mpm/config/
|
|
85
102
|
self._project_root = current.parent.parent.parent
|
|
103
|
+
self._is_installed = False
|
|
86
104
|
logger.warning(f"Project root fallback to: {self._project_root}")
|
|
87
105
|
|
|
88
106
|
@property
|
|
@@ -95,16 +113,26 @@ class ClaudeMPMPaths:
|
|
|
95
113
|
@property
|
|
96
114
|
def src_dir(self) -> Path:
|
|
97
115
|
"""Get the src directory."""
|
|
116
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
117
|
+
# In installed environment, there's no src directory
|
|
118
|
+
# Return the package directory itself
|
|
119
|
+
return self.project_root.parent
|
|
98
120
|
return self.project_root / "src"
|
|
99
121
|
|
|
100
122
|
@property
|
|
101
123
|
def claude_mpm_dir(self) -> Path:
|
|
102
124
|
"""Get the main claude_mpm package directory."""
|
|
125
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
126
|
+
# In installed environment, project_root IS the claude_mpm directory
|
|
127
|
+
return self.project_root
|
|
103
128
|
return self.src_dir / "claude_mpm"
|
|
104
129
|
|
|
105
130
|
@property
|
|
106
131
|
def agents_dir(self) -> Path:
|
|
107
132
|
"""Get the agents directory."""
|
|
133
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
134
|
+
# In installed environment, agents is directly under the package
|
|
135
|
+
return self.project_root / "agents"
|
|
108
136
|
return self.claude_mpm_dir / "agents"
|
|
109
137
|
|
|
110
138
|
@property
|
|
@@ -140,36 +168,58 @@ class ClaudeMPMPaths:
|
|
|
140
168
|
@property
|
|
141
169
|
def scripts_dir(self) -> Path:
|
|
142
170
|
"""Get the scripts directory."""
|
|
171
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
172
|
+
# In installed environment, scripts might be in a different location or not exist
|
|
173
|
+
# Return a path that won't cause issues but indicates it's not available
|
|
174
|
+
return Path.home() / '.claude-mpm' / 'scripts'
|
|
143
175
|
return self.project_root / "scripts"
|
|
144
176
|
|
|
145
177
|
@property
|
|
146
178
|
def tests_dir(self) -> Path:
|
|
147
179
|
"""Get the tests directory."""
|
|
180
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
181
|
+
# Tests aren't distributed with installed packages
|
|
182
|
+
return Path.home() / '.claude-mpm' / 'tests'
|
|
148
183
|
return self.project_root / "tests"
|
|
149
184
|
|
|
150
185
|
@property
|
|
151
186
|
def docs_dir(self) -> Path:
|
|
152
187
|
"""Get the documentation directory."""
|
|
188
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
189
|
+
# Docs might be installed separately or not at all
|
|
190
|
+
return Path.home() / '.claude-mpm' / 'docs'
|
|
153
191
|
return self.project_root / "docs"
|
|
154
192
|
|
|
155
193
|
@property
|
|
156
194
|
def logs_dir(self) -> Path:
|
|
157
195
|
"""Get the logs directory (creates if doesn't exist)."""
|
|
158
|
-
|
|
159
|
-
|
|
196
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
197
|
+
# Use user's home directory for logs in installed environment
|
|
198
|
+
logs = Path.home() / '.claude-mpm' / 'logs'
|
|
199
|
+
else:
|
|
200
|
+
logs = self.project_root / "logs"
|
|
201
|
+
logs.mkdir(parents=True, exist_ok=True)
|
|
160
202
|
return logs
|
|
161
203
|
|
|
162
204
|
@property
|
|
163
205
|
def temp_dir(self) -> Path:
|
|
164
206
|
"""Get the temporary files directory (creates if doesn't exist)."""
|
|
165
|
-
|
|
166
|
-
|
|
207
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
208
|
+
# Use user's home directory for temp files in installed environment
|
|
209
|
+
temp = Path.home() / '.claude-mpm' / '.tmp'
|
|
210
|
+
else:
|
|
211
|
+
temp = self.project_root / ".tmp"
|
|
212
|
+
temp.mkdir(parents=True, exist_ok=True)
|
|
167
213
|
return temp
|
|
168
214
|
|
|
169
215
|
@property
|
|
170
216
|
def claude_mpm_dir_hidden(self) -> Path:
|
|
171
217
|
"""Get the hidden .claude-mpm directory (creates if doesn't exist)."""
|
|
172
|
-
|
|
218
|
+
if hasattr(self, '_is_installed') and self._is_installed:
|
|
219
|
+
# Use current working directory in installed environment
|
|
220
|
+
hidden = Path.cwd() / ".claude-mpm"
|
|
221
|
+
else:
|
|
222
|
+
hidden = self.project_root / ".claude-mpm"
|
|
173
223
|
hidden.mkdir(exist_ok=True)
|
|
174
224
|
return hidden
|
|
175
225
|
|
|
@@ -21,6 +21,7 @@ import time
|
|
|
21
21
|
import asyncio
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from collections import deque
|
|
24
|
+
import threading
|
|
24
25
|
|
|
25
26
|
# Import constants for configuration
|
|
26
27
|
try:
|
|
@@ -87,6 +88,102 @@ except ImportError:
|
|
|
87
88
|
# No fallback needed - we only use Socket.IO now
|
|
88
89
|
|
|
89
90
|
|
|
91
|
+
|
|
92
|
+
class SocketIOConnectionPool:
|
|
93
|
+
"""Connection pool for Socket.IO clients to prevent connection leaks."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, max_connections=3):
|
|
96
|
+
self.max_connections = max_connections
|
|
97
|
+
self.connections = []
|
|
98
|
+
self.last_cleanup = time.time()
|
|
99
|
+
|
|
100
|
+
def get_connection(self, port):
|
|
101
|
+
"""Get or create a connection to the specified port."""
|
|
102
|
+
if time.time() - self.last_cleanup > 60:
|
|
103
|
+
self._cleanup_dead_connections()
|
|
104
|
+
self.last_cleanup = time.time()
|
|
105
|
+
|
|
106
|
+
for conn in self.connections:
|
|
107
|
+
if conn.get('port') == port and conn.get('client'):
|
|
108
|
+
client = conn['client']
|
|
109
|
+
if self._is_connection_alive(client):
|
|
110
|
+
return client
|
|
111
|
+
else:
|
|
112
|
+
self.connections.remove(conn)
|
|
113
|
+
|
|
114
|
+
if len(self.connections) < self.max_connections:
|
|
115
|
+
client = self._create_connection(port)
|
|
116
|
+
if client:
|
|
117
|
+
self.connections.append({
|
|
118
|
+
'port': port,
|
|
119
|
+
'client': client,
|
|
120
|
+
'created': time.time()
|
|
121
|
+
})
|
|
122
|
+
return client
|
|
123
|
+
|
|
124
|
+
if self.connections:
|
|
125
|
+
oldest = min(self.connections, key=lambda x: x['created'])
|
|
126
|
+
self._close_connection(oldest['client'])
|
|
127
|
+
oldest['client'] = self._create_connection(port)
|
|
128
|
+
oldest['port'] = port
|
|
129
|
+
oldest['created'] = time.time()
|
|
130
|
+
return oldest['client']
|
|
131
|
+
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def _create_connection(self, port):
|
|
135
|
+
"""Create a new Socket.IO connection."""
|
|
136
|
+
if not SOCKETIO_AVAILABLE:
|
|
137
|
+
return None
|
|
138
|
+
try:
|
|
139
|
+
client = socketio.Client(
|
|
140
|
+
reconnection=False, # Disable auto-reconnect
|
|
141
|
+
logger=False,
|
|
142
|
+
engineio_logger=False
|
|
143
|
+
)
|
|
144
|
+
client.connect(f'http://localhost:{port}',
|
|
145
|
+
wait=True,
|
|
146
|
+
wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT)
|
|
147
|
+
if client.connected:
|
|
148
|
+
return client
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def _is_connection_alive(self, client):
|
|
154
|
+
"""Check if a connection is still alive."""
|
|
155
|
+
try:
|
|
156
|
+
return client and client.connected
|
|
157
|
+
except:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def _close_connection(self, client):
|
|
161
|
+
"""Safely close a connection."""
|
|
162
|
+
try:
|
|
163
|
+
if client:
|
|
164
|
+
client.disconnect()
|
|
165
|
+
except:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def _cleanup_dead_connections(self):
|
|
169
|
+
"""Remove dead connections from the pool."""
|
|
170
|
+
self.connections = [
|
|
171
|
+
conn for conn in self.connections
|
|
172
|
+
if self._is_connection_alive(conn.get('client'))
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
def close_all(self):
|
|
176
|
+
"""Close all connections in the pool."""
|
|
177
|
+
for conn in self.connections:
|
|
178
|
+
self._close_connection(conn.get('client'))
|
|
179
|
+
self.connections.clear()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Global singleton handler instance
|
|
183
|
+
_global_handler = None
|
|
184
|
+
_handler_lock = threading.Lock()
|
|
185
|
+
|
|
186
|
+
|
|
90
187
|
class ClaudeHookHandler:
|
|
91
188
|
"""Optimized hook handler with direct Socket.IO client.
|
|
92
189
|
|
|
@@ -99,8 +196,17 @@ class ClaudeHookHandler:
|
|
|
99
196
|
|
|
100
197
|
def __init__(self):
|
|
101
198
|
# Socket.IO client (persistent if possible)
|
|
102
|
-
self.
|
|
103
|
-
|
|
199
|
+
self.connection_pool = SocketIOConnectionPool(max_connections=3)
|
|
200
|
+
# Track events for periodic cleanup
|
|
201
|
+
self.events_processed = 0
|
|
202
|
+
self.last_cleanup = time.time()
|
|
203
|
+
|
|
204
|
+
# Maximum sizes for tracking
|
|
205
|
+
self.MAX_DELEGATION_TRACKING = 200
|
|
206
|
+
self.MAX_PROMPT_TRACKING = 100
|
|
207
|
+
self.MAX_CACHE_AGE_SECONDS = 300
|
|
208
|
+
self.CLEANUP_INTERVAL_EVENTS = 100
|
|
209
|
+
|
|
104
210
|
|
|
105
211
|
# Agent delegation tracking
|
|
106
212
|
# Store recent Task delegations: session_id -> agent_type
|
|
@@ -179,6 +285,36 @@ class ClaudeHookHandler:
|
|
|
179
285
|
if key in self.delegation_requests:
|
|
180
286
|
del self.delegation_requests[key]
|
|
181
287
|
|
|
288
|
+
|
|
289
|
+
def _cleanup_old_entries(self):
|
|
290
|
+
"""Clean up old entries to prevent memory growth."""
|
|
291
|
+
cutoff_time = datetime.now().timestamp() - self.MAX_CACHE_AGE_SECONDS
|
|
292
|
+
|
|
293
|
+
# Clean up delegation tracking dictionaries
|
|
294
|
+
for storage in [self.active_delegations, self.delegation_requests]:
|
|
295
|
+
if len(storage) > self.MAX_DELEGATION_TRACKING:
|
|
296
|
+
# Keep only the most recent entries
|
|
297
|
+
sorted_keys = sorted(storage.keys())
|
|
298
|
+
excess = len(storage) - self.MAX_DELEGATION_TRACKING
|
|
299
|
+
for key in sorted_keys[:excess]:
|
|
300
|
+
del storage[key]
|
|
301
|
+
|
|
302
|
+
# Clean up pending prompts
|
|
303
|
+
if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
|
|
304
|
+
sorted_keys = sorted(self.pending_prompts.keys())
|
|
305
|
+
excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
|
|
306
|
+
for key in sorted_keys[:excess]:
|
|
307
|
+
del self.pending_prompts[key]
|
|
308
|
+
|
|
309
|
+
# Clean up git branch cache
|
|
310
|
+
expired_keys = [
|
|
311
|
+
key for key, cache_time in self._git_branch_cache_time.items()
|
|
312
|
+
if datetime.now().timestamp() - cache_time > self.MAX_CACHE_AGE_SECONDS
|
|
313
|
+
]
|
|
314
|
+
for key in expired_keys:
|
|
315
|
+
self._git_branch_cache.pop(key, None)
|
|
316
|
+
self._git_branch_cache_time.pop(key, None)
|
|
317
|
+
|
|
182
318
|
def _get_delegation_agent_type(self, session_id: str) -> str:
|
|
183
319
|
"""Get the agent type for a session's active delegation."""
|
|
184
320
|
# First try exact session match
|
|
@@ -455,97 +591,6 @@ class ClaudeHookHandler:
|
|
|
455
591
|
self._git_branch_cache_time[cache_key] = current_time
|
|
456
592
|
return 'Unknown'
|
|
457
593
|
|
|
458
|
-
def _get_socketio_client(self):
|
|
459
|
-
"""Get or create Socket.IO client with improved reliability.
|
|
460
|
-
|
|
461
|
-
WHY improved approach:
|
|
462
|
-
- Implements retry logic with exponential backoff
|
|
463
|
-
- Properly tests connection before returning
|
|
464
|
-
- Ensures connection persists across events
|
|
465
|
-
- Better error handling and recovery
|
|
466
|
-
"""
|
|
467
|
-
if not SOCKETIO_AVAILABLE:
|
|
468
|
-
return None
|
|
469
|
-
|
|
470
|
-
# Check if we have a connected client
|
|
471
|
-
if self.sio_client and self.sio_connected:
|
|
472
|
-
try:
|
|
473
|
-
# Test if still connected
|
|
474
|
-
if self.sio_client.connected:
|
|
475
|
-
return self.sio_client
|
|
476
|
-
else:
|
|
477
|
-
# Connection lost, clear it
|
|
478
|
-
if DEBUG:
|
|
479
|
-
print("Hook handler: Socket.IO connection lost, reconnecting...", file=sys.stderr)
|
|
480
|
-
self.sio_connected = False
|
|
481
|
-
except:
|
|
482
|
-
self.sio_connected = False
|
|
483
|
-
|
|
484
|
-
# Need to create or reconnect client
|
|
485
|
-
port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
|
|
486
|
-
max_retries = RetryConfig.MAX_RETRIES
|
|
487
|
-
retry_delay = RetryConfig.INITIAL_RETRY_DELAY
|
|
488
|
-
|
|
489
|
-
for attempt in range(max_retries):
|
|
490
|
-
try:
|
|
491
|
-
# Clean up old client if exists
|
|
492
|
-
if self.sio_client and not self.sio_connected:
|
|
493
|
-
try:
|
|
494
|
-
self.sio_client.disconnect()
|
|
495
|
-
except:
|
|
496
|
-
pass
|
|
497
|
-
self.sio_client = None
|
|
498
|
-
|
|
499
|
-
# Create new client
|
|
500
|
-
self.sio_client = socketio.Client(
|
|
501
|
-
reconnection=True, # Enable auto-reconnection
|
|
502
|
-
reconnection_attempts=3,
|
|
503
|
-
reconnection_delay=NetworkConfig.RECONNECTION_DELAY,
|
|
504
|
-
reconnection_delay_max=2,
|
|
505
|
-
logger=False,
|
|
506
|
-
engineio_logger=False
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
# Try to connect with proper wait
|
|
510
|
-
self.sio_client.connect(
|
|
511
|
-
f'http://localhost:{port}',
|
|
512
|
-
wait=True,
|
|
513
|
-
wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
# Verify connection
|
|
517
|
-
if self.sio_client.connected:
|
|
518
|
-
self.sio_connected = True
|
|
519
|
-
if DEBUG:
|
|
520
|
-
print(f"Hook handler: Successfully connected to Socket.IO server on port {port} (attempt {attempt + 1})", file=sys.stderr)
|
|
521
|
-
return self.sio_client
|
|
522
|
-
|
|
523
|
-
except Exception as e:
|
|
524
|
-
if DEBUG and attempt == max_retries - 1:
|
|
525
|
-
print(f"Hook handler: Failed to connect to Socket.IO after {max_retries} attempts: {e}", file=sys.stderr)
|
|
526
|
-
elif DEBUG:
|
|
527
|
-
print(f"Hook handler: Connection attempt {attempt + 1} failed, retrying...", file=sys.stderr)
|
|
528
|
-
|
|
529
|
-
# Exponential backoff with async delay
|
|
530
|
-
if attempt < max_retries - 1:
|
|
531
|
-
# Use asyncio.sleep if in async context, otherwise fall back to time.sleep
|
|
532
|
-
try:
|
|
533
|
-
loop = asyncio.get_event_loop()
|
|
534
|
-
if loop.is_running():
|
|
535
|
-
# We're in an async context, use async sleep
|
|
536
|
-
asyncio.create_task(asyncio.sleep(retry_delay))
|
|
537
|
-
else:
|
|
538
|
-
# Sync context, use regular sleep
|
|
539
|
-
time.sleep(retry_delay)
|
|
540
|
-
except:
|
|
541
|
-
# Fallback to sync sleep if asyncio not available
|
|
542
|
-
time.sleep(retry_delay)
|
|
543
|
-
retry_delay *= 2 # Double the delay for next attempt
|
|
544
|
-
|
|
545
|
-
# All attempts failed
|
|
546
|
-
self.sio_client = None
|
|
547
|
-
self.sio_connected = False
|
|
548
|
-
return None
|
|
549
594
|
|
|
550
595
|
def handle(self):
|
|
551
596
|
"""Process hook event with minimal overhead and zero blocking delays.
|
|
@@ -565,6 +610,13 @@ class ClaudeHookHandler:
|
|
|
565
610
|
self._continue_execution()
|
|
566
611
|
return
|
|
567
612
|
|
|
613
|
+
# Increment event counter and perform periodic cleanup
|
|
614
|
+
self.events_processed += 1
|
|
615
|
+
if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
|
|
616
|
+
self._cleanup_old_entries()
|
|
617
|
+
if DEBUG:
|
|
618
|
+
print(f"🧹 Performed cleanup after {self.events_processed} events", file=sys.stderr)
|
|
619
|
+
|
|
568
620
|
# Route event to appropriate handler
|
|
569
621
|
self._route_event(event)
|
|
570
622
|
|
|
@@ -647,7 +699,8 @@ class ClaudeHookHandler:
|
|
|
647
699
|
# The daemon should be running when manager is active
|
|
648
700
|
|
|
649
701
|
# Get Socket.IO client
|
|
650
|
-
|
|
702
|
+
port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
|
|
703
|
+
client = self.connection_pool.get_connection(port)
|
|
651
704
|
if not client:
|
|
652
705
|
if DEBUG:
|
|
653
706
|
print(f"Hook handler: No Socket.IO client available for event: hook.{event}", file=sys.stderr)
|
|
@@ -680,22 +733,18 @@ class ClaudeHookHandler:
|
|
|
680
733
|
print(f"✅ Successfully emitted Socket.IO event: hook.{event}", file=sys.stderr)
|
|
681
734
|
else:
|
|
682
735
|
print(f"⚠️ Event emitted but connection status uncertain: hook.{event}", file=sys.stderr)
|
|
683
|
-
self.sio_connected = False # Force reconnection next time
|
|
684
736
|
|
|
685
737
|
except Exception as e:
|
|
686
738
|
if DEBUG:
|
|
687
739
|
print(f"❌ Socket.IO emit failed for hook.{event}: {e}", file=sys.stderr)
|
|
688
|
-
# Mark as disconnected so next call will reconnect
|
|
689
|
-
self.sio_connected = False
|
|
690
740
|
|
|
691
741
|
# Try to reconnect immediately for critical events
|
|
692
742
|
if event in ['subagent_stop', 'pre_tool']:
|
|
693
743
|
if DEBUG:
|
|
694
744
|
print(f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}", file=sys.stderr)
|
|
695
|
-
# Clear the client to force reconnection
|
|
696
|
-
self.sio_client = None
|
|
697
745
|
# Try to get a new client and emit again
|
|
698
|
-
|
|
746
|
+
retry_port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
|
|
747
|
+
retry_client = self.connection_pool.get_connection(retry_port)
|
|
699
748
|
if retry_client:
|
|
700
749
|
try:
|
|
701
750
|
retry_client.emit('claude_event', claude_event_data)
|
|
@@ -1667,18 +1716,31 @@ class ClaudeHookHandler:
|
|
|
1667
1716
|
# Don't fail the delegation result - memory is optional
|
|
1668
1717
|
|
|
1669
1718
|
def __del__(self):
|
|
1670
|
-
"""Cleanup Socket.IO
|
|
1671
|
-
if self
|
|
1719
|
+
"""Cleanup Socket.IO connections on handler destruction."""
|
|
1720
|
+
if hasattr(self, 'connection_pool') and self.connection_pool:
|
|
1672
1721
|
try:
|
|
1673
|
-
self.
|
|
1722
|
+
self.connection_pool.close_all()
|
|
1674
1723
|
except:
|
|
1675
1724
|
pass
|
|
1676
1725
|
|
|
1677
1726
|
|
|
1678
1727
|
def main():
|
|
1679
|
-
"""Entry point with
|
|
1728
|
+
"""Entry point with singleton pattern to prevent multiple instances."""
|
|
1729
|
+
global _global_handler
|
|
1730
|
+
|
|
1680
1731
|
try:
|
|
1681
|
-
|
|
1732
|
+
# Use singleton pattern to prevent creating multiple instances
|
|
1733
|
+
with _handler_lock:
|
|
1734
|
+
if _global_handler is None:
|
|
1735
|
+
_global_handler = ClaudeHookHandler()
|
|
1736
|
+
if DEBUG:
|
|
1737
|
+
print(f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
|
|
1738
|
+
else:
|
|
1739
|
+
if DEBUG:
|
|
1740
|
+
print(f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
|
|
1741
|
+
|
|
1742
|
+
handler = _global_handler
|
|
1743
|
+
|
|
1682
1744
|
handler.handle()
|
|
1683
1745
|
except Exception as e:
|
|
1684
1746
|
# Always output continue action to not block Claude
|
claude_mpm/utils/paths.py
CHANGED
|
@@ -5,6 +5,7 @@ to avoid duplication across the codebase.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import sys
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Optional, Union, List
|
|
10
11
|
from functools import lru_cache
|
|
@@ -45,11 +46,17 @@ class PathResolver:
|
|
|
45
46
|
try:
|
|
46
47
|
import claude_mpm
|
|
47
48
|
module_path = Path(claude_mpm.__file__).parent
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
# Check if we're in a development environment (has src/ directory)
|
|
49
51
|
if module_path.parent.name == 'src':
|
|
52
|
+
# Development structure: project_root/src/claude_mpm/
|
|
50
53
|
return module_path.parent.parent
|
|
51
|
-
|
|
54
|
+
|
|
55
|
+
# Check if we're in an installed environment (site-packages)
|
|
56
|
+
# In this case, the module itself IS the framework root for resources
|
|
57
|
+
# No src/ directory exists in installed packages
|
|
52
58
|
return module_path
|
|
59
|
+
|
|
53
60
|
except ImportError:
|
|
54
61
|
pass
|
|
55
62
|
|
|
@@ -78,22 +85,50 @@ class PathResolver:
|
|
|
78
85
|
Raises:
|
|
79
86
|
FileNotFoundError: If agents directory doesn't exist
|
|
80
87
|
"""
|
|
88
|
+
# Try using importlib.resources first (Python 3.9+)
|
|
89
|
+
try:
|
|
90
|
+
if sys.version_info >= (3, 9):
|
|
91
|
+
from importlib.resources import files
|
|
92
|
+
agents_path = files('claude_mpm').joinpath('agents')
|
|
93
|
+
if agents_path.exists():
|
|
94
|
+
return Path(str(agents_path))
|
|
95
|
+
else:
|
|
96
|
+
# For Python 3.7-3.8, use the backport approach
|
|
97
|
+
from importlib import resources
|
|
98
|
+
with resources.path('claude_mpm', 'agents') as agents_path:
|
|
99
|
+
if agents_path.exists():
|
|
100
|
+
return agents_path
|
|
101
|
+
except (ImportError, ModuleNotFoundError, TypeError):
|
|
102
|
+
# Fall back to manual detection if importlib.resources fails
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
# Fallback to manual detection
|
|
81
106
|
framework_root = cls.get_framework_root()
|
|
82
107
|
|
|
83
|
-
#
|
|
84
|
-
|
|
108
|
+
# First check if we're in development structure (framework_root is project root)
|
|
109
|
+
# This happens when framework_root has a src/ directory
|
|
110
|
+
if (framework_root / 'src').exists():
|
|
111
|
+
agents_dir = framework_root / 'src' / 'claude_mpm' / 'agents'
|
|
112
|
+
if agents_dir.exists():
|
|
113
|
+
return agents_dir
|
|
114
|
+
|
|
115
|
+
# Otherwise we're in installed structure (framework_root is claude_mpm package)
|
|
116
|
+
# In pip/pipx installs, framework_root already points to claude_mpm package
|
|
117
|
+
agents_dir = framework_root / 'agents'
|
|
85
118
|
if agents_dir.exists():
|
|
86
119
|
return agents_dir
|
|
87
120
|
|
|
88
|
-
#
|
|
121
|
+
# Last fallback for edge cases
|
|
89
122
|
agents_dir = framework_root / 'claude_mpm' / 'agents'
|
|
90
123
|
if agents_dir.exists():
|
|
91
124
|
return agents_dir
|
|
92
125
|
|
|
93
126
|
raise FileNotFoundError(
|
|
94
127
|
f"Agents directory not found. Searched in:\n"
|
|
95
|
-
f" -
|
|
96
|
-
f" - {framework_root / 'claude_mpm' / 'agents'}"
|
|
128
|
+
f" - importlib.resources lookup\n"
|
|
129
|
+
f" - {framework_root / 'src' / 'claude_mpm' / 'agents'} (development)\n"
|
|
130
|
+
f" - {framework_root / 'agents'} (installed)\n"
|
|
131
|
+
f" - {framework_root / 'claude_mpm' / 'agents'} (fallback)"
|
|
97
132
|
)
|
|
98
133
|
|
|
99
134
|
@classmethod
|
|
@@ -260,6 +295,70 @@ class PathResolver:
|
|
|
260
295
|
logger.debug(f"Found {len(matches)} files matching '{pattern}' in {root}")
|
|
261
296
|
return matches
|
|
262
297
|
|
|
298
|
+
@classmethod
|
|
299
|
+
def get_package_resource_path(cls, resource_path: str) -> Path:
|
|
300
|
+
"""Get the path to a resource within the claude_mpm package.
|
|
301
|
+
|
|
302
|
+
This method handles both development and installed environments correctly.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
resource_path: Relative path within the claude_mpm package (e.g., 'agents/templates')
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Path: Full path to the resource
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
FileNotFoundError: If the resource doesn't exist
|
|
312
|
+
"""
|
|
313
|
+
# Try using importlib.resources first (most reliable for installed packages)
|
|
314
|
+
try:
|
|
315
|
+
if sys.version_info >= (3, 9):
|
|
316
|
+
from importlib.resources import files
|
|
317
|
+
resource = files('claude_mpm').joinpath(resource_path)
|
|
318
|
+
if resource.exists():
|
|
319
|
+
return Path(str(resource))
|
|
320
|
+
else:
|
|
321
|
+
# For Python 3.7-3.8
|
|
322
|
+
from importlib import resources
|
|
323
|
+
parts = resource_path.split('/')
|
|
324
|
+
if len(parts) == 1:
|
|
325
|
+
with resources.path('claude_mpm', parts[0]) as p:
|
|
326
|
+
if p.exists():
|
|
327
|
+
return p
|
|
328
|
+
else:
|
|
329
|
+
# For nested paths, navigate step by step
|
|
330
|
+
package = 'claude_mpm'
|
|
331
|
+
for part in parts[:-1]:
|
|
332
|
+
package = f"{package}.{part}"
|
|
333
|
+
with resources.path(package, parts[-1]) as p:
|
|
334
|
+
if p.exists():
|
|
335
|
+
return p
|
|
336
|
+
except (ImportError, ModuleNotFoundError, TypeError, AttributeError):
|
|
337
|
+
# Fall back to file system detection
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
# Fallback: Use file system detection
|
|
341
|
+
import claude_mpm
|
|
342
|
+
module_path = Path(claude_mpm.__file__).parent
|
|
343
|
+
resource = module_path / resource_path
|
|
344
|
+
|
|
345
|
+
if resource.exists():
|
|
346
|
+
return resource
|
|
347
|
+
|
|
348
|
+
# Try with framework root
|
|
349
|
+
framework_root = cls.get_framework_root()
|
|
350
|
+
if (framework_root / 'src').exists():
|
|
351
|
+
# Development environment
|
|
352
|
+
resource = framework_root / 'src' / 'claude_mpm' / resource_path
|
|
353
|
+
else:
|
|
354
|
+
# Installed environment
|
|
355
|
+
resource = framework_root / resource_path
|
|
356
|
+
|
|
357
|
+
if resource.exists():
|
|
358
|
+
return resource
|
|
359
|
+
|
|
360
|
+
raise FileNotFoundError(f"Resource not found: {resource_path}")
|
|
361
|
+
|
|
263
362
|
@classmethod
|
|
264
363
|
def clear_cache(cls):
|
|
265
364
|
"""Clear all cached path lookups.
|
|
@@ -288,4 +387,9 @@ def get_project_root() -> Path:
|
|
|
288
387
|
|
|
289
388
|
def find_file_upwards(filename: str, start_path: Optional[Path] = None) -> Optional[Path]:
|
|
290
389
|
"""Search for a file by traversing up the directory tree."""
|
|
291
|
-
return PathResolver.find_file_upwards(filename, start_path)
|
|
390
|
+
return PathResolver.find_file_upwards(filename, start_path)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_package_resource_path(resource_path: str) -> Path:
|
|
394
|
+
"""Get the path to a resource within the claude_mpm package."""
|
|
395
|
+
return PathResolver.get_package_resource_path(resource_path)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
claude_mpm/VERSION,sha256=
|
|
1
|
+
claude_mpm/VERSION,sha256=qncwOqyY7d82tgxVa7TxJStIKb52qA2UhTpaLs0fB2Y,6
|
|
2
2
|
claude_mpm/__init__.py,sha256=ix_J0PHZBz37nVBDEYJmLpwnURlWuBKKQ8rK_00TFpk,964
|
|
3
3
|
claude_mpm/__main__.py,sha256=8IcM9tEbTqSN_er04eKTPX3AGo6qzRiTnPI7KfIf7rw,641
|
|
4
4
|
claude_mpm/constants.py,sha256=xdYTQfOdrnp_fp2A-P4gA68X-XMq29cCwF6Xdwg3oQE,5217
|
|
@@ -69,7 +69,7 @@ claude_mpm/cli_module/commands.py,sha256=CBNfO-bXrZ0spjeW_7-swprEq5V4PQSc0qhl9SL
|
|
|
69
69
|
claude_mpm/cli_module/migration_example.py,sha256=C-_GbsW8dGzutnNeRLLld74ibDLyAOQx0stdpYZS0hs,6137
|
|
70
70
|
claude_mpm/config/__init__.py,sha256=KTJkOHVtC1t6goMkj2xaQWZKZfokEHNw0wxPE48bl1g,828
|
|
71
71
|
claude_mpm/config/agent_config.py,sha256=QS-25LSiNz4uOocjIM_FX_SGoRQHJOfkBZCKlz09K5k,13830
|
|
72
|
-
claude_mpm/config/paths.py,sha256=
|
|
72
|
+
claude_mpm/config/paths.py,sha256=MznUhL7kvpOhwqtNdOavEE1JvUauvfIN_8PUXl8MytU,12553
|
|
73
73
|
claude_mpm/config/socketio_config.py,sha256=rf5XKtdAKeypwZZPF8r6uSHnJHVXeMmX1YiYQmzUOZo,9727
|
|
74
74
|
claude_mpm/core/__init__.py,sha256=5Hdb4fLdMEqXdOeBJB0zFGJx8CxtaSRYIiD-foiz3yQ,874
|
|
75
75
|
claude_mpm/core/agent_name_normalizer.py,sha256=-X68oz3_74t9BRbHA54NEGyjy0xjTsGp_sCUHDtKp1s,9269
|
|
@@ -135,7 +135,7 @@ claude_mpm/hooks/memory_integration_hook.py,sha256=z0I5R4rsmLx3mzYf7QLeMTYbRShag
|
|
|
135
135
|
claude_mpm/hooks/tool_call_interceptor.py,sha256=08_Odgm6Sg1zBJhGjwzVa03AReeBPZHTjndyjEO99cY,7629
|
|
136
136
|
claude_mpm/hooks/validation_hooks.py,sha256=7TU2N4SzCm2nwpsR0xiNKsHQNsWODnOVAmK9jHq1QqM,6582
|
|
137
137
|
claude_mpm/hooks/claude_hooks/__init__.py,sha256=bMUwt2RzDGAcEbtDMA7vWS1uJsauOY0OixIe4pHwgQ0,129
|
|
138
|
-
claude_mpm/hooks/claude_hooks/hook_handler.py,sha256=
|
|
138
|
+
claude_mpm/hooks/claude_hooks/hook_handler.py,sha256=hMJRlFIpYtl3SGDMseS1_kiXMrzm_w1mFG-oqU5hJ8Q,79765
|
|
139
139
|
claude_mpm/hooks/claude_hooks/hook_handler_fixed.py,sha256=hYE5Tc4RaaK0kLmFNLVXvDhiuTLA40Gt3hmhCQUO5UA,15626
|
|
140
140
|
claude_mpm/hooks/claude_hooks/hook_wrapper.sh,sha256=JBbedWNs1EHaUsAkmqfPv_tWxV_DcRP707hma74oHU0,2370
|
|
141
141
|
claude_mpm/models/__init__.py,sha256=vy2NLX2KT9QeH76SjCYh9dOYKPLRgxGrnwkQFAg08gc,465
|
|
@@ -254,15 +254,15 @@ claude_mpm/utils/framework_detection.py,sha256=nzs1qRZK9K-zT0382z1FpGDvgzUNrUg8r
|
|
|
254
254
|
claude_mpm/utils/import_migration_example.py,sha256=W4a4XH3FY_VBB00BB8Lae2aRPM021PxLHzdUfEs0B5w,2463
|
|
255
255
|
claude_mpm/utils/imports.py,sha256=wX-SOXUHbemx01MHRGQpVwajzXH6qYdQkYNFCIbb2mw,6851
|
|
256
256
|
claude_mpm/utils/path_operations.py,sha256=6pLMnAWBVzHkgp6JyQHmHbGD-dWn-nX21yV4E_eT-kM,11614
|
|
257
|
-
claude_mpm/utils/paths.py,sha256=
|
|
257
|
+
claude_mpm/utils/paths.py,sha256=_4d2uV7QDLMeybSNch6B5H3s0cQ6Ii-idI8rFC-OCn4,14653
|
|
258
258
|
claude_mpm/utils/robust_installer.py,sha256=5-iW4Qpba4DBitx5Ie3uoUJgXBpbvuLUJ_uNGgOxwi4,19855
|
|
259
259
|
claude_mpm/utils/session_logging.py,sha256=9G0AzB7V0WkhLQlN0ocqbyDv0ifooEsJ5UPXIhA-wt0,3022
|
|
260
260
|
claude_mpm/validation/__init__.py,sha256=bJ19g9lnk7yIjtxzN8XPegp87HTFBzCrGQOpFgRTf3g,155
|
|
261
261
|
claude_mpm/validation/agent_validator.py,sha256=OEYhmy0K99pkoCCoVea2Q-d1JMiDyhEpzEJikuF8T-U,20910
|
|
262
262
|
claude_mpm/validation/frontmatter_validator.py,sha256=vSinu0XD9-31h0-ePYiYivBbxTZEanhymLinTCODr7k,7206
|
|
263
|
-
claude_mpm-3.9.
|
|
264
|
-
claude_mpm-3.9.
|
|
265
|
-
claude_mpm-3.9.
|
|
266
|
-
claude_mpm-3.9.
|
|
267
|
-
claude_mpm-3.9.
|
|
268
|
-
claude_mpm-3.9.
|
|
263
|
+
claude_mpm-3.9.4.dist-info/licenses/LICENSE,sha256=cSdDfXjoTVhstrERrqme4zgxAu4GubU22zVEHsiXGxs,1071
|
|
264
|
+
claude_mpm-3.9.4.dist-info/METADATA,sha256=_rGegraOba_N2dJxC3RuA0Y34dl5raiQVaxdnyNeq0g,8680
|
|
265
|
+
claude_mpm-3.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
266
|
+
claude_mpm-3.9.4.dist-info/entry_points.txt,sha256=3_d7wLrg9sRmQ1SfrFGWoTNL8Wrd6lQb2XVSYbTwRIg,324
|
|
267
|
+
claude_mpm-3.9.4.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
|
|
268
|
+
claude_mpm-3.9.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|