claude-mpm 4.1.11__py3-none-any.whl → 4.1.13__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 (41) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +8 -0
  3. claude_mpm/cli/__init__.py +1 -1
  4. claude_mpm/cli/commands/monitor.py +88 -627
  5. claude_mpm/cli/commands/mpm_init.py +127 -107
  6. claude_mpm/cli/commands/mpm_init_handler.py +24 -23
  7. claude_mpm/cli/parsers/mpm_init_parser.py +34 -28
  8. claude_mpm/core/config.py +18 -0
  9. claude_mpm/core/instruction_reinforcement_hook.py +266 -0
  10. claude_mpm/core/pm_hook_interceptor.py +105 -8
  11. claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
  12. claude_mpm/dashboard/static/built/components/code-tree.js +1 -1
  13. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/css/activity.css +1239 -267
  16. claude_mpm/dashboard/static/css/dashboard.css +511 -0
  17. claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
  18. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  19. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  20. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  21. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  22. claude_mpm/dashboard/static/js/components/activity-tree.js +1193 -892
  23. claude_mpm/dashboard/static/js/components/build-tracker.js +15 -13
  24. claude_mpm/dashboard/static/js/components/code-tree.js +534 -143
  25. claude_mpm/dashboard/static/js/components/module-viewer.js +21 -7
  26. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +1066 -0
  27. claude_mpm/dashboard/static/js/connection-manager.js +1 -1
  28. claude_mpm/dashboard/static/js/dashboard.js +227 -84
  29. claude_mpm/dashboard/static/js/socket-client.js +2 -2
  30. claude_mpm/dashboard/templates/index.html +100 -23
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +11 -7
  32. claude_mpm/services/cli/socketio_manager.py +39 -8
  33. claude_mpm/services/infrastructure/monitoring.py +1 -1
  34. claude_mpm/services/socketio/handlers/code_analysis.py +83 -136
  35. claude_mpm/tools/code_tree_analyzer.py +290 -202
  36. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/METADATA +1 -1
  37. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/RECORD +41 -39
  38. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/WHEEL +0 -0
  39. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/entry_points.txt +0 -0
  40. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/licenses/LICENSE +0 -0
  41. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/top_level.txt +0 -0
@@ -172,10 +172,18 @@ class SocketIOManager(ISocketIOManager):
172
172
 
173
173
  # Check if server already running on this port
174
174
  if self.is_server_running(target_port):
175
- self.logger.info(
176
- f"Socket.IO server already running on port {target_port}"
175
+ # Verify the server is healthy and responding
176
+ if self.wait_for_server(target_port, timeout=2):
177
+ self.logger.info(
178
+ f"Healthy Socket.IO server already running on port {target_port}"
179
+ )
180
+ return True, self.get_server_info(target_port)
181
+ # Server exists but not responding, try to clean it up
182
+ self.logger.warning(
183
+ f"Socket.IO server on port {target_port} not responding, attempting cleanup"
177
184
  )
178
- return True, self.get_server_info(target_port)
185
+ self.stop_server(port=target_port, timeout=5)
186
+ # Continue with starting a new server
179
187
 
180
188
  # Ensure dependencies are available
181
189
  deps_ok, error_msg = self.ensure_dependencies()
@@ -459,14 +467,37 @@ class SocketIOManager(ISocketIOManager):
459
467
  Returns:
460
468
  Available port number
461
469
  """
462
- # Try preferred port first
470
+ # First check if our Socket.IO server is already running on the preferred port
471
+ if self.is_server_running(preferred_port):
472
+ # Check if it's healthy
473
+ if self.wait_for_server(preferred_port, timeout=2):
474
+ self.logger.info(
475
+ f"Healthy Socket.IO server already running on port {preferred_port}"
476
+ )
477
+ return preferred_port
478
+ self.logger.warning(
479
+ f"Socket.IO server on port {preferred_port} not responding, will try to restart"
480
+ )
481
+
482
+ # Try preferred port first if available
463
483
  if self.port_manager.is_port_available(preferred_port):
464
484
  return preferred_port
465
485
 
466
- # Find alternative port
467
- available_port = self.port_manager.get_available_port(preferred_port)
468
- self.logger.info(f"Port {preferred_port} unavailable, using {available_port}")
469
- return available_port
486
+ # Find alternative port using the correct method name
487
+ available_port = self.port_manager.find_available_port(
488
+ preferred_port=preferred_port, reclaim=True
489
+ )
490
+
491
+ if available_port:
492
+ self.logger.info(
493
+ f"Port {preferred_port} unavailable, using {available_port}"
494
+ )
495
+ return available_port
496
+ # If no port found, raise an error
497
+ raise RuntimeError(
498
+ f"No available ports in range {self.port_manager.PORT_RANGE.start}-"
499
+ f"{self.port_manager.PORT_RANGE.stop-1}"
500
+ )
470
501
 
471
502
  def ensure_dependencies(self) -> Tuple[bool, Optional[str]]:
472
503
  """
@@ -30,7 +30,7 @@ For backward compatibility, legacy classes are still available:
30
30
  """
31
31
 
32
32
  # Re-export all components from the modular implementation
33
- from .monitoring import ( # noqa: F401; New service-based API; Base components; Legacy compatibility
33
+ from .monitoring import (
34
34
  AdvancedHealthMonitor,
35
35
  HealthChecker,
36
36
  HealthCheckResult,
@@ -216,56 +216,22 @@ class CodeAnalysisEventHandler(BaseEventHandler):
216
216
  )
217
217
  return
218
218
 
219
- # ADDITIONAL SECURITY: Ensure path is within working directory bounds
220
- # This prevents access to system directories like /Users, /System, etc.
221
- working_dir = Path.cwd().absolute()
222
- try:
223
- requested_path = Path(path).absolute()
224
- # This will raise ValueError if path is not within working_dir
225
- requested_path.relative_to(working_dir)
226
- except ValueError:
227
- self.logger.warning(
228
- f"Access denied - path outside working directory: {path}"
229
- )
230
- await self.server.core.sio.emit(
231
- "code:analysis:error",
232
- {
233
- "error": f"Access denied: Path outside working directory: {path}",
234
- "request_id": data.get("request_id"),
235
- "path": path,
236
- },
237
- room=sid,
238
- )
239
- return
219
+ # SECURITY: Validate the requested path
220
+ # Allow access to the explicitly chosen working directory and its subdirectories
221
+ requested_path = Path(path).absolute()
222
+
223
+ # For now, we trust the frontend to send valid paths
224
+ # In production, you might want to maintain a server-side list of allowed directories
225
+ # or implement a more sophisticated permission system
226
+
227
+ # Basic sanity checks are done below after creating the Path object
240
228
 
241
229
  ignore_patterns = data.get("ignore_patterns", [])
242
230
  request_id = data.get("request_id")
243
- show_hidden_files = data.get("show_hidden_files", False)
244
-
245
- # Extensive debug logging
246
- self.logger.info(f"[DEBUG] handle_discover_top_level START")
247
- self.logger.info(f"[DEBUG] Received show_hidden_files={show_hidden_files} (type: {type(show_hidden_files)})")
248
- self.logger.info(f"[DEBUG] Current analyzer exists: {self.code_analyzer is not None}")
249
- if self.code_analyzer:
250
- current_value = getattr(self.code_analyzer, 'show_hidden_files', 'NOT_FOUND')
251
- self.logger.info(f"[DEBUG] Current analyzer show_hidden_files={current_value}")
252
- self.logger.info(f"[DEBUG] Full request data: {data}")
253
231
 
254
232
  try:
255
- # Create analyzer if needed or recreate if show_hidden_files changed
256
- current_show_hidden = getattr(self.code_analyzer, 'show_hidden_files', None) if self.code_analyzer else None
257
- need_recreate = (
258
- not self.code_analyzer or
259
- current_show_hidden != show_hidden_files
260
- )
261
-
262
- self.logger.info(f"[DEBUG] Analyzer recreation check:")
263
- self.logger.info(f"[DEBUG] - Analyzer exists: {self.code_analyzer is not None}")
264
- self.logger.info(f"[DEBUG] - Current show_hidden: {current_show_hidden}")
265
- self.logger.info(f"[DEBUG] - Requested show_hidden: {show_hidden_files}")
266
- self.logger.info(f"[DEBUG] - Need recreate: {need_recreate}")
267
-
268
- if need_recreate:
233
+ # Create analyzer if needed
234
+ if not self.code_analyzer:
269
235
  # Create a custom emitter that sends to Socket.IO
270
236
  emitter = CodeTreeEventEmitter(use_stdout=False)
271
237
  # Override emit method to send to Socket.IO
@@ -276,14 +242,14 @@ class CodeAnalysisEventHandler(BaseEventHandler):
276
242
  ):
277
243
  # Keep the original event format with colons - frontend expects this!
278
244
  # The frontend listens for 'code:directory:discovered' not 'code.directory.discovered'
279
-
245
+
280
246
  # Special handling for 'info' events - they should be passed through directly
281
- if event_type == 'info':
247
+ if event_type == "info":
282
248
  # INFO events for granular tracking
283
249
  loop = asyncio.get_event_loop()
284
250
  loop.create_task(
285
251
  self.server.core.sio.emit(
286
- 'info', {"request_id": request_id, **event_data}
252
+ "info", {"request_id": request_id, **event_data}
287
253
  )
288
254
  )
289
255
  else:
@@ -298,14 +264,9 @@ class CodeAnalysisEventHandler(BaseEventHandler):
298
264
  original_emit(event_type, event_data, batch)
299
265
 
300
266
  emitter.emit = socket_emit
301
- # Initialize CodeTreeAnalyzer with emitter keyword argument and show_hidden_files
302
- self.logger.info(f"[DEBUG] Creating new CodeTreeAnalyzer with show_hidden_files={show_hidden_files}")
303
- self.code_analyzer = CodeTreeAnalyzer(emitter=emitter, show_hidden_files=show_hidden_files)
304
- self.logger.info(f"[DEBUG] CodeTreeAnalyzer created:")
305
- self.logger.info(f"[DEBUG] - analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
306
- self.logger.info(f"[DEBUG] - gitignore_manager.show_hidden_files={self.code_analyzer.gitignore_manager.show_hidden_files}")
307
- else:
308
- self.logger.info(f"[DEBUG] Reusing existing analyzer with show_hidden_files={self.code_analyzer.show_hidden_files}")
267
+ # Initialize CodeTreeAnalyzer with emitter keyword argument
268
+ self.logger.info("Creating CodeTreeAnalyzer")
269
+ self.code_analyzer = CodeTreeAnalyzer(emitter=emitter)
309
270
 
310
271
  # Use the provided path as-is - the frontend sends the absolute path
311
272
  # Make sure we're using an absolute path
@@ -341,17 +302,7 @@ class CodeAnalysisEventHandler(BaseEventHandler):
341
302
  f"Discovering top-level contents of: {directory.absolute()}"
342
303
  )
343
304
 
344
- # Log before discovery
345
- self.logger.info(f"[DEBUG] About to discover with analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
346
-
347
305
  result = self.code_analyzer.discover_top_level(directory, ignore_patterns)
348
-
349
- # Log what we got back
350
- num_items = len(result.get("children", []))
351
- dotfiles = [c for c in result.get("children", []) if c.get("name", "").startswith(".")]
352
- self.logger.info(f"[DEBUG] Discovery result: {num_items} items, {len(dotfiles)} dotfiles")
353
- if dotfiles:
354
- self.logger.info(f"[DEBUG] Dotfiles found: {[d.get('name') for d in dotfiles]}")
355
306
 
356
307
  # Send result to client with correct event name for top level discovery
357
308
  await self.server.core.sio.emit(
@@ -404,7 +355,6 @@ class CodeAnalysisEventHandler(BaseEventHandler):
404
355
  path = data.get("path")
405
356
  ignore_patterns = data.get("ignore_patterns", [])
406
357
  request_id = data.get("request_id")
407
- show_hidden_files = data.get("show_hidden_files", False)
408
358
 
409
359
  if not path:
410
360
  await self.server.core.sio.emit(
@@ -432,21 +382,34 @@ class CodeAnalysisEventHandler(BaseEventHandler):
432
382
  )
433
383
  return
434
384
 
435
- # ADDITIONAL SECURITY: Ensure path is within working directory bounds
436
- # This prevents access to system directories like /Users, /System, etc.
437
- working_dir = Path.cwd().absolute()
438
- try:
439
- requested_path = Path(path).absolute()
440
- # This will raise ValueError if path is not within working_dir
441
- requested_path.relative_to(working_dir)
442
- except ValueError:
443
- self.logger.warning(
444
- f"Access denied - path outside working directory: {path}"
385
+ # SECURITY: Validate the requested path
386
+ # Allow access to the explicitly chosen working directory and its subdirectories
387
+ requested_path = Path(path).absolute()
388
+
389
+ # For now, we trust the frontend to send valid paths
390
+ # In production, you might want to maintain a server-side list of allowed directories
391
+ # or implement a more sophisticated permission system
392
+
393
+ # Basic sanity checks
394
+ if not requested_path.exists():
395
+ self.logger.warning(f"Path does not exist: {path}")
396
+ await self.server.core.sio.emit(
397
+ "code:analysis:error",
398
+ {
399
+ "error": f"Path does not exist: {path}",
400
+ "request_id": request_id,
401
+ "path": path,
402
+ },
403
+ room=sid,
445
404
  )
405
+ return
406
+
407
+ if not requested_path.is_dir():
408
+ self.logger.warning(f"Path is not a directory: {path}")
446
409
  await self.server.core.sio.emit(
447
410
  "code:analysis:error",
448
411
  {
449
- "error": f"Access denied: Path outside working directory: {path}",
412
+ "error": f"Path is not a directory: {path}",
450
413
  "request_id": request_id,
451
414
  "path": path,
452
415
  },
@@ -455,20 +418,8 @@ class CodeAnalysisEventHandler(BaseEventHandler):
455
418
  return
456
419
 
457
420
  try:
458
- # Ensure analyzer exists or recreate if show_hidden_files changed
459
- current_show_hidden = getattr(self.code_analyzer, 'show_hidden_files', None) if self.code_analyzer else None
460
- need_recreate = (
461
- not self.code_analyzer or
462
- current_show_hidden != show_hidden_files
463
- )
464
-
465
- self.logger.info(f"[DEBUG] Analyzer recreation check:")
466
- self.logger.info(f"[DEBUG] - Analyzer exists: {self.code_analyzer is not None}")
467
- self.logger.info(f"[DEBUG] - Current show_hidden: {current_show_hidden}")
468
- self.logger.info(f"[DEBUG] - Requested show_hidden: {show_hidden_files}")
469
- self.logger.info(f"[DEBUG] - Need recreate: {need_recreate}")
470
-
471
- if need_recreate:
421
+ # Ensure analyzer exists
422
+ if not self.code_analyzer:
472
423
  emitter = CodeTreeEventEmitter(use_stdout=False)
473
424
  # Override emit method to send to Socket.IO
474
425
  original_emit = emitter.emit
@@ -478,14 +429,14 @@ class CodeAnalysisEventHandler(BaseEventHandler):
478
429
  ):
479
430
  # Keep the original event format with colons - frontend expects this!
480
431
  # The frontend listens for 'code:directory:discovered' not 'code.directory.discovered'
481
-
432
+
482
433
  # Special handling for 'info' events - they should be passed through directly
483
- if event_type == 'info':
434
+ if event_type == "info":
484
435
  # INFO events for granular tracking
485
436
  loop = asyncio.get_event_loop()
486
437
  loop.create_task(
487
438
  self.server.core.sio.emit(
488
- 'info', {"request_id": request_id, **event_data}
439
+ "info", {"request_id": request_id, **event_data}
489
440
  )
490
441
  )
491
442
  else:
@@ -499,17 +450,19 @@ class CodeAnalysisEventHandler(BaseEventHandler):
499
450
  original_emit(event_type, event_data, batch)
500
451
 
501
452
  emitter.emit = socket_emit
502
- # Initialize CodeTreeAnalyzer with emitter keyword argument and show_hidden_files
503
- self.logger.info(f"[DEBUG] Creating new CodeTreeAnalyzer with show_hidden_files={show_hidden_files}")
504
- self.code_analyzer = CodeTreeAnalyzer(emitter=emitter, show_hidden_files=show_hidden_files)
505
- self.logger.info(f"[DEBUG] CodeTreeAnalyzer created, analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
506
- self.logger.info(f"[DEBUG] GitignoreManager.show_hidden_files={self.code_analyzer.gitignore_manager.show_hidden_files}")
507
- else:
508
- self.logger.info(f"[DEBUG] Reusing analyzer with show_hidden_files={self.code_analyzer.show_hidden_files}")
453
+ # Initialize CodeTreeAnalyzer with emitter keyword argument
454
+ self.logger.info("Creating CodeTreeAnalyzer")
455
+ self.code_analyzer = CodeTreeAnalyzer(emitter=emitter)
509
456
 
510
457
  # Discover directory
511
458
  result = self.code_analyzer.discover_directory(path, ignore_patterns)
512
459
 
460
+ # Log what we're sending
461
+ self.logger.info(
462
+ f"Discovery result for {path}: {len(result.get('children', []))} children found"
463
+ )
464
+ self.logger.debug(f"Full result: {result}")
465
+
513
466
  # Send result with correct event name (using colons, not dots!)
514
467
  await self.server.core.sio.emit(
515
468
  "code:directory:discovered",
@@ -571,20 +524,30 @@ class CodeAnalysisEventHandler(BaseEventHandler):
571
524
  )
572
525
  return
573
526
 
574
- # ADDITIONAL SECURITY: Ensure file is within working directory bounds
575
- working_dir = Path.cwd().absolute()
576
- try:
577
- requested_path = Path(path).absolute()
578
- # This will raise ValueError if path is not within working_dir
579
- requested_path.relative_to(working_dir)
580
- except ValueError:
581
- self.logger.warning(
582
- f"Access denied - file outside working directory: {path}"
527
+ # SECURITY: Validate the requested file path
528
+ # Allow access to files within the explicitly chosen working directory
529
+ requested_path = Path(path).absolute()
530
+
531
+ # Basic sanity checks
532
+ if not requested_path.exists():
533
+ self.logger.warning(f"File does not exist: {path}")
534
+ await self.server.core.sio.emit(
535
+ "code:analysis:error",
536
+ {
537
+ "error": f"File does not exist: {path}",
538
+ "request_id": request_id,
539
+ "path": path,
540
+ },
541
+ room=sid,
583
542
  )
543
+ return
544
+
545
+ if not requested_path.is_file():
546
+ self.logger.warning(f"Path is not a file: {path}")
584
547
  await self.server.core.sio.emit(
585
548
  "code:analysis:error",
586
549
  {
587
- "error": f"Access denied: File outside working directory: {path}",
550
+ "error": f"Path is not a file: {path}",
588
551
  "request_id": request_id,
589
552
  "path": path,
590
553
  },
@@ -593,20 +556,8 @@ class CodeAnalysisEventHandler(BaseEventHandler):
593
556
  return
594
557
 
595
558
  try:
596
- # Ensure analyzer exists or recreate if show_hidden_files changed
597
- current_show_hidden = getattr(self.code_analyzer, 'show_hidden_files', None) if self.code_analyzer else None
598
- need_recreate = (
599
- not self.code_analyzer or
600
- current_show_hidden != show_hidden_files
601
- )
602
-
603
- self.logger.info(f"[DEBUG] Analyzer recreation check:")
604
- self.logger.info(f"[DEBUG] - Analyzer exists: {self.code_analyzer is not None}")
605
- self.logger.info(f"[DEBUG] - Current show_hidden: {current_show_hidden}")
606
- self.logger.info(f"[DEBUG] - Requested show_hidden: {show_hidden_files}")
607
- self.logger.info(f"[DEBUG] - Need recreate: {need_recreate}")
608
-
609
- if need_recreate:
559
+ # Ensure analyzer exists
560
+ if not self.code_analyzer:
610
561
  emitter = CodeTreeEventEmitter(use_stdout=False)
611
562
  # Override emit method to send to Socket.IO
612
563
  original_emit = emitter.emit
@@ -616,14 +567,14 @@ class CodeAnalysisEventHandler(BaseEventHandler):
616
567
  ):
617
568
  # Keep the original event format with colons - frontend expects this!
618
569
  # The frontend listens for 'code:file:analyzed' not 'code.file.analyzed'
619
-
570
+
620
571
  # Special handling for 'info' events - they should be passed through directly
621
- if event_type == 'info':
572
+ if event_type == "info":
622
573
  # INFO events for granular tracking
623
574
  loop = asyncio.get_event_loop()
624
575
  loop.create_task(
625
576
  self.server.core.sio.emit(
626
- 'info', {"request_id": request_id, **event_data}
577
+ "info", {"request_id": request_id, **event_data}
627
578
  )
628
579
  )
629
580
  else:
@@ -637,13 +588,9 @@ class CodeAnalysisEventHandler(BaseEventHandler):
637
588
  original_emit(event_type, event_data, batch)
638
589
 
639
590
  emitter.emit = socket_emit
640
- # Initialize CodeTreeAnalyzer with emitter keyword argument and show_hidden_files
641
- self.logger.info(f"[DEBUG] Creating new CodeTreeAnalyzer with show_hidden_files={show_hidden_files}")
642
- self.code_analyzer = CodeTreeAnalyzer(emitter=emitter, show_hidden_files=show_hidden_files)
643
- self.logger.info(f"[DEBUG] CodeTreeAnalyzer created, analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
644
- self.logger.info(f"[DEBUG] GitignoreManager.show_hidden_files={self.code_analyzer.gitignore_manager.show_hidden_files}")
645
- else:
646
- self.logger.info(f"[DEBUG] Reusing analyzer with show_hidden_files={self.code_analyzer.show_hidden_files}")
591
+ # Initialize CodeTreeAnalyzer with emitter keyword argument
592
+ self.logger.info("Creating CodeTreeAnalyzer")
593
+ self.code_analyzer = CodeTreeAnalyzer(emitter=emitter)
647
594
 
648
595
  # Analyze file
649
596
  result = self.code_analyzer.analyze_file(path)