claude-mpm 4.2.9__py3-none-any.whl → 4.2.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/dashboard.py +59 -126
  3. claude_mpm/cli/commands/monitor.py +71 -212
  4. claude_mpm/cli/commands/run.py +33 -33
  5. claude_mpm/dashboard/static/css/code-tree.css +8 -16
  6. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  7. claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
  8. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  9. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  11. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  12. claude_mpm/dashboard/static/js/components/code-tree.js +692 -114
  13. claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
  14. claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
  15. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
  16. claude_mpm/dashboard/static/js/dashboard.js +108 -91
  17. claude_mpm/dashboard/static/js/socket-client.js +9 -7
  18. claude_mpm/dashboard/templates/index.html +2 -7
  19. claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
  20. claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
  21. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
  22. claude_mpm/services/agents/deployment/agent_template_builder.py +0 -1
  23. claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
  24. claude_mpm/services/monitor/__init__.py +20 -0
  25. claude_mpm/services/monitor/daemon.py +256 -0
  26. claude_mpm/services/monitor/event_emitter.py +279 -0
  27. claude_mpm/services/monitor/handlers/__init__.py +20 -0
  28. claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
  29. claude_mpm/services/monitor/handlers/dashboard.py +298 -0
  30. claude_mpm/services/monitor/handlers/hooks.py +491 -0
  31. claude_mpm/services/monitor/management/__init__.py +18 -0
  32. claude_mpm/services/monitor/management/health.py +124 -0
  33. claude_mpm/services/monitor/management/lifecycle.py +298 -0
  34. claude_mpm/services/monitor/server.py +442 -0
  35. claude_mpm/tools/code_tree_analyzer.py +33 -17
  36. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/METADATA +1 -1
  37. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/RECORD +41 -36
  38. claude_mpm/cli/commands/socketio_monitor.py +0 -233
  39. claude_mpm/scripts/socketio_daemon.py +0 -571
  40. claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
  41. claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
  42. claude_mpm/scripts/socketio_server_manager.py +0 -349
  43. claude_mpm/services/cli/dashboard_launcher.py +0 -423
  44. claude_mpm/services/cli/socketio_manager.py +0 -595
  45. claude_mpm/services/dashboard/stable_server.py +0 -1020
  46. claude_mpm/services/socketio/monitor_server.py +0 -505
  47. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/WHEEL +0 -0
  48. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/entry_points.txt +0 -0
  49. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/licenses/LICENSE +0 -0
  50. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,442 @@
1
+ """
2
+ Unified Monitor Server for Claude MPM
3
+ ====================================
4
+
5
+ WHY: This server combines HTTP dashboard serving and Socket.IO event handling
6
+ into a single, stable process. It uses real AST analysis instead of mock data
7
+ and provides all monitoring functionality on a single port.
8
+
9
+ DESIGN DECISIONS:
10
+ - Combines aiohttp HTTP server with Socket.IO server
11
+ - Uses real CodeTreeAnalyzer for AST analysis
12
+ - Single port (8765) for all functionality
13
+ - Event-driven architecture with proper handler registration
14
+ - Built for stability and daemon operation
15
+ """
16
+
17
+ import asyncio
18
+ import threading
19
+ from pathlib import Path
20
+ from typing import Dict
21
+
22
+ import socketio
23
+ from aiohttp import web
24
+
25
+ from ...core.logging_config import get_logger
26
+ from ...dashboard.api.simple_directory import list_directory
27
+ from .event_emitter import get_event_emitter
28
+ from .handlers.code_analysis import CodeAnalysisHandler
29
+ from .handlers.dashboard import DashboardHandler
30
+ from .handlers.hooks import HookHandler
31
+
32
+ # EventBus integration
33
+ try:
34
+ from ...services.event_bus import EventBus
35
+
36
+ EVENTBUS_AVAILABLE = True
37
+ except ImportError:
38
+ EventBus = None
39
+ EVENTBUS_AVAILABLE = False
40
+
41
+
42
+ class UnifiedMonitorServer:
43
+ """Unified server that combines HTTP dashboard and Socket.IO functionality.
44
+
45
+ WHY: Provides a single server process that handles all monitoring needs.
46
+ Replaces multiple competing server implementations with one stable solution.
47
+ """
48
+
49
+ def __init__(self, host: str = "localhost", port: int = 8765):
50
+ """Initialize the unified monitor server.
51
+
52
+ Args:
53
+ host: Host to bind to
54
+ port: Port to bind to
55
+ """
56
+ self.host = host
57
+ self.port = port
58
+ self.logger = get_logger(__name__)
59
+
60
+ # Core components
61
+ self.app = None
62
+ self.sio = None
63
+ self.runner = None
64
+ self.site = None
65
+
66
+ # Event handlers
67
+ self.code_analysis_handler = None
68
+ self.dashboard_handler = None
69
+ self.hook_handler = None
70
+
71
+ # High-performance event emitter
72
+ self.event_emitter = None
73
+
74
+ # State
75
+ self.running = False
76
+ self.loop = None
77
+ self.server_thread = None
78
+
79
+ def start(self) -> bool:
80
+ """Start the unified monitor server.
81
+
82
+ Returns:
83
+ True if started successfully, False otherwise
84
+ """
85
+ try:
86
+ self.logger.info(
87
+ f"Starting unified monitor server on {self.host}:{self.port}"
88
+ )
89
+
90
+ # Start in a separate thread to avoid blocking
91
+ self.server_thread = threading.Thread(target=self._run_server, daemon=True)
92
+ self.server_thread.start()
93
+
94
+ # Wait for server to start
95
+ import time
96
+
97
+ for _ in range(50): # Wait up to 5 seconds
98
+ if self.running:
99
+ break
100
+ time.sleep(0.1)
101
+
102
+ if not self.running:
103
+ self.logger.error("Server failed to start within timeout")
104
+ return False
105
+
106
+ self.logger.info("Unified monitor server started successfully")
107
+ return True
108
+
109
+ except Exception as e:
110
+ self.logger.error(f"Failed to start unified monitor server: {e}")
111
+ return False
112
+
113
+ def _run_server(self):
114
+ """Run the server in its own event loop."""
115
+ try:
116
+ # Create new event loop for this thread
117
+ self.loop = asyncio.new_event_loop()
118
+ asyncio.set_event_loop(self.loop)
119
+
120
+ # Run the async server
121
+ self.loop.run_until_complete(self._start_async_server())
122
+
123
+ except Exception as e:
124
+ self.logger.error(f"Error in server thread: {e}")
125
+ finally:
126
+ if self.loop:
127
+ self.loop.close()
128
+
129
+ async def _start_async_server(self):
130
+ """Start the async server components."""
131
+ try:
132
+ # Create Socket.IO server
133
+ self.sio = socketio.AsyncServer(
134
+ cors_allowed_origins="*", logger=False, engineio_logger=False
135
+ )
136
+
137
+ # Create aiohttp application
138
+ self.app = web.Application()
139
+
140
+ # Attach Socket.IO to the app
141
+ self.sio.attach(self.app)
142
+
143
+ # Setup event handlers
144
+ self._setup_event_handlers()
145
+
146
+ # Setup high-performance event emitter
147
+ await self._setup_event_emitter()
148
+
149
+ self.logger.info(
150
+ "Using high-performance async event architecture with direct calls"
151
+ )
152
+
153
+ # Setup HTTP routes
154
+ self._setup_http_routes()
155
+
156
+ # Create and start the server
157
+ self.runner = web.AppRunner(self.app)
158
+ await self.runner.setup()
159
+
160
+ self.site = web.TCPSite(self.runner, self.host, self.port)
161
+ await self.site.start()
162
+
163
+ self.running = True
164
+ self.logger.info(f"Server running on http://{self.host}:{self.port}")
165
+
166
+ # Keep the server running
167
+ while self.running:
168
+ await asyncio.sleep(1)
169
+
170
+ except Exception as e:
171
+ self.logger.error(f"Error starting async server: {e}")
172
+ raise
173
+ finally:
174
+ await self._cleanup_async()
175
+
176
+ def _setup_event_handlers(self):
177
+ """Setup Socket.IO event handlers."""
178
+ try:
179
+ # Create event handlers
180
+ self.code_analysis_handler = CodeAnalysisHandler(self.sio)
181
+ self.dashboard_handler = DashboardHandler(self.sio)
182
+ self.hook_handler = HookHandler(self.sio)
183
+
184
+ # Register handlers
185
+ self.code_analysis_handler.register()
186
+ self.dashboard_handler.register()
187
+ self.hook_handler.register()
188
+
189
+ self.logger.info("Event handlers registered successfully")
190
+
191
+ except Exception as e:
192
+ self.logger.error(f"Error setting up event handlers: {e}")
193
+ raise
194
+
195
+ async def _setup_event_emitter(self):
196
+ """Setup high-performance event emitter."""
197
+ try:
198
+ # Get the global event emitter instance
199
+ self.event_emitter = await get_event_emitter()
200
+
201
+ # Register this Socket.IO server for direct event emission
202
+ self.event_emitter.register_socketio_server(self.sio)
203
+
204
+ self.logger.info("Event emitter setup complete - direct calls enabled")
205
+
206
+ except Exception as e:
207
+ self.logger.error(f"Error setting up event emitter: {e}")
208
+ raise
209
+
210
+ def _setup_http_routes(self):
211
+ """Setup HTTP routes for the dashboard."""
212
+ try:
213
+ # Dashboard static files
214
+ dashboard_dir = Path(__file__).parent.parent.parent / "dashboard"
215
+
216
+ # Main dashboard route
217
+ async def dashboard_index(request):
218
+ template_path = dashboard_dir / "templates" / "index.html"
219
+ if template_path.exists():
220
+ with open(template_path) as f:
221
+ content = f.read()
222
+ return web.Response(text=content, content_type="text/html")
223
+ return web.Response(text="Dashboard not found", status=404)
224
+
225
+ # Health check
226
+ async def health_check(request):
227
+ return web.json_response(
228
+ {
229
+ "status": "healthy",
230
+ "service": "unified-monitor",
231
+ "version": "1.0.0",
232
+ "port": self.port,
233
+ }
234
+ )
235
+
236
+ # Event ingestion endpoint for hook handlers
237
+ async def api_events_handler(request):
238
+ """Handle HTTP POST events from hook handlers."""
239
+ try:
240
+ data = await request.json()
241
+
242
+ # Extract event data
243
+ namespace = data.get("namespace", "hook")
244
+ event = data.get("event", "claude_event")
245
+ event_data = data.get("data", {})
246
+
247
+ # Emit to Socket.IO clients via the appropriate event
248
+ if self.sio:
249
+ await self.sio.emit(event, event_data)
250
+ self.logger.debug(f"HTTP event forwarded to Socket.IO: {event}")
251
+
252
+ return web.Response(status=204) # No content response
253
+
254
+ except Exception as e:
255
+ self.logger.error(f"Error handling HTTP event: {e}")
256
+ return web.Response(text=f"Error: {e!s}", status=500)
257
+
258
+ # File content endpoint for file viewer
259
+ async def api_file_handler(request):
260
+ """Handle file content requests."""
261
+ import json
262
+ import os
263
+
264
+ try:
265
+ data = await request.json()
266
+ file_path = data.get("path", "")
267
+
268
+ # Security check: ensure path is absolute and exists
269
+ if not file_path or not os.path.isabs(file_path):
270
+ return web.json_response(
271
+ {"success": False, "error": "Invalid file path"}, status=400
272
+ )
273
+
274
+ # Check if file exists and is readable
275
+ if not os.path.exists(file_path):
276
+ return web.json_response(
277
+ {"success": False, "error": "File not found"}, status=404
278
+ )
279
+
280
+ if not os.path.isfile(file_path):
281
+ return web.json_response(
282
+ {"success": False, "error": "Path is not a file"},
283
+ status=400,
284
+ )
285
+
286
+ # Read file content (with size limit for safety)
287
+ max_size = 10 * 1024 * 1024 # 10MB limit
288
+ file_size = os.path.getsize(file_path)
289
+
290
+ if file_size > max_size:
291
+ return web.json_response(
292
+ {
293
+ "success": False,
294
+ "error": f"File too large (>{max_size} bytes)",
295
+ },
296
+ status=413,
297
+ )
298
+
299
+ try:
300
+ with open(file_path, encoding="utf-8") as f:
301
+ content = f.read()
302
+ lines = content.count("\n") + 1
303
+ except UnicodeDecodeError:
304
+ # Try reading as binary if UTF-8 fails
305
+ return web.json_response(
306
+ {"success": False, "error": "File is not a text file"},
307
+ status=415,
308
+ )
309
+
310
+ # Get file extension for type detection
311
+ file_ext = os.path.splitext(file_path)[1].lstrip(".")
312
+
313
+ return web.json_response(
314
+ {
315
+ "success": True,
316
+ "content": content,
317
+ "lines": lines,
318
+ "size": file_size,
319
+ "type": file_ext or "text",
320
+ }
321
+ )
322
+
323
+ except json.JSONDecodeError:
324
+ return web.json_response(
325
+ {"success": False, "error": "Invalid JSON in request"},
326
+ status=400,
327
+ )
328
+ except Exception as e:
329
+ self.logger.error(f"Error reading file: {e}")
330
+ return web.json_response(
331
+ {"success": False, "error": str(e)}, status=500
332
+ )
333
+
334
+ # Version endpoint for dashboard build tracker
335
+ async def version_handler(request):
336
+ """Serve version information for dashboard build tracker."""
337
+ try:
338
+ # Try to get version from version service
339
+ from claude_mpm.services.version_service import VersionService
340
+
341
+ version_service = VersionService()
342
+ version_info = version_service.get_version_info()
343
+
344
+ return web.json_response(
345
+ {
346
+ "version": version_info.get("base_version", "1.0.0"),
347
+ "build": version_info.get("build_number", 1),
348
+ "formatted_build": f"{version_info.get('build_number', 1):04d}",
349
+ "full_version": version_info.get("version", "v1.0.0-0001"),
350
+ "service": "unified-monitor",
351
+ }
352
+ )
353
+ except Exception as e:
354
+ self.logger.warning(f"Error getting version info: {e}")
355
+ # Return default version info if service fails
356
+ return web.json_response(
357
+ {
358
+ "version": "1.0.0",
359
+ "build": 1,
360
+ "formatted_build": "0001",
361
+ "full_version": "v1.0.0-0001",
362
+ "service": "unified-monitor",
363
+ }
364
+ )
365
+
366
+ # Register routes
367
+ self.app.router.add_get("/", dashboard_index)
368
+ self.app.router.add_get("/health", health_check)
369
+ self.app.router.add_get("/version.json", version_handler)
370
+ self.app.router.add_get("/api/directory", list_directory)
371
+ self.app.router.add_post("/api/events", api_events_handler)
372
+ self.app.router.add_post("/api/file", api_file_handler)
373
+
374
+ # Static files
375
+ static_dir = dashboard_dir / "static"
376
+ if static_dir.exists():
377
+ self.app.router.add_static("/static/", static_dir)
378
+
379
+ # Templates
380
+ templates_dir = dashboard_dir / "templates"
381
+ if templates_dir.exists():
382
+ self.app.router.add_static("/templates/", templates_dir)
383
+
384
+ self.logger.info("HTTP routes registered successfully")
385
+
386
+ except Exception as e:
387
+ self.logger.error(f"Error setting up HTTP routes: {e}")
388
+ raise
389
+
390
+ def stop(self):
391
+ """Stop the unified monitor server."""
392
+ try:
393
+ self.logger.info("Stopping unified monitor server")
394
+
395
+ self.running = False
396
+
397
+ # Wait for server thread to finish
398
+ if self.server_thread and self.server_thread.is_alive():
399
+ self.server_thread.join(timeout=5)
400
+
401
+ self.logger.info("Unified monitor server stopped")
402
+
403
+ except Exception as e:
404
+ self.logger.error(f"Error stopping unified monitor server: {e}")
405
+
406
+ async def _cleanup_async(self):
407
+ """Cleanup async resources."""
408
+ try:
409
+ # Cleanup event emitter
410
+ if self.event_emitter:
411
+ try:
412
+ self.event_emitter.unregister_socketio_server(self.sio)
413
+ await self.event_emitter.close()
414
+ self.logger.info("Event emitter cleaned up")
415
+ except Exception as e:
416
+ self.logger.warning(f"Error cleaning up event emitter: {e}")
417
+
418
+ if self.site:
419
+ await self.site.stop()
420
+
421
+ if self.runner:
422
+ await self.runner.cleanup()
423
+
424
+ except Exception as e:
425
+ self.logger.error(f"Error during async cleanup: {e}")
426
+
427
+ def get_status(self) -> Dict:
428
+ """Get server status information.
429
+
430
+ Returns:
431
+ Dictionary with server status
432
+ """
433
+ return {
434
+ "server_running": self.running,
435
+ "host": self.host,
436
+ "port": self.port,
437
+ "handlers": {
438
+ "code_analysis": self.code_analysis_handler is not None,
439
+ "dashboard": self.dashboard_handler is not None,
440
+ "hooks": self.hook_handler is not None,
441
+ },
442
+ }
@@ -1727,27 +1727,43 @@ class CodeTreeAnalyzer:
1727
1727
 
1728
1728
  def _is_internal_node(self, node: CodeNode) -> bool:
1729
1729
  """Check if node is an internal function that should be filtered."""
1730
- # Filter patterns for internal functions
1731
- internal_patterns = [
1732
- "handle", # Event handlers
1733
- "on_", # Event callbacks
1734
- "_", # Private methods
1735
- "get_", # Simple getters
1736
- "set_", # Simple setters
1737
- "__", # Python magic methods
1738
- ]
1730
+ # Don't filter classes - always show them
1731
+ if node.node_type == "class":
1732
+ return False
1733
+
1734
+ # Don't filter variables or imports - they're useful for tree view
1735
+ if node.node_type in ["variable", "import"]:
1736
+ return False
1739
1737
 
1740
1738
  name_lower = node.name.lower()
1741
1739
 
1742
- # Don't filter classes or important public methods
1743
- if node.node_type == "class":
1744
- return False
1740
+ # Filter only very specific internal patterns
1741
+ # Be more conservative - only filter obvious internal handlers
1742
+ if name_lower.startswith("handle_") or name_lower.startswith("on_"):
1743
+ return True
1744
+
1745
+ # Filter Python magic methods except important ones
1746
+ if name_lower.startswith("__") and name_lower.endswith("__"):
1747
+ # Keep important magic methods
1748
+ important_magic = [
1749
+ "__init__",
1750
+ "__call__",
1751
+ "__enter__",
1752
+ "__exit__",
1753
+ "__str__",
1754
+ "__repr__",
1755
+ ]
1756
+ return node.name not in important_magic
1745
1757
 
1746
- # Check patterns
1747
- for pattern in internal_patterns:
1748
- if name_lower.startswith(pattern):
1749
- # Exception: include __init__ methods
1750
- return node.name != "__init__"
1758
+ # Filter very generic getters/setters only if they're trivial
1759
+ if (name_lower.startswith("get_") or name_lower.startswith("set_")) and len(
1760
+ node.name
1761
+ ) <= 8:
1762
+ return True
1763
+
1764
+ # Don't filter single underscore functions - they're often important
1765
+ # (like _setup_logging, _validate_input, etc.)
1766
+ return False
1751
1767
 
1752
1768
  return False
1753
1769
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.2.9
3
+ Version: 4.2.11
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team