claude-mpm 4.2.39__py3-none-any.whl → 4.2.42__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/agents/BASE_ENGINEER.md +114 -1
- claude_mpm/agents/BASE_OPS.md +156 -1
- claude_mpm/agents/INSTRUCTIONS.md +120 -11
- claude_mpm/agents/WORKFLOW.md +160 -10
- claude_mpm/agents/templates/agentic-coder-optimizer.json +17 -12
- claude_mpm/agents/templates/react_engineer.json +217 -0
- claude_mpm/agents/templates/web_qa.json +40 -4
- claude_mpm/cli/__init__.py +3 -5
- claude_mpm/commands/mpm-browser-monitor.md +370 -0
- claude_mpm/commands/mpm-monitor.md +177 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +1076 -2
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +465 -2
- claude_mpm/dashboard/static/css/dashboard.css +2 -0
- claude_mpm/dashboard/static/js/browser-console-monitor.js +495 -0
- claude_mpm/dashboard/static/js/components/browser-log-viewer.js +763 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +931 -340
- claude_mpm/dashboard/static/js/components/diff-viewer.js +891 -0
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +443 -0
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +690 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +307 -19
- claude_mpm/dashboard/static/js/socket-client.js +2 -2
- claude_mpm/dashboard/static/test-browser-monitor.html +470 -0
- claude_mpm/dashboard/templates/index.html +62 -99
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/monitor/daemon.py +69 -36
- claude_mpm/services/monitor/daemon_manager.py +186 -29
- claude_mpm/services/monitor/handlers/browser.py +451 -0
- claude_mpm/services/monitor/server.py +272 -5
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/RECORD +35 -29
- claude_mpm/agents/templates/agentic-coder-optimizer.md +0 -44
- claude_mpm/agents/templates/agentic_coder_optimizer.json +0 -238
- claude_mpm/agents/templates/test-non-mpm.json +0 -20
- claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,451 @@
|
|
1
|
+
"""
|
2
|
+
Browser Console Handler for Unified Monitor
|
3
|
+
===========================================
|
4
|
+
|
5
|
+
WHY: This handler manages browser console events from web pages that have
|
6
|
+
the browser console monitor script injected. It provides centralized logging
|
7
|
+
and debugging capabilities for client-side applications.
|
8
|
+
|
9
|
+
DESIGN DECISIONS:
|
10
|
+
- Creates separate log files per browser session for easy tracking
|
11
|
+
- Stores logs in .claude-mpm/logs/client/ directory
|
12
|
+
- Handles multiple concurrent browser connections
|
13
|
+
- Tracks active browser sessions with metadata
|
14
|
+
- Provides structured logging with timestamps and context
|
15
|
+
- Integrates with the unified monitor architecture
|
16
|
+
"""
|
17
|
+
|
18
|
+
import json
|
19
|
+
from datetime import datetime
|
20
|
+
from pathlib import Path
|
21
|
+
from typing import Dict, Set
|
22
|
+
|
23
|
+
import socketio
|
24
|
+
|
25
|
+
from ....core.logging_config import get_logger
|
26
|
+
|
27
|
+
|
28
|
+
class BrowserHandler:
|
29
|
+
"""Event handler for browser console monitoring functionality.
|
30
|
+
|
31
|
+
WHY: Manages browser console events from injected monitoring scripts
|
32
|
+
and provides centralized logging for client-side debugging.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, sio: socketio.AsyncServer):
|
36
|
+
"""Initialize the browser handler.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
sio: Socket.IO server instance
|
40
|
+
"""
|
41
|
+
self.sio = sio
|
42
|
+
self.logger = get_logger(__name__)
|
43
|
+
|
44
|
+
# Browser session management
|
45
|
+
self.active_browsers: Set[str] = set()
|
46
|
+
self.browser_info: Dict[str, Dict] = {}
|
47
|
+
|
48
|
+
# Logging configuration
|
49
|
+
self.log_dir = Path.cwd() / ".claude-mpm" / "logs" / "client"
|
50
|
+
self.log_files: Dict[str, Path] = {}
|
51
|
+
|
52
|
+
# Ensure log directory exists
|
53
|
+
self._ensure_log_directory()
|
54
|
+
|
55
|
+
def register(self):
|
56
|
+
"""Register Socket.IO event handlers."""
|
57
|
+
try:
|
58
|
+
# Browser connection events
|
59
|
+
self.sio.on("browser:connect", self.handle_browser_connect)
|
60
|
+
self.sio.on("browser:disconnect", self.handle_browser_disconnect)
|
61
|
+
self.sio.on("browser:hide", self.handle_browser_hide)
|
62
|
+
|
63
|
+
# Console events
|
64
|
+
self.sio.on("browser:console", self.handle_console_event)
|
65
|
+
|
66
|
+
# Browser management events
|
67
|
+
self.sio.on("browser:list", self.handle_browser_list)
|
68
|
+
self.sio.on("browser:info", self.handle_browser_info)
|
69
|
+
|
70
|
+
self.logger.info("Browser event handlers registered")
|
71
|
+
|
72
|
+
except Exception as e:
|
73
|
+
self.logger.error(f"Error registering browser handlers: {e}")
|
74
|
+
raise
|
75
|
+
|
76
|
+
def _ensure_log_directory(self):
|
77
|
+
"""Ensure the client logs directory exists."""
|
78
|
+
try:
|
79
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
80
|
+
self.logger.debug(f"Client logs directory ensured: {self.log_dir}")
|
81
|
+
except Exception as e:
|
82
|
+
self.logger.error(f"Error creating client logs directory: {e}")
|
83
|
+
raise
|
84
|
+
|
85
|
+
def _get_log_file_path(self, browser_id: str) -> Path:
|
86
|
+
"""Get log file path for a browser session.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
browser_id: Unique browser identifier
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Path to the log file
|
93
|
+
"""
|
94
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
95
|
+
log_filename = f"{browser_id}_{timestamp}.log"
|
96
|
+
return self.log_dir / log_filename
|
97
|
+
|
98
|
+
def _write_log_entry(
|
99
|
+
self,
|
100
|
+
browser_id: str,
|
101
|
+
level: str,
|
102
|
+
message: str,
|
103
|
+
timestamp: str = None,
|
104
|
+
extra_data: Dict = None,
|
105
|
+
):
|
106
|
+
"""Write a log entry to the browser's log file.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
browser_id: Browser identifier
|
110
|
+
level: Log level (INFO, ERROR, etc.)
|
111
|
+
message: Log message
|
112
|
+
timestamp: Event timestamp (ISO format)
|
113
|
+
extra_data: Additional event data
|
114
|
+
"""
|
115
|
+
try:
|
116
|
+
# Get or create log file for this browser
|
117
|
+
if browser_id not in self.log_files:
|
118
|
+
self.log_files[browser_id] = self._get_log_file_path(browser_id)
|
119
|
+
|
120
|
+
log_file = self.log_files[browser_id]
|
121
|
+
|
122
|
+
# Format timestamp
|
123
|
+
if not timestamp:
|
124
|
+
timestamp = datetime.now().isoformat()
|
125
|
+
|
126
|
+
# Format log entry
|
127
|
+
log_entry = f"[{timestamp}] [{level}] [{browser_id}] {message}"
|
128
|
+
|
129
|
+
# Add extra data if provided
|
130
|
+
if extra_data:
|
131
|
+
filtered_data = {
|
132
|
+
k: v
|
133
|
+
for k, v in extra_data.items()
|
134
|
+
if k not in ["browser_id", "level", "timestamp", "message"]
|
135
|
+
}
|
136
|
+
if filtered_data:
|
137
|
+
log_entry += f"\n Data: {json.dumps(filtered_data, indent=2)}"
|
138
|
+
|
139
|
+
# Write to file
|
140
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
141
|
+
f.write(log_entry + "\n")
|
142
|
+
|
143
|
+
self.logger.debug(f"Log entry written for {browser_id}: {level}")
|
144
|
+
|
145
|
+
except Exception as e:
|
146
|
+
self.logger.error(f"Error writing log entry for {browser_id}: {e}")
|
147
|
+
|
148
|
+
async def handle_browser_connect(self, sid: str, data: Dict):
|
149
|
+
"""Handle browser connection event.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
sid: Socket.IO session ID
|
153
|
+
data: Browser connection data
|
154
|
+
"""
|
155
|
+
try:
|
156
|
+
browser_id = data.get("browser_id")
|
157
|
+
if not browser_id:
|
158
|
+
self.logger.warning(f"Browser connect without ID from {sid}")
|
159
|
+
return
|
160
|
+
|
161
|
+
# Track browser session
|
162
|
+
self.active_browsers.add(browser_id)
|
163
|
+
|
164
|
+
# Store browser info
|
165
|
+
browser_info = {
|
166
|
+
"browser_id": browser_id,
|
167
|
+
"socket_id": sid,
|
168
|
+
"connected_at": datetime.now().isoformat(),
|
169
|
+
"user_agent": data.get("user_agent", "Unknown"),
|
170
|
+
"url": data.get("url", "Unknown"),
|
171
|
+
"last_activity": datetime.now().isoformat(),
|
172
|
+
}
|
173
|
+
self.browser_info[browser_id] = browser_info
|
174
|
+
|
175
|
+
# Log connection
|
176
|
+
self._write_log_entry(
|
177
|
+
browser_id,
|
178
|
+
"INFO",
|
179
|
+
f'Browser connected from {data.get("url", "unknown URL")}',
|
180
|
+
data.get("timestamp"),
|
181
|
+
browser_info,
|
182
|
+
)
|
183
|
+
|
184
|
+
self.logger.info(f"Browser connected: {browser_id} from {data.get('url')}")
|
185
|
+
|
186
|
+
# Send acknowledgment
|
187
|
+
await self.sio.emit(
|
188
|
+
"browser:connected",
|
189
|
+
{
|
190
|
+
"browser_id": browser_id,
|
191
|
+
"status": "connected",
|
192
|
+
"message": "Console monitoring active",
|
193
|
+
},
|
194
|
+
room=sid,
|
195
|
+
)
|
196
|
+
|
197
|
+
# Broadcast browser count update to dashboard
|
198
|
+
await self._broadcast_browser_stats()
|
199
|
+
|
200
|
+
except Exception as e:
|
201
|
+
self.logger.error(f"Error handling browser connect: {e}")
|
202
|
+
|
203
|
+
async def handle_browser_disconnect(self, sid: str, data: Dict):
|
204
|
+
"""Handle browser disconnection event.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
sid: Socket.IO session ID
|
208
|
+
data: Browser disconnection data
|
209
|
+
"""
|
210
|
+
try:
|
211
|
+
browser_id = data.get("browser_id")
|
212
|
+
if not browser_id:
|
213
|
+
return
|
214
|
+
|
215
|
+
# Log disconnection
|
216
|
+
self._write_log_entry(
|
217
|
+
browser_id, "INFO", "Browser disconnected", data.get("timestamp")
|
218
|
+
)
|
219
|
+
|
220
|
+
# Update browser info
|
221
|
+
if browser_id in self.browser_info:
|
222
|
+
self.browser_info[browser_id][
|
223
|
+
"disconnected_at"
|
224
|
+
] = datetime.now().isoformat()
|
225
|
+
self.browser_info[browser_id]["status"] = "disconnected"
|
226
|
+
|
227
|
+
self.logger.info(f"Browser disconnected: {browser_id}")
|
228
|
+
|
229
|
+
# Broadcast browser count update
|
230
|
+
await self._broadcast_browser_stats()
|
231
|
+
|
232
|
+
except Exception as e:
|
233
|
+
self.logger.error(f"Error handling browser disconnect: {e}")
|
234
|
+
|
235
|
+
async def handle_browser_hide(self, sid: str, data: Dict):
|
236
|
+
"""Handle browser hide event (tab hidden/mobile app backgrounded).
|
237
|
+
|
238
|
+
Args:
|
239
|
+
sid: Socket.IO session ID
|
240
|
+
data: Browser hide data
|
241
|
+
"""
|
242
|
+
try:
|
243
|
+
browser_id = data.get("browser_id")
|
244
|
+
if not browser_id:
|
245
|
+
return
|
246
|
+
|
247
|
+
# Log hide event
|
248
|
+
self._write_log_entry(
|
249
|
+
browser_id,
|
250
|
+
"INFO",
|
251
|
+
"Browser tab hidden/backgrounded",
|
252
|
+
data.get("timestamp"),
|
253
|
+
)
|
254
|
+
|
255
|
+
# Update browser info
|
256
|
+
if browser_id in self.browser_info:
|
257
|
+
self.browser_info[browser_id]["hidden_at"] = datetime.now().isoformat()
|
258
|
+
self.browser_info[browser_id]["status"] = "hidden"
|
259
|
+
|
260
|
+
self.logger.debug(f"Browser hidden: {browser_id}")
|
261
|
+
|
262
|
+
except Exception as e:
|
263
|
+
self.logger.error(f"Error handling browser hide: {e}")
|
264
|
+
|
265
|
+
async def handle_console_event(self, sid: str, data: Dict):
|
266
|
+
"""Handle console event from browser.
|
267
|
+
|
268
|
+
Args:
|
269
|
+
sid: Socket.IO session ID
|
270
|
+
data: Console event data
|
271
|
+
"""
|
272
|
+
try:
|
273
|
+
browser_id = data.get("browser_id")
|
274
|
+
level = data.get("level", "LOG")
|
275
|
+
message = data.get("message", "")
|
276
|
+
timestamp = data.get("timestamp")
|
277
|
+
|
278
|
+
if not browser_id:
|
279
|
+
self.logger.warning(f"Console event without browser ID from {sid}")
|
280
|
+
return
|
281
|
+
|
282
|
+
# Update last activity
|
283
|
+
if browser_id in self.browser_info:
|
284
|
+
self.browser_info[browser_id][
|
285
|
+
"last_activity"
|
286
|
+
] = datetime.now().isoformat()
|
287
|
+
|
288
|
+
# Log console event
|
289
|
+
self._write_log_entry(browser_id, level, message, timestamp, data)
|
290
|
+
|
291
|
+
# Log to main logger based on level
|
292
|
+
log_message = f"[{browser_id}] {message}"
|
293
|
+
if level == "ERROR":
|
294
|
+
self.logger.error(log_message)
|
295
|
+
elif level == "WARN":
|
296
|
+
self.logger.warning(log_message)
|
297
|
+
else:
|
298
|
+
self.logger.debug(log_message)
|
299
|
+
|
300
|
+
# Format log entry for dashboard
|
301
|
+
log_entry = {
|
302
|
+
"browser_id": browser_id,
|
303
|
+
"level": level,
|
304
|
+
"message": message,
|
305
|
+
"timestamp": timestamp,
|
306
|
+
"url": data.get("url"),
|
307
|
+
"line_info": data.get("line_info"),
|
308
|
+
}
|
309
|
+
|
310
|
+
# Forward event to dashboard clients using both event names for compatibility
|
311
|
+
await self.sio.emit("dashboard:browser:console", log_entry)
|
312
|
+
await self.sio.emit("browser_log", log_entry)
|
313
|
+
|
314
|
+
except Exception as e:
|
315
|
+
self.logger.error(f"Error handling console event: {e}")
|
316
|
+
|
317
|
+
async def handle_browser_list(self, sid: str, data: Dict):
|
318
|
+
"""Handle browser list request.
|
319
|
+
|
320
|
+
Args:
|
321
|
+
sid: Socket.IO session ID
|
322
|
+
data: Request data
|
323
|
+
"""
|
324
|
+
try:
|
325
|
+
browser_list = []
|
326
|
+
for browser_id, info in self.browser_info.items():
|
327
|
+
browser_list.append(
|
328
|
+
{
|
329
|
+
"browser_id": browser_id,
|
330
|
+
"url": info.get("url", "Unknown"),
|
331
|
+
"user_agent": info.get("user_agent", "Unknown"),
|
332
|
+
"connected_at": info.get("connected_at"),
|
333
|
+
"last_activity": info.get("last_activity"),
|
334
|
+
"status": info.get("status", "active"),
|
335
|
+
}
|
336
|
+
)
|
337
|
+
|
338
|
+
await self.sio.emit(
|
339
|
+
"browser:list:response",
|
340
|
+
{
|
341
|
+
"browsers": browser_list,
|
342
|
+
"total": len(browser_list),
|
343
|
+
"active": len(self.active_browsers),
|
344
|
+
},
|
345
|
+
room=sid,
|
346
|
+
)
|
347
|
+
|
348
|
+
except Exception as e:
|
349
|
+
self.logger.error(f"Error getting browser list: {e}")
|
350
|
+
await self.sio.emit(
|
351
|
+
"browser:error", {"error": f"Browser list error: {e!s}"}, room=sid
|
352
|
+
)
|
353
|
+
|
354
|
+
async def handle_browser_info(self, sid: str, data: Dict):
|
355
|
+
"""Handle browser info request.
|
356
|
+
|
357
|
+
Args:
|
358
|
+
sid: Socket.IO session ID
|
359
|
+
data: Request data containing browser_id
|
360
|
+
"""
|
361
|
+
try:
|
362
|
+
browser_id = data.get("browser_id")
|
363
|
+
if not browser_id or browser_id not in self.browser_info:
|
364
|
+
await self.sio.emit(
|
365
|
+
"browser:error", {"error": "Browser not found"}, room=sid
|
366
|
+
)
|
367
|
+
return
|
368
|
+
|
369
|
+
info = self.browser_info[browser_id]
|
370
|
+
|
371
|
+
# Add log file info
|
372
|
+
log_file_path = self.log_files.get(browser_id)
|
373
|
+
if log_file_path and log_file_path.exists():
|
374
|
+
info["log_file"] = str(log_file_path)
|
375
|
+
info["log_size"] = log_file_path.stat().st_size
|
376
|
+
|
377
|
+
await self.sio.emit("browser:info:response", info, room=sid)
|
378
|
+
|
379
|
+
except Exception as e:
|
380
|
+
self.logger.error(f"Error getting browser info: {e}")
|
381
|
+
await self.sio.emit(
|
382
|
+
"browser:error", {"error": f"Browser info error: {e!s}"}, room=sid
|
383
|
+
)
|
384
|
+
|
385
|
+
async def _broadcast_browser_stats(self):
|
386
|
+
"""Broadcast browser statistics to all dashboard clients."""
|
387
|
+
try:
|
388
|
+
stats = {
|
389
|
+
"total_browsers": len(self.browser_info),
|
390
|
+
"active_browsers": len(self.active_browsers),
|
391
|
+
"connected_browsers": len(
|
392
|
+
[
|
393
|
+
info
|
394
|
+
for info in self.browser_info.values()
|
395
|
+
if info.get("status") != "disconnected"
|
396
|
+
]
|
397
|
+
),
|
398
|
+
}
|
399
|
+
|
400
|
+
await self.sio.emit("dashboard:browser:stats", stats)
|
401
|
+
|
402
|
+
except Exception as e:
|
403
|
+
self.logger.error(f"Error broadcasting browser stats: {e}")
|
404
|
+
|
405
|
+
def cleanup_old_sessions(self, max_age_hours: int = 24):
|
406
|
+
"""Clean up old browser sessions and log files.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
max_age_hours: Maximum age in hours before cleanup
|
410
|
+
"""
|
411
|
+
try:
|
412
|
+
from datetime import timedelta
|
413
|
+
|
414
|
+
cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
|
415
|
+
|
416
|
+
# Remove old browser info
|
417
|
+
to_remove = []
|
418
|
+
for browser_id, info in self.browser_info.items():
|
419
|
+
try:
|
420
|
+
last_activity = datetime.fromisoformat(
|
421
|
+
info.get("last_activity", "")
|
422
|
+
)
|
423
|
+
if last_activity < cutoff_time:
|
424
|
+
to_remove.append(browser_id)
|
425
|
+
except (ValueError, TypeError):
|
426
|
+
# Invalid timestamp, mark for removal
|
427
|
+
to_remove.append(browser_id)
|
428
|
+
|
429
|
+
for browser_id in to_remove:
|
430
|
+
self.browser_info.pop(browser_id, None)
|
431
|
+
self.active_browsers.discard(browser_id)
|
432
|
+
self.log_files.pop(browser_id, None)
|
433
|
+
|
434
|
+
if to_remove:
|
435
|
+
self.logger.info(f"Cleaned up {len(to_remove)} old browser sessions")
|
436
|
+
|
437
|
+
except Exception as e:
|
438
|
+
self.logger.error(f"Error cleaning up browser sessions: {e}")
|
439
|
+
|
440
|
+
def get_stats(self) -> Dict:
|
441
|
+
"""Get handler statistics.
|
442
|
+
|
443
|
+
Returns:
|
444
|
+
Dictionary with handler stats
|
445
|
+
"""
|
446
|
+
return {
|
447
|
+
"total_browsers": len(self.browser_info),
|
448
|
+
"active_browsers": len(self.active_browsers),
|
449
|
+
"log_files": len(self.log_files),
|
450
|
+
"log_directory": str(self.log_dir),
|
451
|
+
}
|