claude-mpm 5.4.14__py3-none-any.whl → 5.4.36__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (103) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT.md +164 -0
  3. claude_mpm/agents/BASE_ENGINEER.md +658 -0
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  5. claude_mpm/agents/MEMORY.md +1 -1
  6. claude_mpm/agents/PM_INSTRUCTIONS.md +363 -817
  7. claude_mpm/agents/WORKFLOW.md +5 -254
  8. claude_mpm/agents/agent_loader.py +1 -1
  9. claude_mpm/agents/base_agent.json +31 -0
  10. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  11. claude_mpm/cli/commands/agent_state_manager.py +10 -10
  12. claude_mpm/cli/commands/agents.py +9 -40
  13. claude_mpm/cli/commands/auto_configure.py +4 -4
  14. claude_mpm/cli/commands/configure.py +1 -1
  15. claude_mpm/cli/commands/postmortem.py +1 -1
  16. claude_mpm/cli/commands/skills.py +193 -187
  17. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  18. claude_mpm/cli/parsers/agents_parser.py +0 -9
  19. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  20. claude_mpm/cli/startup.py +330 -78
  21. claude_mpm/commands/mpm-config.md +1 -2
  22. claude_mpm/commands/mpm-help.md +14 -95
  23. claude_mpm/commands/mpm-organize.md +350 -153
  24. claude_mpm/core/config.py +2 -4
  25. claude_mpm/core/framework/loaders/agent_loader.py +1 -1
  26. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  27. claude_mpm/core/unified_agent_registry.py +1 -1
  28. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  29. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  30. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  31. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  32. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  33. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  34. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  35. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  36. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  37. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  38. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  39. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  40. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  41. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  42. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  43. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  44. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  45. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  46. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  47. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  48. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  49. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  50. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  51. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  52. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  53. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  54. claude_mpm/hooks/claude_hooks/event_handlers.py +5 -0
  55. claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
  56. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  58. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  59. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
  63. claude_mpm/models/git_repository.py +3 -3
  64. claude_mpm/scripts/start_activity_logging.py +0 -0
  65. claude_mpm/services/agents/cache_git_manager.py +6 -6
  66. claude_mpm/services/agents/deployment/agent_deployment.py +7 -7
  67. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -2
  68. claude_mpm/services/agents/deployment/agent_template_builder.py +2 -2
  69. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  70. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +20 -22
  71. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +55 -53
  72. claude_mpm/services/agents/git_source_manager.py +2 -2
  73. claude_mpm/services/agents/recommender.py +5 -3
  74. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  75. claude_mpm/services/agents/sources/git_source_sync_service.py +5 -5
  76. claude_mpm/services/agents/startup_sync.py +22 -2
  77. claude_mpm/services/command_deployment_service.py +10 -0
  78. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  79. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  80. claude_mpm/services/git/git_operations_service.py +8 -8
  81. claude_mpm/services/monitor/server.py +473 -3
  82. claude_mpm/services/skills/selective_skill_deployer.py +475 -1
  83. claude_mpm/services/skills_deployer.py +62 -6
  84. claude_mpm/services/socketio/dashboard_server.py +1 -0
  85. claude_mpm/services/socketio/event_normalizer.py +37 -6
  86. claude_mpm/services/socketio/server/core.py +262 -123
  87. claude_mpm/utils/agent_dependency_loader.py +14 -2
  88. claude_mpm/utils/agent_filters.py +1 -1
  89. claude_mpm/utils/migration.py +4 -4
  90. claude_mpm/utils/robust_installer.py +47 -3
  91. {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/METADATA +5 -3
  92. {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/RECORD +96 -66
  93. claude_mpm/cli/commands/agents_detect.py +0 -380
  94. claude_mpm/cli/commands/agents_recommend.py +0 -309
  95. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  96. claude_mpm/commands/mpm-agents-detect.md +0 -177
  97. claude_mpm/commands/mpm-agents-list.md +0 -131
  98. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  99. {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/WHEEL +0 -0
  100. {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/entry_points.txt +0 -0
  101. {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/licenses/LICENSE +0 -0
  102. {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  103. {claude_mpm-5.4.14.dist-info → claude_mpm-5.4.36.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ This normalizer ensures all events follow a consistent schema before broadcastin
6
6
  providing backward compatibility while establishing a standard format.
7
7
 
8
8
  DESIGN DECISION: Transform all events to a consistent schema:
9
- - event: Socket.IO event name (always "claude_event")
9
+ - event: Socket.IO event name (always "mpm_event")
10
10
  - type: Main category (hook, system, session, file, connection)
11
11
  - subtype: Specific event type (pre_tool, heartbeat, started, etc.)
12
12
  - timestamp: ISO format timestamp
@@ -72,7 +72,7 @@ class NormalizedEvent:
72
72
  structure explicit and self-documenting.
73
73
  """
74
74
 
75
- event: str = "claude_event" # Socket.IO event name
75
+ event: str = "mpm_event" # Socket.IO event name
76
76
  source: str = "" # WHERE the event comes from
77
77
  type: str = "" # WHAT category of event
78
78
  subtype: str = "" # Specific event type
@@ -81,6 +81,8 @@ class NormalizedEvent:
81
81
  correlation_id: Optional[str] = (
82
82
  None # For correlating related events (e.g., pre_tool/post_tool)
83
83
  )
84
+ session_id: Optional[str] = None # Session identifier for stream grouping
85
+ cwd: Optional[str] = None # Working directory for project identification
84
86
 
85
87
  def to_dict(self) -> Dict[str, Any]:
86
88
  """Convert to dictionary for emission."""
@@ -95,6 +97,12 @@ class NormalizedEvent:
95
97
  # Include correlation_id if present
96
98
  if self.correlation_id:
97
99
  result["correlation_id"] = self.correlation_id
100
+ # Include session_id if present
101
+ if self.session_id:
102
+ result["session_id"] = self.session_id
103
+ # Include cwd if present
104
+ if self.cwd:
105
+ result["cwd"] = self.cwd
98
106
  return result
99
107
 
100
108
 
@@ -113,6 +121,7 @@ class EventNormalizer:
113
121
  "pre_response": (EventType.HOOK, "pre_response"),
114
122
  "post_response": (EventType.HOOK, "post_response"),
115
123
  "hook_event": (EventType.HOOK, "generic"),
124
+ "hook_execution": (EventType.HOOK, "execution"), # Hook execution metadata
116
125
  "UserPrompt": (EventType.HOOK, "user_prompt"), # Legacy format
117
126
  # Test events (legacy format)
118
127
  "TestStart": (EventType.TEST, "start"),
@@ -225,20 +234,32 @@ class EventNormalizer:
225
234
  # Get or generate timestamp
226
235
  timestamp = self._extract_timestamp(event_data)
227
236
 
228
- # Extract correlation_id if present
237
+ # Extract correlation_id, session_id, and cwd if present
229
238
  correlation_id = None
239
+ session_id = None
240
+ cwd = None
230
241
  if isinstance(event_data, dict):
231
242
  correlation_id = event_data.get("correlation_id")
243
+ # Try both naming conventions for session_id
244
+ session_id = event_data.get("session_id") or event_data.get("sessionId")
245
+ # Try multiple field names for working directory
246
+ cwd = (
247
+ event_data.get("cwd")
248
+ or event_data.get("working_directory")
249
+ or event_data.get("workingDirectory")
250
+ )
232
251
 
233
252
  # Create normalized event
234
253
  normalized = NormalizedEvent(
235
- event="claude_event",
254
+ event="mpm_event",
236
255
  source=event_source,
237
256
  type=event_type,
238
257
  subtype=subtype,
239
258
  timestamp=timestamp,
240
259
  data=data,
241
260
  correlation_id=correlation_id,
261
+ session_id=session_id,
262
+ cwd=cwd,
242
263
  )
243
264
 
244
265
  self.stats["normalized"] += 1
@@ -252,7 +273,7 @@ class EventNormalizer:
252
273
 
253
274
  # Return a generic event on error
254
275
  return NormalizedEvent(
255
- event="claude_event",
276
+ event="mpm_event",
256
277
  source="system",
257
278
  type="unknown",
258
279
  subtype="error",
@@ -285,8 +306,16 @@ class EventNormalizer:
285
306
  # If source is not a valid EventSource value, keep it as-is
286
307
  pass
287
308
 
309
+ # Extract session_id and cwd, trying multiple naming conventions
310
+ session_id = event_data.get("session_id") or event_data.get("sessionId")
311
+ cwd = (
312
+ event_data.get("cwd")
313
+ or event_data.get("working_directory")
314
+ or event_data.get("workingDirectory")
315
+ )
316
+
288
317
  return NormalizedEvent(
289
- event="claude_event", # Always use standard event name
318
+ event="mpm_event", # Always use standard event name
290
319
  source=source,
291
320
  type=event_data.get("type", "unknown"),
292
321
  subtype=event_data.get("subtype", "generic"),
@@ -295,6 +324,8 @@ class EventNormalizer:
295
324
  ),
296
325
  data=event_data.get("data", {}),
297
326
  correlation_id=event_data.get("correlation_id"),
327
+ session_id=session_id,
328
+ cwd=cwd,
298
329
  )
299
330
 
300
331
  def _extract_event_info(self, event_data: Any) -> Tuple[str, str, Dict[str, Any]]:
@@ -32,7 +32,6 @@ except ImportError:
32
32
  # Import VersionService for dynamic version retrieval
33
33
  import contextlib
34
34
 
35
- import claude_mpm
36
35
  from claude_mpm.services.version_service import VersionService
37
36
 
38
37
  from ....core.constants import SystemLimits, TimeoutConfig
@@ -277,9 +276,16 @@ class SocketIOServerCore:
277
276
  # Extract event data from payload (handles both direct and wrapped formats)
278
277
  # ConnectionManagerService sends: {"namespace": "...", "event": "...", "data": {...}}
279
278
  # Direct hook events may send data directly
280
- if "data" in payload and isinstance(payload.get("data"), dict):
279
+ # CRITICAL: Check if payload has the expected event structure (type, subtype, timestamp)
280
+ # If it does, use it directly. Only extract 'data' field if it's a wrapper object.
281
+ if "type" in payload and "subtype" in payload:
282
+ # Payload is already in normalized format, use it directly
283
+ event_data = payload
284
+ elif "data" in payload and isinstance(payload.get("data"), dict):
285
+ # Payload is a wrapper with 'data' field (from ConnectionManagerService)
281
286
  event_data = payload["data"]
282
287
  else:
288
+ # Fallback: use entire payload
283
289
  event_data = payload
284
290
 
285
291
  # Log receipt with more detail
@@ -409,9 +415,23 @@ class SocketIOServerCore:
409
415
  self.event_buffer.append(event_data)
410
416
  self.stats["events_buffered"] = len(self.event_buffer)
411
417
 
412
- # Add to main server's event history
413
- if hasattr(self.main_server, "event_history"):
418
+ # Add to main server's event history UNCONDITIONALLY
419
+ # WHY: event_history is always initialized in SocketIOServer.__init__
420
+ # This ensures events persist for new clients who connect later
421
+ if self.main_server and hasattr(
422
+ self.main_server, "event_history"
423
+ ):
414
424
  self.main_server.event_history.append(event_data)
425
+ self.logger.debug(
426
+ f"Added to history (total: {len(self.main_server.event_history)})"
427
+ )
428
+ else:
429
+ # CRITICAL: Log warning if event_history is not available
430
+ # This indicates a configuration or initialization problem
431
+ self.logger.warning(
432
+ "event_history not initialized on main_server! "
433
+ "Events will not persist for new clients."
434
+ )
415
435
 
416
436
  # Use the broadcaster's sio to emit (it's the same as self.sio)
417
437
  # This ensures the event goes through the proper channels
@@ -445,6 +465,21 @@ class SocketIOServerCore:
445
465
  self.event_buffer.append(event_data)
446
466
  self.stats["events_buffered"] = len(self.event_buffer)
447
467
 
468
+ # Add to main server's event history (fallback path)
469
+ # WHY: Ensure events persist even when broadcaster is unavailable
470
+ if self.main_server and hasattr(
471
+ self.main_server, "event_history"
472
+ ):
473
+ self.main_server.event_history.append(event_data)
474
+ self.logger.debug(
475
+ f"Added to history via fallback (total: {len(self.main_server.event_history)})"
476
+ )
477
+ else:
478
+ self.logger.warning(
479
+ "event_history not initialized on main_server (fallback path)! "
480
+ "Events will not persist for new clients."
481
+ )
482
+
448
483
  # Return 204 No Content for success
449
484
  self.logger.debug(f"✅ HTTP event processed successfully: {event_type}")
450
485
  return web.Response(status=204)
@@ -504,7 +539,7 @@ class SocketIOServerCore:
504
539
  from pathlib import Path
505
540
 
506
541
  try:
507
- working_dir = Path.cwd()
542
+ working_dir = str(Path.cwd())
508
543
  home_dir = str(Path.home())
509
544
 
510
545
  return web.json_response(
@@ -594,6 +629,169 @@ class SocketIOServerCore:
594
629
  self.app.router.add_get("/api/file/read", file_read_handler)
595
630
  self.logger.info("✅ File reading API registered at /api/file/read")
596
631
 
632
+ # Add files listing endpoint for file browser
633
+ async def files_list_handler(request):
634
+ """Handle GET /api/files for listing files in working directory."""
635
+
636
+ try:
637
+ # Get working directory from query params or use current directory
638
+ working_dir = request.query.get("path", str(Path.cwd()))
639
+ abs_working_dir = Path(working_dir).resolve().expanduser()
640
+
641
+ # Security check - ensure directory is accessible
642
+ if not abs_working_dir.exists():
643
+ return web.json_response(
644
+ {"error": "Directory not found"}, status=404
645
+ )
646
+
647
+ if not abs_working_dir.is_dir():
648
+ return web.json_response(
649
+ {"error": "Path is not a directory"}, status=400
650
+ )
651
+
652
+ # Collect files and directories
653
+ files = []
654
+ directories = []
655
+
656
+ # Common patterns to exclude
657
+ exclude_patterns = {
658
+ ".git",
659
+ ".venv",
660
+ "venv",
661
+ "node_modules",
662
+ "__pycache__",
663
+ ".pytest_cache",
664
+ ".mypy_cache",
665
+ "dist",
666
+ "build",
667
+ ".next",
668
+ "coverage",
669
+ ".coverage",
670
+ ".tox",
671
+ ".eggs",
672
+ "*.egg-info",
673
+ }
674
+
675
+ for entry in abs_working_dir.iterdir():
676
+ # Skip hidden files and excluded patterns
677
+ if entry.name.startswith("."):
678
+ # Allow .py, .ts, .md, etc. files but skip directories like .git
679
+ if entry.is_dir():
680
+ continue
681
+
682
+ # Skip excluded directories
683
+ if entry.name in exclude_patterns:
684
+ continue
685
+
686
+ try:
687
+ stat_info = entry.stat()
688
+ entry_data = {
689
+ "name": entry.name,
690
+ "path": str(entry),
691
+ "type": "directory" if entry.is_dir() else "file",
692
+ "size": stat_info.st_size if entry.is_file() else 0,
693
+ "modified": stat_info.st_mtime,
694
+ }
695
+
696
+ if entry.is_dir():
697
+ directories.append(entry_data)
698
+ else:
699
+ # Add file extension for syntax highlighting
700
+ entry_data["extension"] = entry.suffix.lower()
701
+ files.append(entry_data)
702
+ except (PermissionError, OSError):
703
+ # Skip files we can't access
704
+ continue
705
+
706
+ # Sort directories and files alphabetically
707
+ directories.sort(key=lambda x: x["name"].lower())
708
+ files.sort(key=lambda x: x["name"].lower())
709
+
710
+ return web.json_response(
711
+ {
712
+ "success": True,
713
+ "path": str(abs_working_dir),
714
+ "directories": directories,
715
+ "files": files,
716
+ "total_files": len(files),
717
+ "total_directories": len(directories),
718
+ }
719
+ )
720
+
721
+ except Exception as e:
722
+ self.logger.error(f"Error listing files: {e}")
723
+ return web.json_response({"error": str(e)}, status=500)
724
+
725
+ self.app.router.add_get("/api/files", files_list_handler)
726
+ self.logger.info("✅ Files listing API registered at /api/files")
727
+
728
+ # Add git history endpoint
729
+ async def git_history_handler(request):
730
+ """Handle POST /api/git-history for getting file git history."""
731
+ import subprocess
732
+
733
+ try:
734
+ # Parse JSON body
735
+ data = await request.json()
736
+ file_path = data.get("path", "")
737
+ limit = data.get("limit", 10)
738
+
739
+ if not file_path:
740
+ return web.json_response(
741
+ {"success": False, "error": "No path provided", "commits": []},
742
+ status=400,
743
+ )
744
+
745
+ abs_path = Path(Path(file_path).resolve().expanduser())
746
+
747
+ if not Path(abs_path).exists():
748
+ return web.json_response(
749
+ {"success": False, "error": "File not found", "commits": []},
750
+ status=404,
751
+ )
752
+
753
+ # Get git log for file
754
+ result = subprocess.run(
755
+ [
756
+ "git",
757
+ "log",
758
+ f"-{limit}",
759
+ "--pretty=format:%H|%an|%ar|%s",
760
+ "--",
761
+ abs_path,
762
+ ],
763
+ check=False,
764
+ capture_output=True,
765
+ text=True,
766
+ cwd=Path(abs_path).parent,
767
+ )
768
+
769
+ commits = []
770
+ if result.returncode == 0 and result.stdout:
771
+ for line in result.stdout.strip().split("\n"):
772
+ if line:
773
+ parts = line.split("|", 3)
774
+ if len(parts) == 4:
775
+ commits.append(
776
+ {
777
+ "hash": parts[0][:7], # Short hash
778
+ "author": parts[1],
779
+ "date": parts[2],
780
+ "message": parts[3],
781
+ }
782
+ )
783
+
784
+ return web.json_response({"success": True, "commits": commits})
785
+
786
+ except Exception as e:
787
+ self.logger.error(f"Error getting git history: {e}")
788
+ return web.json_response(
789
+ {"success": False, "error": str(e), "commits": []}, status=500
790
+ )
791
+
792
+ self.app.router.add_post("/api/git-history", git_history_handler)
793
+ self.logger.info("✅ Git history API registered at /api/git-history")
794
+
597
795
  def _setup_directory_api(self):
598
796
  """Setup simple directory listing API.
599
797
 
@@ -611,7 +809,7 @@ class SocketIOServerCore:
611
809
  self.logger.error(f"Failed to setup directory API: {e}")
612
810
 
613
811
  def _setup_static_files(self):
614
- """Setup static file serving for the dashboard."""
812
+ """Setup static file serving for the Svelte dashboard."""
615
813
  try:
616
814
  # Add debug logging for deployment context
617
815
  try:
@@ -624,65 +822,43 @@ class SocketIOServerCore:
624
822
  except Exception as e:
625
823
  self.logger.debug(f"Could not detect deployment context: {e}")
626
824
 
627
- self.dashboard_path = self._find_static_path()
825
+ # Find Svelte build directory
826
+ svelte_build_path = self._find_static_path()
628
827
 
629
- if self.dashboard_path and self.dashboard_path.exists():
630
- self.logger.info(f"✅ Dashboard found at: {self.dashboard_path}")
828
+ if svelte_build_path and svelte_build_path.exists():
829
+ self.logger.info(f"✅ Svelte dashboard found at: {svelte_build_path}")
830
+ self.dashboard_path = svelte_build_path
631
831
 
632
- # Serve index.html at root
832
+ # Serve Svelte index.html at root
633
833
  async def index_handler(request):
634
- index_file = self.dashboard_path / "index.html"
834
+ index_file = svelte_build_path / "index.html"
635
835
  if index_file.exists():
636
- self.logger.debug(f"Serving dashboard index from: {index_file}")
836
+ self.logger.debug(
837
+ f"Serving Svelte dashboard from: {index_file}"
838
+ )
637
839
  return web.FileResponse(index_file)
638
- self.logger.warning(
639
- f"Dashboard index.html not found at: {index_file}"
640
- )
840
+ self.logger.warning(f"Svelte index.html not found at: {index_file}")
641
841
  return web.Response(text="Dashboard not available", status=404)
642
842
 
643
843
  self.app.router.add_get("/", index_handler)
644
844
 
645
- # Serve the actual dashboard template at /dashboard
646
- async def dashboard_handler(request):
647
- dashboard_template = (
648
- self.dashboard_path.parent / "templates" / "index.html"
649
- )
650
- if dashboard_template.exists():
651
- self.logger.debug(
652
- f"Serving dashboard template from: {dashboard_template}"
653
- )
654
- return web.FileResponse(dashboard_template)
655
- # Fallback to the main index if template doesn't exist
656
- self.logger.warning(
657
- f"Dashboard template not found at: {dashboard_template}, falling back to index"
845
+ # Serve Svelte app assets at /_app/ (needed for SvelteKit builds)
846
+ svelte_app_path = svelte_build_path / "_app"
847
+ if svelte_app_path.exists():
848
+ self.app.router.add_static(
849
+ "/_app/", svelte_app_path, name="svelte_app"
658
850
  )
659
- return await index_handler(request)
660
-
661
- self.app.router.add_get("/dashboard", dashboard_handler)
662
-
663
- # Serve simple code view template at /code-simple
664
- async def code_simple_handler(request):
665
- code_simple_template = (
666
- self.dashboard_path.parent / "templates" / "code_simple.html"
851
+ self.logger.info(
852
+ f"✅ Svelte dashboard available at http://{self.host}:{self.port}/ (build: {svelte_build_path})"
667
853
  )
668
- if code_simple_template.exists():
669
- self.logger.debug(
670
- f"Serving code simple template from: {code_simple_template}"
671
- )
672
- return web.FileResponse(code_simple_template)
673
- # Return error if template doesn't exist
854
+ else:
674
855
  self.logger.warning(
675
- f"Code simple template not found at: {code_simple_template}"
676
- )
677
- return web.Response(
678
- text="Simple code view not available", status=404
856
+ f"⚠️ Svelte _app directory not found at: {svelte_app_path}"
679
857
  )
680
858
 
681
- self.app.router.add_get("/code-simple", code_simple_handler)
682
-
683
- # Serve version.json from dashboard directory
859
+ # Serve version.json from Svelte build directory
684
860
  async def version_handler(request):
685
- version_file = self.dashboard_path / "version.json"
861
+ version_file = svelte_build_path / "version.json"
686
862
  if version_file.exists():
687
863
  self.logger.debug(f"Serving version.json from: {version_file}")
688
864
  return web.FileResponse(version_file)
@@ -698,55 +874,10 @@ class SocketIOServerCore:
698
874
 
699
875
  self.app.router.add_get("/version.json", version_handler)
700
876
 
701
- # Serve static assets (CSS, JS) from the dashboard static directory
702
- # Use package-relative path (works for both dev and installed package)
703
- package_root = Path(claude_mpm.__file__).parent
704
- dashboard_static_path = package_root / "dashboard" / "static"
705
- if dashboard_static_path.exists():
706
- self.app.router.add_static(
707
- "/static/", dashboard_static_path, name="dashboard_static"
708
- )
709
- self.logger.info(
710
- f"✅ Static assets available at: {dashboard_static_path}"
711
- )
712
- else:
713
- self.logger.warning(
714
- f"⚠️ Static assets directory not found at: {dashboard_static_path}"
715
- )
716
-
717
- # Serve Svelte dashboard build
718
- svelte_build_path = (
719
- package_root / "dashboard" / "static" / "svelte-build"
720
- )
721
- if svelte_build_path.exists():
722
- # Serve Svelte dashboard at /svelte route
723
- async def svelte_handler(request):
724
- svelte_index = svelte_build_path / "index.html"
725
- if svelte_index.exists():
726
- self.logger.debug(
727
- f"Serving Svelte dashboard from: {svelte_index}"
728
- )
729
- return web.FileResponse(svelte_index)
730
- return web.Response(
731
- text="Svelte dashboard not available", status=404
732
- )
733
-
734
- self.app.router.add_get("/svelte", svelte_handler)
735
-
736
- # Serve Svelte app assets at /_app/ (needed for SvelteKit builds)
737
- svelte_app_path = svelte_build_path / "_app"
738
- if svelte_app_path.exists():
739
- self.app.router.add_static(
740
- "/_app/", svelte_app_path, name="svelte_app"
741
- )
742
- self.logger.info(
743
- f"✅ Svelte dashboard available at /svelte (build: {svelte_build_path})"
744
- )
745
- else:
746
- self.logger.debug(f"Svelte build not found at: {svelte_build_path}")
747
-
748
877
  else:
749
- self.logger.warning("⚠️ No dashboard found, serving fallback response")
878
+ self.logger.warning(
879
+ "⚠️ Svelte dashboard not found, serving fallback response"
880
+ )
750
881
 
751
882
  # Fallback handler
752
883
  async def fallback_handler(request):
@@ -773,10 +904,10 @@ class SocketIOServerCore:
773
904
  self.app.router.add_get("/", error_handler)
774
905
 
775
906
  def _find_static_path(self):
776
- """Find the static files directory using multiple approaches.
907
+ """Find the Svelte build directory using multiple approaches.
777
908
 
778
- WHY: The static files location varies depending on how the application
779
- is installed and run. We try multiple common locations to find them.
909
+ WHY: The dashboard is now pure Svelte, located at dashboard/static/svelte-build/.
910
+ We search for this specific structure across different deployment contexts.
780
911
  """
781
912
  # Get deployment-context-aware paths
782
913
  try:
@@ -797,43 +928,45 @@ class SocketIOServerCore:
797
928
  package_root = None
798
929
  project_root = get_project_root()
799
930
 
800
- # Try multiple possible locations for static files and dashboard
931
+ # Try multiple possible locations for Svelte build directory
801
932
  possible_paths = [
802
933
  # Package-based paths (for pipx and pip installations)
803
- package_root / "dashboard" / "templates" if package_root else None,
804
- package_root / "services" / "socketio" / "static" if package_root else None,
805
- package_root / "static" if package_root else None,
934
+ package_root / "dashboard" / "static" / "svelte-build"
935
+ if package_root
936
+ else None,
806
937
  # Project-based paths (for development)
807
- project_root / "src" / "claude_mpm" / "dashboard" / "templates",
808
- project_root / "dashboard" / "templates",
809
- project_root / "src" / "claude_mpm" / "services" / "static",
810
- project_root / "src" / "claude_mpm" / "services" / "socketio" / "static",
811
- project_root / "static",
812
- project_root / "src" / "static",
938
+ project_root
939
+ / "src"
940
+ / "claude_mpm"
941
+ / "dashboard"
942
+ / "static"
943
+ / "svelte-build",
944
+ project_root / "dashboard" / "static" / "svelte-build",
813
945
  # Package installation locations (fallback)
814
- Path(__file__).parent.parent / "static",
815
- Path(__file__).parent / "static",
946
+ Path(__file__).parent.parent.parent
947
+ / "dashboard"
948
+ / "static"
949
+ / "svelte-build",
816
950
  # Scripts directory (for standalone installations)
817
- get_scripts_dir() / "static",
818
- get_scripts_dir() / "socketio" / "static",
951
+ get_scripts_dir() / "dashboard" / "static" / "svelte-build",
819
952
  # Current working directory
820
- Path.cwd() / "static",
821
- Path.cwd() / "socketio" / "static",
953
+ Path.cwd() / "src" / "claude_mpm" / "dashboard" / "static" / "svelte-build",
954
+ Path.cwd() / "dashboard" / "static" / "svelte-build",
822
955
  ]
823
956
 
824
957
  # Filter out None values
825
958
  possible_paths = [p for p in possible_paths if p is not None]
826
959
  self.logger.debug(
827
- f"Searching {len(possible_paths)} possible static file locations"
960
+ f"Searching {len(possible_paths)} possible Svelte build locations"
828
961
  )
829
962
 
830
963
  for path in possible_paths:
831
- self.logger.debug(f"Checking for static files at: {path}")
964
+ self.logger.debug(f"Checking for Svelte build at: {path}")
832
965
  try:
833
966
  if path.exists() and path.is_dir():
834
- # Check if it contains expected files
967
+ # Check if it contains expected Svelte build files
835
968
  if (path / "index.html").exists():
836
- self.logger.info(f"✅ Found static files at: {path}")
969
+ self.logger.info(f"✅ Found Svelte build at: {path}")
837
970
  return path
838
971
  self.logger.debug(f"Directory exists but no index.html: {path}")
839
972
  else:
@@ -842,7 +975,7 @@ class SocketIOServerCore:
842
975
  self.logger.debug(f"Error checking path {path}: {e}")
843
976
 
844
977
  self.logger.warning(
845
- "⚠️ Static files not found - dashboard will not be available"
978
+ "⚠️ Svelte build not found - dashboard will not be available"
846
979
  )
847
980
  self.logger.debug(f"Searched paths: {[str(p) for p in possible_paths]}")
848
981
  return None
@@ -918,9 +1051,15 @@ class SocketIOServerCore:
918
1051
  },
919
1052
  }
920
1053
 
921
- # Add to event history if main server is available
1054
+ # Add to event history UNCONDITIONALLY
1055
+ # WHY: Heartbeat events should persist for new clients too
922
1056
  if self.main_server and hasattr(self.main_server, "event_history"):
923
1057
  self.main_server.event_history.append(heartbeat_data)
1058
+ self.logger.debug(
1059
+ f"Heartbeat added to history (total: {len(self.main_server.event_history)})"
1060
+ )
1061
+ else:
1062
+ self.logger.warning("event_history not initialized for heartbeat!")
924
1063
 
925
1064
  # Emit heartbeat to all connected clients (already using new schema)
926
1065
  await self.sio.emit("system_event", heartbeat_data)
@@ -658,12 +658,24 @@ class AgentDependencyLoader:
658
658
  "Robust installer not available, falling back to simple installation"
659
659
  )
660
660
  try:
661
- cmd = [sys.executable, "-m", "pip", "install"]
662
-
663
661
  # Check environment and add appropriate flags
664
662
  import os
665
663
  import sysconfig
666
664
 
665
+ # Check if in UV tool environment (no pip available)
666
+ uv_tool_dir = os.environ.get("UV_TOOL_DIR", "")
667
+ is_uv_tool = (
668
+ (uv_tool_dir and "claude-mpm" in uv_tool_dir)
669
+ or ".local/share/uv/tools/" in sys.executable
670
+ or "/uv/tools/" in sys.executable
671
+ )
672
+
673
+ if is_uv_tool:
674
+ cmd = ["uv", "pip", "install"]
675
+ logger.debug("Using 'uv pip install' for UV tool environment")
676
+ else:
677
+ cmd = [sys.executable, "-m", "pip", "install"]
678
+
667
679
  # Check if in virtualenv
668
680
  in_virtualenv = (
669
681
  (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)
@@ -5,7 +5,7 @@ WHY: This module provides centralized filtering logic to remove non-deployable
5
5
  agents (BASE_AGENT) and already-deployed agents from user-facing displays.
6
6
 
7
7
  ARCHITECTURE:
8
- - SOURCE: ~/.claude-mpm/cache/remote-agents/ (git repository cache)
8
+ - SOURCE: ~/.claude-mpm/cache/agents/ (git repository cache)
9
9
  - DEPLOYMENT: .claude/agents/ (project-level deployment location)
10
10
 
11
11
  DESIGN DECISIONS: