claude-mpm 3.3.0__py3-none-any.whl → 3.3.2__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 (33) 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 +575 -25
  12. claude_mpm/cli/commands/run.py +115 -14
  13. claude_mpm/cli/parser.py +76 -0
  14. claude_mpm/constants.py +5 -0
  15. claude_mpm/core/claude_runner.py +13 -11
  16. claude_mpm/core/session_manager.py +46 -0
  17. claude_mpm/core/simple_runner.py +13 -11
  18. claude_mpm/hooks/claude_hooks/hook_handler.py +2 -26
  19. claude_mpm/services/agent_memory_manager.py +264 -23
  20. claude_mpm/services/memory_builder.py +491 -0
  21. claude_mpm/services/memory_optimizer.py +619 -0
  22. claude_mpm/services/memory_router.py +445 -0
  23. claude_mpm/services/socketio_server.py +184 -20
  24. claude_mpm-3.3.2.dist-info/METADATA +159 -0
  25. {claude_mpm-3.3.0.dist-info → claude_mpm-3.3.2.dist-info}/RECORD +29 -28
  26. claude_mpm/agents/templates/test-integration-agent.md +0 -34
  27. claude_mpm/core/websocket_handler.py +0 -233
  28. claude_mpm/services/websocket_server.py +0 -376
  29. claude_mpm-3.3.0.dist-info/METADATA +0 -432
  30. {claude_mpm-3.3.0.dist-info → claude_mpm-3.3.2.dist-info}/WHEEL +0 -0
  31. {claude_mpm-3.3.0.dist-info → claude_mpm-3.3.2.dist-info}/entry_points.txt +0 -0
  32. {claude_mpm-3.3.0.dist-info → claude_mpm-3.3.2.dist-info}/licenses/LICENSE +0 -0
  33. {claude_mpm-3.3.0.dist-info → claude_mpm-3.3.2.dist-info}/top_level.txt +0 -0
@@ -1,34 +0,0 @@
1
- ---
2
- author: test-script
3
- model_preference: claude-3-sonnet
4
- specializations: []
5
- tags:
6
- - test
7
- - integration
8
- type: custom
9
- version: 1.0.0
10
- ---
11
-
12
- # Test Integration Agent
13
-
14
- ## 🎯 Primary Role
15
- test-integration-agent agent
16
-
17
- ## 🎯 When to Use This Agent
18
-
19
- **Select this agent when:**
20
-
21
- **Do NOT select for:**
22
-
23
- ## 🔧 Core Capabilities
24
-
25
- ## 🔑 Authority & Permissions
26
-
27
- ### ✅ Exclusive Write Access
28
-
29
- ### ❌ Forbidden Operations
30
-
31
- ---
32
- **Agent Type**: custom
33
- **Model Preference**: claude-3-sonnet
34
- **Version**: 1.0.0
@@ -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()
@@ -1,376 +0,0 @@
1
- """WebSocket server for real-time monitoring of Claude MPM sessions."""
2
-
3
- import asyncio
4
- import json
5
- import logging
6
- import os
7
- import subprocess
8
- import threading
9
- import time
10
- from datetime import datetime
11
- from typing import Set, Dict, Any, Optional, List
12
- from collections import deque
13
-
14
- try:
15
- import websockets
16
- from websockets.server import WebSocketServerProtocol
17
- WEBSOCKETS_AVAILABLE = True
18
- except ImportError:
19
- WEBSOCKETS_AVAILABLE = False
20
- websockets = None
21
- WebSocketServerProtocol = None
22
-
23
- from ..core.logger import get_logger
24
-
25
-
26
- class WebSocketServer:
27
- """WebSocket server for broadcasting Claude MPM events."""
28
-
29
- def __init__(self, host: str = "localhost", port: int = 8765):
30
- self.host = host
31
- self.port = port
32
- self.logger = get_logger("websocket_server")
33
- self.clients: Set[WebSocketServerProtocol] = set() if WEBSOCKETS_AVAILABLE else set()
34
- self.event_history: deque = deque(maxlen=1000) # Keep last 1000 events
35
- self.server = None
36
- self.loop = None
37
- self.thread = None
38
- self.running = False
39
-
40
- # Session state
41
- self.session_id = None
42
- self.session_start = None
43
- self.claude_status = "stopped"
44
- self.claude_pid = None
45
-
46
- if not WEBSOCKETS_AVAILABLE:
47
- self.logger.warning("WebSocket support not available. Install 'websockets' package to enable.")
48
-
49
- def start(self):
50
- """Start the WebSocket server in a background thread."""
51
- if not WEBSOCKETS_AVAILABLE:
52
- self.logger.debug("WebSocket server skipped - websockets package not installed")
53
- return
54
-
55
- if self.running:
56
- return
57
-
58
- self.running = True
59
- self.thread = threading.Thread(target=self._run_server, daemon=True)
60
- self.thread.start()
61
- self.logger.info(f"WebSocket server starting on ws://{self.host}:{self.port}")
62
-
63
- def stop(self):
64
- """Stop the WebSocket server."""
65
- self.running = False
66
- if self.loop:
67
- asyncio.run_coroutine_threadsafe(self._shutdown(), self.loop)
68
- if self.thread:
69
- self.thread.join(timeout=5)
70
- self.logger.info("WebSocket server stopped")
71
-
72
- def _run_server(self):
73
- """Run the server event loop."""
74
- self.loop = asyncio.new_event_loop()
75
- asyncio.set_event_loop(self.loop)
76
-
77
- try:
78
- self.loop.run_until_complete(self._serve())
79
- except Exception as e:
80
- self.logger.error(f"WebSocket server error: {e}")
81
- finally:
82
- self.loop.close()
83
-
84
- async def _serve(self):
85
- """Start the WebSocket server."""
86
- async with websockets.serve(self._handle_client, self.host, self.port):
87
- self.logger.info(f"WebSocket server listening on ws://{self.host}:{self.port}")
88
- while self.running:
89
- await asyncio.sleep(0.1)
90
-
91
- async def _shutdown(self):
92
- """Shutdown the server."""
93
- # Close all client connections
94
- if self.clients:
95
- await asyncio.gather(
96
- *[client.close() for client in self.clients],
97
- return_exceptions=True
98
- )
99
-
100
- async def _handle_client(self, websocket: WebSocketServerProtocol, path: str):
101
- """Handle a new client connection."""
102
- self.clients.add(websocket)
103
- client_addr = websocket.remote_address
104
- self.logger.info(f"Client connected from {client_addr}")
105
-
106
- try:
107
- # Send current status
108
- await self._send_current_status(websocket)
109
-
110
- # Handle client messages
111
- async for message in websocket:
112
- try:
113
- data = json.loads(message)
114
- await self._handle_command(websocket, data)
115
- except json.JSONDecodeError:
116
- await websocket.send(json.dumps({
117
- "type": "error",
118
- "data": {"message": "Invalid JSON"}
119
- }))
120
-
121
- except websockets.exceptions.ConnectionClosed:
122
- pass
123
- finally:
124
- self.clients.remove(websocket)
125
- self.logger.info(f"Client disconnected from {client_addr}")
126
-
127
- async def _send_current_status(self, websocket: WebSocketServerProtocol):
128
- """Send current system status to a client."""
129
- status = {
130
- "type": "system.status",
131
- "timestamp": datetime.utcnow().isoformat() + "Z",
132
- "data": {
133
- "session_id": self.session_id,
134
- "session_start": self.session_start,
135
- "claude_status": self.claude_status,
136
- "claude_pid": self.claude_pid,
137
- "connected_clients": len(self.clients),
138
- "websocket_port": self.port,
139
- "instance_info": {
140
- "port": self.port,
141
- "host": self.host,
142
- "working_dir": os.getcwd() if self.session_id else None
143
- }
144
- }
145
- }
146
- await websocket.send(json.dumps(status))
147
-
148
- async def _handle_command(self, websocket: WebSocketServerProtocol, data: Dict[str, Any]):
149
- """Handle commands from clients."""
150
- command = data.get("command")
151
-
152
- if command == "get_status":
153
- await self._send_current_status(websocket)
154
-
155
- elif command == "get_history":
156
- # Send recent events
157
- params = data.get("params", {})
158
- event_types = params.get("event_types", [])
159
- limit = min(params.get("limit", 100), len(self.event_history))
160
-
161
- history = []
162
- for event in reversed(self.event_history):
163
- if not event_types or event["type"] in event_types:
164
- history.append(event)
165
- if len(history) >= limit:
166
- break
167
-
168
- await websocket.send(json.dumps({
169
- "type": "history",
170
- "data": {"events": list(reversed(history))}
171
- }))
172
-
173
- elif command == "subscribe":
174
- # For now, all clients get all events
175
- await websocket.send(json.dumps({
176
- "type": "subscribed",
177
- "data": {"channels": data.get("channels", ["*"])}
178
- }))
179
-
180
- def broadcast_event(self, event_type: str, data: Dict[str, Any]):
181
- """Broadcast an event to all connected clients."""
182
- if not WEBSOCKETS_AVAILABLE:
183
- return
184
-
185
- event = {
186
- "type": event_type,
187
- "timestamp": datetime.utcnow().isoformat() + "Z",
188
- "data": data
189
- }
190
-
191
- # Store in history
192
- self.event_history.append(event)
193
-
194
- # Broadcast to clients
195
- if self.clients and self.loop:
196
- asyncio.run_coroutine_threadsafe(
197
- self._broadcast(json.dumps(event)),
198
- self.loop
199
- )
200
-
201
- async def _broadcast(self, message: str):
202
- """Send a message to all connected clients."""
203
- if self.clients:
204
- # Send to all clients concurrently
205
- await asyncio.gather(
206
- *[client.send(message) for client in self.clients],
207
- return_exceptions=True
208
- )
209
-
210
- # Convenience methods for common events
211
-
212
- def _get_git_branch(self, working_dir: str) -> str:
213
- """Get the current git branch for the working directory."""
214
- try:
215
- result = subprocess.run(
216
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
217
- cwd=working_dir,
218
- capture_output=True,
219
- text=True,
220
- timeout=2
221
- )
222
- if result.returncode == 0:
223
- return result.stdout.strip()
224
- except Exception:
225
- pass
226
- return "not a git repo"
227
-
228
- def session_started(self, session_id: str, launch_method: str, working_dir: str):
229
- """Notify that a session has started."""
230
- self.session_id = session_id
231
- self.session_start = datetime.utcnow().isoformat() + "Z"
232
-
233
- # Get git branch if in a git repo
234
- git_branch = self._get_git_branch(working_dir)
235
-
236
- self.broadcast_event("session.start", {
237
- "session_id": session_id,
238
- "start_time": self.session_start,
239
- "launch_method": launch_method,
240
- "working_directory": working_dir,
241
- "git_branch": git_branch,
242
- "websocket_port": self.port,
243
- "instance_info": {
244
- "port": self.port,
245
- "host": self.host,
246
- "working_dir": working_dir
247
- }
248
- })
249
-
250
- def session_ended(self):
251
- """Notify that a session has ended."""
252
- if self.session_id:
253
- duration = None
254
- if self.session_start:
255
- start = datetime.fromisoformat(self.session_start.replace("Z", "+00:00"))
256
- duration = (datetime.utcnow() - start.replace(tzinfo=None)).total_seconds()
257
-
258
- self.broadcast_event("session.end", {
259
- "session_id": self.session_id,
260
- "end_time": datetime.utcnow().isoformat() + "Z",
261
- "duration_seconds": duration
262
- })
263
-
264
- self.session_id = None
265
- self.session_start = None
266
-
267
- def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
268
- """Notify Claude status change."""
269
- self.claude_status = status
270
- self.claude_pid = pid
271
- self.broadcast_event("claude.status", {
272
- "status": status,
273
- "pid": pid,
274
- "message": message
275
- })
276
-
277
- def claude_output(self, content: str, stream: str = "stdout"):
278
- """Broadcast Claude output."""
279
- self.broadcast_event("claude.output", {
280
- "content": content,
281
- "stream": stream
282
- })
283
-
284
- def agent_delegated(self, agent: str, task: str, status: str = "started"):
285
- """Notify agent delegation."""
286
- self.broadcast_event("agent.delegation", {
287
- "agent": agent,
288
- "task": task,
289
- "status": status,
290
- "timestamp": datetime.utcnow().isoformat() + "Z"
291
- })
292
-
293
- def todo_updated(self, todos: List[Dict[str, Any]]):
294
- """Notify todo list update."""
295
- stats = {
296
- "total": len(todos),
297
- "completed": sum(1 for t in todos if t.get("status") == "completed"),
298
- "in_progress": sum(1 for t in todos if t.get("status") == "in_progress"),
299
- "pending": sum(1 for t in todos if t.get("status") == "pending")
300
- }
301
-
302
- self.broadcast_event("todo.update", {
303
- "todos": todos,
304
- "stats": stats
305
- })
306
-
307
- def ticket_created(self, ticket_id: str, title: str, priority: str = "medium"):
308
- """Notify ticket creation."""
309
- self.broadcast_event("ticket.created", {
310
- "id": ticket_id,
311
- "title": title,
312
- "priority": priority,
313
- "created_at": datetime.utcnow().isoformat() + "Z"
314
- })
315
-
316
- def memory_loaded(self, agent_id: str, memory_size: int, sections_count: int):
317
- """Notify when agent memory is loaded from file."""
318
- self.broadcast_event("memory:loaded", {
319
- "agent_id": agent_id,
320
- "memory_size": memory_size,
321
- "sections_count": sections_count,
322
- "timestamp": datetime.utcnow().isoformat() + "Z"
323
- })
324
-
325
- def memory_created(self, agent_id: str, template_type: str):
326
- """Notify when new agent memory is created from template."""
327
- self.broadcast_event("memory:created", {
328
- "agent_id": agent_id,
329
- "template_type": template_type,
330
- "timestamp": datetime.utcnow().isoformat() + "Z"
331
- })
332
-
333
- def memory_updated(self, agent_id: str, learning_type: str, content: str, section: str):
334
- """Notify when learning is added to agent memory."""
335
- self.broadcast_event("memory:updated", {
336
- "agent_id": agent_id,
337
- "learning_type": learning_type,
338
- "content": content,
339
- "section": section,
340
- "timestamp": datetime.utcnow().isoformat() + "Z"
341
- })
342
-
343
- def memory_injected(self, agent_id: str, context_size: int):
344
- """Notify when agent memory is injected into context."""
345
- self.broadcast_event("memory:injected", {
346
- "agent_id": agent_id,
347
- "context_size": context_size,
348
- "timestamp": datetime.utcnow().isoformat() + "Z"
349
- })
350
-
351
-
352
- # Global instance for easy access
353
- _websocket_server: Optional[WebSocketServer] = None
354
-
355
-
356
- def get_websocket_server() -> WebSocketServer:
357
- """Get or create the global WebSocket server instance."""
358
- global _websocket_server
359
- if _websocket_server is None:
360
- _websocket_server = WebSocketServer()
361
- return _websocket_server
362
-
363
-
364
- def start_websocket_server():
365
- """Start the global WebSocket server."""
366
- server = get_websocket_server()
367
- server.start()
368
- return server
369
-
370
-
371
- def stop_websocket_server():
372
- """Stop the global WebSocket server."""
373
- global _websocket_server
374
- if _websocket_server:
375
- _websocket_server.stop()
376
- _websocket_server = None