claude-mpm 3.3.0__py3-none-any.whl → 3.4.0__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 (58) hide show
  1. claude_mpm/agents/templates/data_engineer.json +1 -1
  2. claude_mpm/agents/templates/documentation.json +1 -1
  3. claude_mpm/agents/templates/engineer.json +1 -1
  4. claude_mpm/agents/templates/ops.json +1 -1
  5. claude_mpm/agents/templates/pm.json +1 -1
  6. claude_mpm/agents/templates/qa.json +1 -1
  7. claude_mpm/agents/templates/research.json +1 -1
  8. claude_mpm/agents/templates/security.json +1 -1
  9. claude_mpm/agents/templates/test_integration.json +112 -0
  10. claude_mpm/agents/templates/version_control.json +1 -1
  11. claude_mpm/cli/commands/memory.py +749 -26
  12. claude_mpm/cli/commands/run.py +115 -14
  13. claude_mpm/cli/parser.py +89 -1
  14. claude_mpm/constants.py +6 -0
  15. claude_mpm/core/claude_runner.py +74 -11
  16. claude_mpm/core/config.py +1 -1
  17. claude_mpm/core/session_manager.py +46 -0
  18. claude_mpm/core/simple_runner.py +74 -11
  19. claude_mpm/hooks/builtin/mpm_command_hook.py +5 -5
  20. claude_mpm/hooks/claude_hooks/hook_handler.py +213 -30
  21. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -2
  22. claude_mpm/hooks/memory_integration_hook.py +51 -5
  23. claude_mpm/services/__init__.py +23 -5
  24. claude_mpm/services/agent_memory_manager.py +800 -71
  25. claude_mpm/services/memory_builder.py +823 -0
  26. claude_mpm/services/memory_optimizer.py +619 -0
  27. claude_mpm/services/memory_router.py +445 -0
  28. claude_mpm/services/project_analyzer.py +771 -0
  29. claude_mpm/services/socketio_server.py +649 -45
  30. claude_mpm/services/version_control/git_operations.py +26 -0
  31. claude_mpm-3.4.0.dist-info/METADATA +183 -0
  32. {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/RECORD +36 -52
  33. claude_mpm/agents/agent-template.yaml +0 -83
  34. claude_mpm/agents/templates/test-integration-agent.md +0 -34
  35. claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +0 -6
  36. claude_mpm/cli/README.md +0 -109
  37. claude_mpm/cli_module/refactoring_guide.md +0 -253
  38. claude_mpm/core/agent_registry.py.bak +0 -312
  39. claude_mpm/core/base_service.py.bak +0 -406
  40. claude_mpm/core/websocket_handler.py +0 -233
  41. claude_mpm/hooks/README.md +0 -97
  42. claude_mpm/orchestration/SUBPROCESS_DESIGN.md +0 -66
  43. claude_mpm/schemas/README_SECURITY.md +0 -92
  44. claude_mpm/schemas/agent_schema.json +0 -395
  45. claude_mpm/schemas/agent_schema_documentation.md +0 -181
  46. claude_mpm/schemas/agent_schema_security_notes.md +0 -165
  47. claude_mpm/schemas/examples/standard_workflow.json +0 -505
  48. claude_mpm/schemas/ticket_workflow_documentation.md +0 -482
  49. claude_mpm/schemas/ticket_workflow_schema.json +0 -590
  50. claude_mpm/services/framework_claude_md_generator/README.md +0 -92
  51. claude_mpm/services/parent_directory_manager/README.md +0 -83
  52. claude_mpm/services/version_control/VERSION +0 -1
  53. claude_mpm/services/websocket_server.py +0 -376
  54. claude_mpm-3.3.0.dist-info/METADATA +0 -432
  55. {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/WHEEL +0 -0
  56. {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/entry_points.txt +0 -0
  57. {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/licenses/LICENSE +0 -0
  58. {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/top_level.txt +0 -0
@@ -1,406 +0,0 @@
1
- """
2
- Base service class for Claude PM Framework services.
3
-
4
- Provides common functionality including:
5
- - Lifecycle management (start, stop, health checks)
6
- - Configuration management
7
- - Logging
8
- - Metrics collection
9
- - Error handling and retry logic
10
- - Service discovery and registration
11
- """
12
-
13
- import asyncio
14
- import logging
15
- import signal
16
- import sys
17
- import os
18
- from abc import ABC, abstractmethod
19
- from dataclasses import dataclass, field
20
- from datetime import datetime
21
- from typing import Any, Dict, List, Optional, Union
22
- from pathlib import Path
23
- import json
24
-
25
- from .config import Config
26
- from .logger import setup_logging
27
- from .mixins import LoggerMixin
28
-
29
-
30
- @dataclass
31
- class ServiceHealth:
32
- """Service health status information."""
33
-
34
- status: str # healthy, degraded, unhealthy, unknown
35
- message: str
36
- timestamp: str
37
- metrics: Dict[str, Any] = field(default_factory=dict)
38
- checks: Dict[str, bool] = field(default_factory=dict)
39
-
40
-
41
- @dataclass
42
- class ServiceMetrics:
43
- """Service metrics collection."""
44
-
45
- requests_total: int = 0
46
- requests_failed: int = 0
47
- response_time_avg: float = 0.0
48
- uptime_seconds: int = 0
49
- memory_usage_mb: float = 0.0
50
- custom_metrics: Dict[str, Any] = field(default_factory=dict)
51
-
52
-
53
- class BaseService(LoggerMixin, ABC):
54
- """
55
- Abstract base class for all Claude PM services.
56
-
57
- Provides common infrastructure for service lifecycle management,
58
- health monitoring, configuration, logging, and error handling.
59
- """
60
-
61
- def __init__(
62
- self, name: str, config: Optional[Dict[str, Any]] = None, config_path: Optional[Path] = None
63
- ):
64
- """
65
- Initialize the base service.
66
-
67
- Args:
68
- name: Service name for identification
69
- config: Optional configuration dictionary
70
- config_path: Optional path to configuration file
71
- """
72
- self.name = name
73
- self.config = Config(config or {}, config_path)
74
-
75
- # Set custom logger name based on service name
76
- self._logger_name = f"{self.__class__.__module__}.{self.__class__.__name__}.{name}"
77
-
78
- # Service state
79
- self._running = False
80
- self._start_time: Optional[datetime] = None
81
- self._stop_event = asyncio.Event()
82
-
83
- # Health and metrics
84
- self._health = ServiceHealth(
85
- status="unknown", message="Service not started", timestamp=datetime.now().isoformat()
86
- )
87
- self._metrics = ServiceMetrics()
88
-
89
- # Background tasks
90
- self._background_tasks: List[asyncio.Task] = []
91
-
92
- # Check for quiet mode environment variable
93
- default_log_level = "INFO"
94
- if os.getenv('CLAUDE_PM_QUIET_MODE') == 'true':
95
- default_log_level = "WARNING"
96
- # Set logger level if needed
97
- self.logger.setLevel(logging.WARNING)
98
-
99
- # Only log if not in quiet mode
100
- if not os.environ.get('CLAUDE_PM_QUIET_MODE', '').lower() == 'true':
101
- self.logger.info(f"Initialized {self.name} service")
102
-
103
- @property
104
- def running(self) -> bool:
105
- """Check if service is currently running."""
106
- return self._running
107
-
108
- @property
109
- def uptime(self) -> Optional[float]:
110
- """Get service uptime in seconds."""
111
- if self._start_time and self._running:
112
- return (datetime.now() - self._start_time).total_seconds()
113
- return None
114
-
115
- @property
116
- def health(self) -> ServiceHealth:
117
- """Get current service health status."""
118
- return self._health
119
-
120
- @property
121
- def metrics(self) -> ServiceMetrics:
122
- """Get current service metrics."""
123
- if self.uptime:
124
- self._metrics.uptime_seconds = int(self.uptime)
125
- return self._metrics
126
-
127
- async def start(self) -> None:
128
- """Start the service."""
129
- if self._running:
130
- self.logger.warning(f"Service {self.name} is already running")
131
- return
132
-
133
- self.logger.info(f"Starting {self.name} service...")
134
-
135
- try:
136
- # Setup signal handlers
137
- self._setup_signal_handlers()
138
-
139
- # Initialize service
140
- await self._initialize()
141
-
142
- # Start background tasks
143
- await self._start_background_tasks()
144
-
145
- # Mark as running
146
- self._running = True
147
- self._start_time = datetime.now()
148
-
149
- # Update health status
150
- self._health = ServiceHealth(
151
- status="healthy",
152
- message="Service started successfully",
153
- timestamp=datetime.now().isoformat(),
154
- checks={"startup": True},
155
- )
156
-
157
- self.logger.info(f"Service {self.name} started successfully")
158
-
159
- except Exception as e:
160
- self.logger.error(f"Failed to start service {self.name}: {e}")
161
- self._health = ServiceHealth(
162
- status="unhealthy",
163
- message=f"Startup failed: {str(e)}",
164
- timestamp=datetime.now().isoformat(),
165
- checks={"startup": False},
166
- )
167
- raise
168
-
169
- async def stop(self) -> None:
170
- """Stop the service gracefully."""
171
- if not self._running:
172
- self.logger.warning(f"Service {self.name} is not running")
173
- return
174
-
175
- self.logger.info(f"Stopping {self.name} service...")
176
-
177
- try:
178
- # Signal stop to background tasks
179
- self._stop_event.set()
180
-
181
- # Cancel background tasks
182
- for task in self._background_tasks:
183
- if not task.done():
184
- task.cancel()
185
-
186
- # Wait for tasks to complete
187
- if self._background_tasks:
188
- await asyncio.gather(*self._background_tasks, return_exceptions=True)
189
-
190
- # Cleanup service
191
- await self._cleanup()
192
-
193
- # Mark as stopped
194
- self._running = False
195
-
196
- # Update health status
197
- self._health = ServiceHealth(
198
- status="unknown",
199
- message="Service stopped",
200
- timestamp=datetime.now().isoformat(),
201
- checks={"running": False},
202
- )
203
-
204
- self.logger.info(f"Service {self.name} stopped successfully")
205
-
206
- except Exception as e:
207
- self.logger.error(f"Error stopping service {self.name}: {e}")
208
- raise
209
-
210
- async def restart(self) -> None:
211
- """Restart the service."""
212
- self.logger.info(f"Restarting {self.name} service...")
213
- await self.stop()
214
- await self.start()
215
-
216
- async def health_check(self) -> ServiceHealth:
217
- """
218
- Perform comprehensive health check.
219
-
220
- Returns:
221
- ServiceHealth object with current status
222
- """
223
- try:
224
- checks = {}
225
-
226
- # Basic running check
227
- checks["running"] = self._running
228
-
229
- # Custom health checks
230
- custom_checks = await self._health_check()
231
- checks.update(custom_checks)
232
-
233
- # Determine overall status
234
- if not checks["running"]:
235
- status = "unhealthy"
236
- message = "Service is not running"
237
- elif all(checks.values()):
238
- status = "healthy"
239
- message = "All health checks passed"
240
- elif any(checks.values()):
241
- status = "degraded"
242
- message = "Some health checks failed"
243
- else:
244
- status = "unhealthy"
245
- message = "Multiple health checks failed"
246
-
247
- # Update health status
248
- self._health = ServiceHealth(
249
- status=status,
250
- message=message,
251
- timestamp=datetime.now().isoformat(),
252
- checks=checks,
253
- metrics={
254
- "uptime": self.uptime,
255
- "requests_total": self._metrics.requests_total,
256
- "requests_failed": self._metrics.requests_failed,
257
- },
258
- )
259
-
260
- return self._health
261
-
262
- except Exception as e:
263
- self.logger.error(f"Health check failed for {self.name}: {e}")
264
- self._health = ServiceHealth(
265
- status="unhealthy",
266
- message=f"Health check error: {str(e)}",
267
- timestamp=datetime.now().isoformat(),
268
- checks={"health_check_error": True},
269
- )
270
- return self._health
271
-
272
- def update_metrics(self, **kwargs) -> None:
273
- """Update service metrics."""
274
- for key, value in kwargs.items():
275
- if hasattr(self._metrics, key):
276
- setattr(self._metrics, key, value)
277
- else:
278
- self._metrics.custom_metrics[key] = value
279
-
280
- def get_config(self, key: str, default: Any = None) -> Any:
281
- """Get configuration value."""
282
- return self.config.get(key, default)
283
-
284
- def _setup_signal_handlers(self) -> None:
285
- """Setup signal handlers for graceful shutdown."""
286
-
287
- def signal_handler(signum, frame):
288
- self.logger.info(f"Received signal {signum}, initiating graceful shutdown...")
289
- asyncio.create_task(self.stop())
290
-
291
- signal.signal(signal.SIGINT, signal_handler)
292
- signal.signal(signal.SIGTERM, signal_handler)
293
-
294
- async def _start_background_tasks(self) -> None:
295
- """Start background tasks."""
296
- # Health check task
297
- if self.get_config("enable_health_monitoring", True):
298
- interval = self.get_config("health_check_interval", 30)
299
- task = asyncio.create_task(self._health_monitor_task(interval))
300
- self._background_tasks.append(task)
301
-
302
- # Metrics collection task
303
- if self.get_config("enable_metrics", True):
304
- interval = self.get_config("metrics_interval", 60)
305
- task = asyncio.create_task(self._metrics_task(interval))
306
- self._background_tasks.append(task)
307
-
308
- # Custom background tasks
309
- custom_tasks = await self._start_custom_tasks()
310
- if custom_tasks:
311
- self._background_tasks.extend(custom_tasks)
312
-
313
- async def _health_monitor_task(self, interval: int) -> None:
314
- """Background task for periodic health monitoring."""
315
- while not self._stop_event.is_set():
316
- try:
317
- await self.health_check()
318
- await asyncio.sleep(interval)
319
- except asyncio.CancelledError:
320
- break
321
- except Exception as e:
322
- self.logger.error(f"Health monitor task error: {e}")
323
- await asyncio.sleep(interval)
324
-
325
- async def _metrics_task(self, interval: int) -> None:
326
- """Background task for metrics collection."""
327
- while not self._stop_event.is_set():
328
- try:
329
- await self._collect_metrics()
330
- await asyncio.sleep(interval)
331
- except asyncio.CancelledError:
332
- break
333
- except Exception as e:
334
- self.logger.error(f"Metrics task error: {e}")
335
- await asyncio.sleep(interval)
336
-
337
- async def _collect_metrics(self) -> None:
338
- """Collect service metrics."""
339
- try:
340
- # Update uptime
341
- if self.uptime:
342
- self._metrics.uptime_seconds = int(self.uptime)
343
-
344
- # Memory usage (basic implementation)
345
- import psutil
346
-
347
- process = psutil.Process()
348
- self._metrics.memory_usage_mb = process.memory_info().rss / 1024 / 1024
349
-
350
- # Custom metrics collection
351
- await self._collect_custom_metrics()
352
-
353
- except Exception as e:
354
- self.logger.warning(f"Failed to collect metrics: {e}")
355
-
356
- # Abstract methods to be implemented by subclasses
357
-
358
- @abstractmethod
359
- async def _initialize(self) -> None:
360
- """Initialize the service. Must be implemented by subclasses."""
361
- pass
362
-
363
- @abstractmethod
364
- async def _cleanup(self) -> None:
365
- """Cleanup service resources. Must be implemented by subclasses."""
366
- pass
367
-
368
- async def _health_check(self) -> Dict[str, bool]:
369
- """
370
- Perform custom health checks. Override in subclasses.
371
-
372
- Returns:
373
- Dictionary of check name -> success boolean
374
- """
375
- return {}
376
-
377
- async def _start_custom_tasks(self) -> Optional[List[asyncio.Task]]:
378
- """
379
- Start custom background tasks. Override in subclasses.
380
-
381
- Returns:
382
- List of asyncio tasks or None
383
- """
384
- return None
385
-
386
- async def _collect_custom_metrics(self) -> None:
387
- """Collect custom metrics. Override in subclasses."""
388
- pass
389
-
390
- # Utility methods
391
-
392
- async def run_forever(self) -> None:
393
- """Run the service until stopped."""
394
- await self.start()
395
- try:
396
- # Wait for stop signal
397
- while self._running:
398
- await asyncio.sleep(1)
399
- except KeyboardInterrupt:
400
- self.logger.info("Received keyboard interrupt")
401
- finally:
402
- await self.stop()
403
-
404
- def __repr__(self) -> str:
405
- """String representation of the service."""
406
- return f"<{self.__class__.__name__}(name='{self.name}', running={self._running})>"
@@ -1,233 +0,0 @@
1
- """Socket.IO logging handler with connection pooling for real-time log streaming.
2
-
3
- This handler now uses the Socket.IO connection pool to reduce overhead
4
- and implement circuit breaker and batching patterns for log events.
5
-
6
- WHY connection pooling approach:
7
- - Reduces connection setup/teardown overhead by 80%
8
- - Implements circuit breaker for resilience during outages
9
- - Provides micro-batching for high-frequency log events
10
- - Maintains persistent connections for better performance
11
- - Falls back gracefully when pool unavailable
12
- """
13
-
14
- import logging
15
- import json
16
- import os
17
- from datetime import datetime
18
- from typing import Optional
19
-
20
- # Connection pool import
21
- try:
22
- from .socketio_pool import get_connection_pool
23
- POOL_AVAILABLE = True
24
- except ImportError:
25
- POOL_AVAILABLE = False
26
- get_connection_pool = None
27
-
28
- # Fallback imports
29
- from ..services.websocket_server import get_server_instance
30
-
31
-
32
- class WebSocketHandler(logging.Handler):
33
- """Logging handler that broadcasts log messages via Socket.IO connection pool.
34
-
35
- WHY connection pooling design:
36
- - Uses shared connection pool to reduce overhead by 80%
37
- - Implements circuit breaker pattern for resilience
38
- - Provides micro-batching for high-frequency log events (50ms window)
39
- - Maintains persistent connections across log events
40
- - Falls back gracefully when pool unavailable
41
- """
42
-
43
- def __init__(self, level=logging.NOTSET):
44
- super().__init__(level)
45
- self._websocket_server = None
46
- self._connection_pool = None
47
- self._pool_initialized = False
48
- self._debug = os.environ.get('CLAUDE_MPM_HOOK_DEBUG', '').lower() == 'true'
49
-
50
- def _init_connection_pool(self):
51
- """Initialize connection pool with lazy loading.
52
-
53
- WHY connection pool approach:
54
- - Reuses connections to reduce overhead by 80%
55
- - Implements circuit breaker for resilience
56
- - Provides micro-batching for high-frequency log events
57
- - Falls back gracefully when unavailable
58
- """
59
- if not POOL_AVAILABLE:
60
- if self._debug:
61
- import sys
62
- print("Connection pool not available for logging - falling back to legacy mode", file=sys.stderr)
63
- return
64
-
65
- try:
66
- self._connection_pool = get_connection_pool()
67
- if self._debug:
68
- import sys
69
- print("WebSocket handler: Using Socket.IO connection pool", file=sys.stderr)
70
- except Exception as e:
71
- if self._debug:
72
- import sys
73
- print(f"WebSocket handler: Failed to initialize connection pool: {e}", file=sys.stderr)
74
- self._connection_pool = None
75
-
76
- @property
77
- def websocket_server(self):
78
- """Get WebSocket server instance lazily (fallback compatibility)."""
79
- if self._websocket_server is None:
80
- self._websocket_server = get_server_instance()
81
- return self._websocket_server
82
-
83
- def emit(self, record: logging.LogRecord):
84
- """Emit a log record via Socket.IO connection pool with batching.
85
-
86
- WHY connection pool approach:
87
- - Uses shared connection pool to reduce overhead by 80%
88
- - Implements circuit breaker for resilience during outages
89
- - Provides micro-batching for high-frequency log events (50ms window)
90
- - Falls back gracefully when pool unavailable
91
- """
92
- try:
93
- # Skip connection pool logs to avoid infinite recursion
94
- if "socketio" in record.name.lower() or record.name == "claude_mpm.websocket_client_proxy":
95
- return
96
-
97
- # Skip circuit breaker logs to avoid recursion
98
- if "circuit_breaker" in record.name.lower() or "socketio_pool" in record.name.lower():
99
- return
100
-
101
- # Format the log message
102
- log_data = {
103
- "timestamp": datetime.utcnow().isoformat() + "Z",
104
- "level": record.levelname,
105
- "logger": record.name,
106
- "message": self.format(record),
107
- "module": record.module,
108
- "function": record.funcName,
109
- "line": record.lineno,
110
- "thread": record.thread,
111
- "thread_name": record.threadName
112
- }
113
-
114
- # Add exception info if present
115
- if record.exc_info:
116
- import traceback
117
- log_data["exception"] = ''.join(traceback.format_exception(*record.exc_info))
118
-
119
- # Lazy initialize connection pool on first use
120
- if POOL_AVAILABLE and not self._pool_initialized:
121
- self._pool_initialized = True
122
- self._init_connection_pool()
123
-
124
- # Try connection pool first (preferred method)
125
- if self._connection_pool:
126
- try:
127
- self._connection_pool.emit_event('/log', 'message', log_data)
128
- if self._debug:
129
- import sys
130
- print(f"Emitted pooled Socket.IO log event: /log/message", file=sys.stderr)
131
- return
132
- except Exception as e:
133
- if self._debug:
134
- import sys
135
- print(f"Connection pool log emit failed: {e}", file=sys.stderr)
136
-
137
- # Fallback to legacy WebSocket server
138
- if self.websocket_server:
139
- try:
140
- # Debug: Check what type of server we have
141
- server_type = type(self.websocket_server).__name__
142
- if server_type == "SocketIOClientProxy":
143
- # For exec mode with Socket.IO client proxy, skip local emission
144
- # The persistent server process handles its own logging
145
- return
146
-
147
- # Use new Socket.IO event format
148
- if hasattr(self.websocket_server, 'log_message'):
149
- self.websocket_server.log_message(
150
- level=record.levelname,
151
- message=self.format(record),
152
- module=record.module
153
- )
154
- else:
155
- # Legacy fallback
156
- self.websocket_server.broadcast_event("log.message", log_data)
157
-
158
- if self._debug:
159
- import sys
160
- print(f"Emitted legacy log event", file=sys.stderr)
161
-
162
- except Exception as fallback_error:
163
- if self._debug:
164
- import sys
165
- print(f"Legacy log emit failed: {fallback_error}", file=sys.stderr)
166
-
167
- except Exception as e:
168
- # Don't let logging errors break the application
169
- # But print for debugging
170
- import sys
171
- print(f"WebSocketHandler.emit error: {e}", file=sys.stderr)
172
-
173
- def __del__(self):
174
- """Cleanup connection pool on handler destruction.
175
-
176
- NOTE: Connection pool is shared across handlers, so we don't
177
- shut it down here. The pool manages its own lifecycle.
178
- """
179
- # Connection pool is managed globally, no cleanup needed per handler
180
- pass
181
-
182
-
183
- class WebSocketFormatter(logging.Formatter):
184
- """Custom formatter for WebSocket log messages."""
185
-
186
- def __init__(self):
187
- super().__init__(
188
- fmt='%(name)s - %(levelname)s - %(message)s',
189
- datefmt='%Y-%m-%d %H:%M:%S'
190
- )
191
-
192
-
193
- def setup_websocket_logging(logger_name: Optional[str] = None, level: int = logging.INFO):
194
- """
195
- Set up WebSocket logging for a logger.
196
-
197
- Args:
198
- logger_name: Name of logger to configure (None for root logger)
199
- level: Minimum logging level to broadcast
200
-
201
- Returns:
202
- The configured WebSocketHandler
203
- """
204
- handler = WebSocketHandler(level=level)
205
- handler.setFormatter(WebSocketFormatter())
206
-
207
- # Get the logger
208
- logger = logging.getLogger(logger_name)
209
-
210
- # Add handler if not already present
211
- # Check by handler type to avoid duplicates
212
- has_websocket_handler = any(
213
- isinstance(h, WebSocketHandler) for h in logger.handlers
214
- )
215
-
216
- if not has_websocket_handler:
217
- logger.addHandler(handler)
218
-
219
- return handler
220
-
221
-
222
- def remove_websocket_logging(logger_name: Optional[str] = None):
223
- """Remove WebSocket handler from a logger."""
224
- logger = logging.getLogger(logger_name)
225
-
226
- # Remove all WebSocket handlers
227
- handlers_to_remove = [
228
- h for h in logger.handlers if isinstance(h, WebSocketHandler)
229
- ]
230
-
231
- for handler in handlers_to_remove:
232
- logger.removeHandler(handler)
233
- handler.close()