claude-mpm 4.2.9__py3-none-any.whl → 4.2.12__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 (51) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/dashboard.py +59 -126
  3. claude_mpm/cli/commands/monitor.py +82 -212
  4. claude_mpm/cli/commands/run.py +33 -33
  5. claude_mpm/cli/parsers/monitor_parser.py +12 -2
  6. claude_mpm/dashboard/static/css/code-tree.css +8 -16
  7. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  8. claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
  9. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
  11. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  12. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  13. claude_mpm/dashboard/static/js/components/code-tree.js +692 -114
  14. claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
  15. claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
  16. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
  17. claude_mpm/dashboard/static/js/dashboard.js +108 -91
  18. claude_mpm/dashboard/static/js/socket-client.js +9 -7
  19. claude_mpm/dashboard/templates/index.html +2 -7
  20. claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
  21. claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
  22. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
  23. claude_mpm/services/agents/deployment/agent_template_builder.py +0 -1
  24. claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
  25. claude_mpm/services/monitor/__init__.py +20 -0
  26. claude_mpm/services/monitor/daemon.py +378 -0
  27. claude_mpm/services/monitor/event_emitter.py +342 -0
  28. claude_mpm/services/monitor/handlers/__init__.py +20 -0
  29. claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
  30. claude_mpm/services/monitor/handlers/dashboard.py +298 -0
  31. claude_mpm/services/monitor/handlers/hooks.py +491 -0
  32. claude_mpm/services/monitor/management/__init__.py +18 -0
  33. claude_mpm/services/monitor/management/health.py +124 -0
  34. claude_mpm/services/monitor/management/lifecycle.py +338 -0
  35. claude_mpm/services/monitor/server.py +596 -0
  36. claude_mpm/tools/code_tree_analyzer.py +33 -17
  37. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/METADATA +1 -1
  38. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/RECORD +42 -37
  39. claude_mpm/cli/commands/socketio_monitor.py +0 -233
  40. claude_mpm/scripts/socketio_daemon.py +0 -571
  41. claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
  42. claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
  43. claude_mpm/scripts/socketio_server_manager.py +0 -349
  44. claude_mpm/services/cli/dashboard_launcher.py +0 -423
  45. claude_mpm/services/cli/socketio_manager.py +0 -595
  46. claude_mpm/services/dashboard/stable_server.py +0 -1020
  47. claude_mpm/services/socketio/monitor_server.py +0 -505
  48. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/WHEEL +0 -0
  49. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/entry_points.txt +0 -0
  50. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/licenses/LICENSE +0 -0
  51. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,596 @@
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
+ loop = None
116
+ try:
117
+ # Create new event loop for this thread
118
+ loop = asyncio.new_event_loop()
119
+ asyncio.set_event_loop(loop)
120
+ self.loop = loop
121
+
122
+ # Run the async server
123
+ loop.run_until_complete(self._start_async_server())
124
+
125
+ except Exception as e:
126
+ self.logger.error(f"Error in server thread: {e}")
127
+ finally:
128
+ # Always ensure loop cleanup happens
129
+ if loop is not None:
130
+ try:
131
+ # Cancel all pending tasks first
132
+ self._cancel_all_tasks(loop)
133
+
134
+ # Give tasks a moment to cancel gracefully
135
+ if not loop.is_closed():
136
+ try:
137
+ loop.run_until_complete(asyncio.sleep(0.1))
138
+ except RuntimeError:
139
+ # Loop might be stopped already, that's ok
140
+ pass
141
+
142
+ except Exception as e:
143
+ self.logger.debug(f"Error during task cancellation: {e}")
144
+ finally:
145
+ try:
146
+ # Clear the loop reference from the instance first
147
+ self.loop = None
148
+
149
+ # Stop the loop if it's still running
150
+ if loop.is_running():
151
+ loop.stop()
152
+
153
+ # CRITICAL: Wait a moment for the loop to stop
154
+ import time
155
+
156
+ time.sleep(0.1)
157
+
158
+ # Clear the event loop from the thread BEFORE closing
159
+ # This prevents other code from accidentally using it
160
+ asyncio.set_event_loop(None)
161
+
162
+ # Now close the loop - this is critical to prevent the kqueue error
163
+ if not loop.is_closed():
164
+ loop.close()
165
+ # Wait for the close to complete
166
+ time.sleep(0.05)
167
+
168
+ except Exception as e:
169
+ self.logger.debug(f"Error during event loop cleanup: {e}")
170
+
171
+ async def _start_async_server(self):
172
+ """Start the async server components."""
173
+ try:
174
+ # Create Socket.IO server
175
+ self.sio = socketio.AsyncServer(
176
+ cors_allowed_origins="*", logger=False, engineio_logger=False
177
+ )
178
+
179
+ # Create aiohttp application
180
+ self.app = web.Application()
181
+
182
+ # Attach Socket.IO to the app
183
+ self.sio.attach(self.app)
184
+
185
+ # Setup event handlers
186
+ self._setup_event_handlers()
187
+
188
+ # Setup high-performance event emitter
189
+ await self._setup_event_emitter()
190
+
191
+ self.logger.info(
192
+ "Using high-performance async event architecture with direct calls"
193
+ )
194
+
195
+ # Setup HTTP routes
196
+ self._setup_http_routes()
197
+
198
+ # Create and start the server
199
+ self.runner = web.AppRunner(self.app)
200
+ await self.runner.setup()
201
+
202
+ self.site = web.TCPSite(self.runner, self.host, self.port)
203
+ await self.site.start()
204
+
205
+ self.running = True
206
+ self.logger.info(f"Server running on http://{self.host}:{self.port}")
207
+
208
+ # Keep the server running
209
+ while self.running:
210
+ await asyncio.sleep(1)
211
+
212
+ except Exception as e:
213
+ self.logger.error(f"Error starting async server: {e}")
214
+ raise
215
+ finally:
216
+ await self._cleanup_async()
217
+
218
+ def _setup_event_handlers(self):
219
+ """Setup Socket.IO event handlers."""
220
+ try:
221
+ # Create event handlers
222
+ self.code_analysis_handler = CodeAnalysisHandler(self.sio)
223
+ self.dashboard_handler = DashboardHandler(self.sio)
224
+ self.hook_handler = HookHandler(self.sio)
225
+
226
+ # Register handlers
227
+ self.code_analysis_handler.register()
228
+ self.dashboard_handler.register()
229
+ self.hook_handler.register()
230
+
231
+ self.logger.info("Event handlers registered successfully")
232
+
233
+ except Exception as e:
234
+ self.logger.error(f"Error setting up event handlers: {e}")
235
+ raise
236
+
237
+ async def _setup_event_emitter(self):
238
+ """Setup high-performance event emitter."""
239
+ try:
240
+ # Get the global event emitter instance
241
+ self.event_emitter = await get_event_emitter()
242
+
243
+ # Register this Socket.IO server for direct event emission
244
+ self.event_emitter.register_socketio_server(self.sio)
245
+
246
+ self.logger.info("Event emitter setup complete - direct calls enabled")
247
+
248
+ except Exception as e:
249
+ self.logger.error(f"Error setting up event emitter: {e}")
250
+ raise
251
+
252
+ def _setup_http_routes(self):
253
+ """Setup HTTP routes for the dashboard."""
254
+ try:
255
+ # Dashboard static files
256
+ dashboard_dir = Path(__file__).parent.parent.parent / "dashboard"
257
+
258
+ # Main dashboard route
259
+ async def dashboard_index(request):
260
+ template_path = dashboard_dir / "templates" / "index.html"
261
+ if template_path.exists():
262
+ with open(template_path) as f:
263
+ content = f.read()
264
+ return web.Response(text=content, content_type="text/html")
265
+ return web.Response(text="Dashboard not found", status=404)
266
+
267
+ # Health check
268
+ async def health_check(request):
269
+ return web.json_response(
270
+ {
271
+ "status": "healthy",
272
+ "service": "unified-monitor",
273
+ "version": "1.0.0",
274
+ "port": self.port,
275
+ }
276
+ )
277
+
278
+ # Event ingestion endpoint for hook handlers
279
+ async def api_events_handler(request):
280
+ """Handle HTTP POST events from hook handlers."""
281
+ try:
282
+ data = await request.json()
283
+
284
+ # Extract event data
285
+ namespace = data.get("namespace", "hook")
286
+ event = data.get("event", "claude_event")
287
+ event_data = data.get("data", {})
288
+
289
+ # Emit to Socket.IO clients via the appropriate event
290
+ if self.sio:
291
+ await self.sio.emit(event, event_data)
292
+ self.logger.debug(f"HTTP event forwarded to Socket.IO: {event}")
293
+
294
+ return web.Response(status=204) # No content response
295
+
296
+ except Exception as e:
297
+ self.logger.error(f"Error handling HTTP event: {e}")
298
+ return web.Response(text=f"Error: {e!s}", status=500)
299
+
300
+ # File content endpoint for file viewer
301
+ async def api_file_handler(request):
302
+ """Handle file content requests."""
303
+ import json
304
+ import os
305
+
306
+ try:
307
+ data = await request.json()
308
+ file_path = data.get("path", "")
309
+
310
+ # Security check: ensure path is absolute and exists
311
+ if not file_path or not os.path.isabs(file_path):
312
+ return web.json_response(
313
+ {"success": False, "error": "Invalid file path"}, status=400
314
+ )
315
+
316
+ # Check if file exists and is readable
317
+ if not os.path.exists(file_path):
318
+ return web.json_response(
319
+ {"success": False, "error": "File not found"}, status=404
320
+ )
321
+
322
+ if not os.path.isfile(file_path):
323
+ return web.json_response(
324
+ {"success": False, "error": "Path is not a file"},
325
+ status=400,
326
+ )
327
+
328
+ # Read file content (with size limit for safety)
329
+ max_size = 10 * 1024 * 1024 # 10MB limit
330
+ file_size = os.path.getsize(file_path)
331
+
332
+ if file_size > max_size:
333
+ return web.json_response(
334
+ {
335
+ "success": False,
336
+ "error": f"File too large (>{max_size} bytes)",
337
+ },
338
+ status=413,
339
+ )
340
+
341
+ try:
342
+ with open(file_path, encoding="utf-8") as f:
343
+ content = f.read()
344
+ lines = content.count("\n") + 1
345
+ except UnicodeDecodeError:
346
+ # Try reading as binary if UTF-8 fails
347
+ return web.json_response(
348
+ {"success": False, "error": "File is not a text file"},
349
+ status=415,
350
+ )
351
+
352
+ # Get file extension for type detection
353
+ file_ext = os.path.splitext(file_path)[1].lstrip(".")
354
+
355
+ return web.json_response(
356
+ {
357
+ "success": True,
358
+ "content": content,
359
+ "lines": lines,
360
+ "size": file_size,
361
+ "type": file_ext or "text",
362
+ }
363
+ )
364
+
365
+ except json.JSONDecodeError:
366
+ return web.json_response(
367
+ {"success": False, "error": "Invalid JSON in request"},
368
+ status=400,
369
+ )
370
+ except Exception as e:
371
+ self.logger.error(f"Error reading file: {e}")
372
+ return web.json_response(
373
+ {"success": False, "error": str(e)}, status=500
374
+ )
375
+
376
+ # Version endpoint for dashboard build tracker
377
+ async def version_handler(request):
378
+ """Serve version information for dashboard build tracker."""
379
+ try:
380
+ # Try to get version from version service
381
+ from claude_mpm.services.version_service import VersionService
382
+
383
+ version_service = VersionService()
384
+ version_info = version_service.get_version_info()
385
+
386
+ return web.json_response(
387
+ {
388
+ "version": version_info.get("base_version", "1.0.0"),
389
+ "build": version_info.get("build_number", 1),
390
+ "formatted_build": f"{version_info.get('build_number', 1):04d}",
391
+ "full_version": version_info.get("version", "v1.0.0-0001"),
392
+ "service": "unified-monitor",
393
+ }
394
+ )
395
+ except Exception as e:
396
+ self.logger.warning(f"Error getting version info: {e}")
397
+ # Return default version info if service fails
398
+ return web.json_response(
399
+ {
400
+ "version": "1.0.0",
401
+ "build": 1,
402
+ "formatted_build": "0001",
403
+ "full_version": "v1.0.0-0001",
404
+ "service": "unified-monitor",
405
+ }
406
+ )
407
+
408
+ # Register routes
409
+ self.app.router.add_get("/", dashboard_index)
410
+ self.app.router.add_get("/health", health_check)
411
+ self.app.router.add_get("/version.json", version_handler)
412
+ self.app.router.add_get("/api/directory", list_directory)
413
+ self.app.router.add_post("/api/events", api_events_handler)
414
+ self.app.router.add_post("/api/file", api_file_handler)
415
+
416
+ # Static files
417
+ static_dir = dashboard_dir / "static"
418
+ if static_dir.exists():
419
+ self.app.router.add_static("/static/", static_dir)
420
+
421
+ # Templates
422
+ templates_dir = dashboard_dir / "templates"
423
+ if templates_dir.exists():
424
+ self.app.router.add_static("/templates/", templates_dir)
425
+
426
+ self.logger.info("HTTP routes registered successfully")
427
+
428
+ except Exception as e:
429
+ self.logger.error(f"Error setting up HTTP routes: {e}")
430
+ raise
431
+
432
+ def stop(self):
433
+ """Stop the unified monitor server."""
434
+ try:
435
+ self.logger.info("Stopping unified monitor server")
436
+
437
+ # Signal shutdown first
438
+ self.running = False
439
+
440
+ # If we have a loop, schedule the cleanup
441
+ if self.loop and not self.loop.is_closed():
442
+ try:
443
+ # Use call_soon_threadsafe to schedule cleanup from another thread
444
+ future = asyncio.run_coroutine_threadsafe(
445
+ self._graceful_shutdown(), self.loop
446
+ )
447
+ # Wait for cleanup to complete (with timeout)
448
+ future.result(timeout=3)
449
+ except Exception as e:
450
+ self.logger.debug(f"Error during graceful shutdown: {e}")
451
+
452
+ # Wait for server thread to finish with a reasonable timeout
453
+ if self.server_thread and self.server_thread.is_alive():
454
+ self.server_thread.join(timeout=5)
455
+
456
+ # If thread is still alive after timeout, log a warning
457
+ if self.server_thread.is_alive():
458
+ self.logger.warning("Server thread did not stop within timeout")
459
+
460
+ # Clear all references to help with cleanup
461
+ self.server_thread = None
462
+ self.app = None
463
+ self.sio = None
464
+ self.runner = None
465
+ self.site = None
466
+ self.event_emitter = None
467
+
468
+ # Give the system a moment to cleanup resources
469
+ import time
470
+
471
+ time.sleep(0.2)
472
+
473
+ self.logger.info("Unified monitor server stopped")
474
+
475
+ except Exception as e:
476
+ self.logger.error(f"Error stopping unified monitor server: {e}")
477
+
478
+ async def _cleanup_async(self):
479
+ """Cleanup async resources."""
480
+ try:
481
+ # Close the Socket.IO server first to stop accepting new connections
482
+ if self.sio:
483
+ try:
484
+ await self.sio.shutdown()
485
+ self.logger.debug("Socket.IO shutdown complete")
486
+ except Exception as e:
487
+ self.logger.debug(f"Error shutting down Socket.IO: {e}")
488
+ finally:
489
+ self.sio = None
490
+
491
+ # Cleanup event emitter
492
+ if self.event_emitter:
493
+ try:
494
+ if self.sio:
495
+ self.event_emitter.unregister_socketio_server(self.sio)
496
+
497
+ # Use the global cleanup function to ensure proper cleanup
498
+ from .event_emitter import cleanup_event_emitter
499
+
500
+ await cleanup_event_emitter()
501
+
502
+ self.logger.info("Event emitter cleaned up")
503
+ except Exception as e:
504
+ self.logger.warning(f"Error cleaning up event emitter: {e}")
505
+ finally:
506
+ self.event_emitter = None
507
+
508
+ # Stop the site (must be done before runner cleanup)
509
+ if self.site:
510
+ try:
511
+ await self.site.stop()
512
+ self.logger.debug("Site stopped")
513
+ except Exception as e:
514
+ self.logger.debug(f"Error stopping site: {e}")
515
+ finally:
516
+ self.site = None
517
+
518
+ # Cleanup the runner (after site is stopped)
519
+ if self.runner:
520
+ try:
521
+ await self.runner.cleanup()
522
+ self.logger.debug("Runner cleaned up")
523
+ except Exception as e:
524
+ self.logger.debug(f"Error cleaning up runner: {e}")
525
+ finally:
526
+ self.runner = None
527
+
528
+ # Clear app reference
529
+ self.app = None
530
+
531
+ except Exception as e:
532
+ self.logger.error(f"Error during async cleanup: {e}")
533
+
534
+ def get_status(self) -> Dict:
535
+ """Get server status information.
536
+
537
+ Returns:
538
+ Dictionary with server status
539
+ """
540
+ return {
541
+ "server_running": self.running,
542
+ "host": self.host,
543
+ "port": self.port,
544
+ "handlers": {
545
+ "code_analysis": self.code_analysis_handler is not None,
546
+ "dashboard": self.dashboard_handler is not None,
547
+ "hooks": self.hook_handler is not None,
548
+ },
549
+ }
550
+
551
+ def _cancel_all_tasks(self, loop=None):
552
+ """Cancel all pending tasks in the event loop."""
553
+ if loop is None:
554
+ loop = self.loop
555
+
556
+ if not loop or loop.is_closed():
557
+ return
558
+
559
+ try:
560
+ # Get all tasks in the loop
561
+ pending = asyncio.all_tasks(loop)
562
+
563
+ # Count tasks to cancel
564
+ tasks_to_cancel = [task for task in pending if not task.done()]
565
+
566
+ if tasks_to_cancel:
567
+ # Cancel each task
568
+ for task in tasks_to_cancel:
569
+ task.cancel()
570
+
571
+ # Wait for all tasks to complete cancellation
572
+ gather = asyncio.gather(*tasks_to_cancel, return_exceptions=True)
573
+ try:
574
+ loop.run_until_complete(gather)
575
+ except Exception:
576
+ # Some tasks might fail to cancel, that's ok
577
+ pass
578
+
579
+ self.logger.debug(f"Cancelled {len(tasks_to_cancel)} pending tasks")
580
+ except Exception as e:
581
+ self.logger.debug(f"Error cancelling tasks: {e}")
582
+
583
+ async def _graceful_shutdown(self):
584
+ """Perform graceful shutdown of async resources."""
585
+ try:
586
+ # Stop accepting new connections
587
+ self.running = False
588
+
589
+ # Give ongoing operations a moment to complete
590
+ await asyncio.sleep(0.5)
591
+
592
+ # Then cleanup resources
593
+ await self._cleanup_async()
594
+
595
+ except Exception as e:
596
+ self.logger.debug(f"Error in graceful shutdown: {e}")
@@ -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.12
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