claude-mpm 5.4.21__py3-none-any.whl → 5.4.59__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 (176) 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/MEMORY.md +1 -1
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +771 -1019
  6. claude_mpm/agents/WORKFLOW.md +5 -254
  7. claude_mpm/agents/agent_loader.py +1 -1
  8. claude_mpm/agents/base_agent.json +31 -0
  9. claude_mpm/agents/frontmatter_validator.py +2 -2
  10. claude_mpm/cli/commands/agent_state_manager.py +10 -10
  11. claude_mpm/cli/commands/agents.py +9 -9
  12. claude_mpm/cli/commands/auto_configure.py +4 -4
  13. claude_mpm/cli/commands/configure.py +1 -1
  14. claude_mpm/cli/commands/configure_agent_display.py +12 -0
  15. claude_mpm/cli/commands/mpm_init/core.py +72 -0
  16. claude_mpm/cli/commands/postmortem.py +1 -1
  17. claude_mpm/cli/commands/profile.py +276 -0
  18. claude_mpm/cli/commands/skills.py +14 -18
  19. claude_mpm/cli/executor.py +10 -0
  20. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  21. claude_mpm/cli/parsers/base_parser.py +7 -0
  22. claude_mpm/cli/parsers/profile_parser.py +147 -0
  23. claude_mpm/cli/parsers/skills_parser.py +0 -6
  24. claude_mpm/cli/startup.py +506 -180
  25. claude_mpm/commands/mpm-config.md +13 -250
  26. claude_mpm/commands/mpm-doctor.md +9 -22
  27. claude_mpm/commands/mpm-help.md +5 -206
  28. claude_mpm/commands/mpm-init.md +81 -507
  29. claude_mpm/commands/mpm-monitor.md +15 -402
  30. claude_mpm/commands/mpm-organize.md +61 -441
  31. claude_mpm/commands/mpm-postmortem.md +6 -108
  32. claude_mpm/commands/mpm-session-resume.md +12 -363
  33. claude_mpm/commands/mpm-status.md +5 -69
  34. claude_mpm/commands/mpm-ticket-view.md +52 -495
  35. claude_mpm/commands/mpm-version.md +5 -107
  36. claude_mpm/core/config.py +2 -4
  37. claude_mpm/core/framework/loaders/agent_loader.py +1 -1
  38. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  39. claude_mpm/core/optimized_startup.py +61 -0
  40. claude_mpm/core/shared/config_loader.py +3 -1
  41. claude_mpm/core/unified_agent_registry.py +1 -1
  42. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +1 -0
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +1 -0
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BSNlmTZj.js +1 -0
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
  58. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
  77. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
  78. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
  79. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
  80. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
  81. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
  82. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
  83. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
  84. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
  85. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
  86. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
  87. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
  88. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
  89. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
  90. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
  91. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
  92. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/NqQ1dWOy.js +1 -0
  93. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
  94. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Vzk33B_K.js +2 -0
  95. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
  96. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
  97. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
  98. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
  99. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
  100. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
  101. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
  102. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
  103. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
  104. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.m1gL8KXf.js +1 -0
  105. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
  106. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -0
  107. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  108. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  109. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  110. claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
  111. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
  112. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
  113. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
  114. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
  115. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  116. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  117. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  118. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  119. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  120. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  121. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  122. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  123. claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
  124. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  125. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  126. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  127. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  128. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  129. claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
  130. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  131. claude_mpm/init.py +276 -0
  132. claude_mpm/models/git_repository.py +3 -3
  133. claude_mpm/scripts/start_activity_logging.py +0 -0
  134. claude_mpm/services/agents/agent_builder.py +3 -3
  135. claude_mpm/services/agents/cache_git_manager.py +6 -6
  136. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  137. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -2
  138. claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
  139. claude_mpm/services/agents/deployment/agent_template_builder.py +31 -19
  140. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  141. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  142. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  143. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +169 -26
  144. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +98 -75
  145. claude_mpm/services/agents/git_source_manager.py +23 -4
  146. claude_mpm/services/agents/recommender.py +5 -3
  147. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  148. claude_mpm/services/agents/sources/git_source_sync_service.py +121 -10
  149. claude_mpm/services/agents/startup_sync.py +22 -2
  150. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  151. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  152. claude_mpm/services/git/git_operations_service.py +8 -8
  153. claude_mpm/services/monitor/management/lifecycle.py +7 -1
  154. claude_mpm/services/monitor/server.py +473 -3
  155. claude_mpm/services/pm_skills_deployer.py +711 -0
  156. claude_mpm/services/profile_manager.py +337 -0
  157. claude_mpm/services/skills/git_skill_source_manager.py +148 -11
  158. claude_mpm/services/skills/selective_skill_deployer.py +97 -48
  159. claude_mpm/services/skills_deployer.py +161 -65
  160. claude_mpm/services/socketio/dashboard_server.py +1 -0
  161. claude_mpm/services/socketio/event_normalizer.py +37 -6
  162. claude_mpm/services/socketio/server/core.py +262 -123
  163. claude_mpm/skills/bundled/security-scanning.md +112 -0
  164. claude_mpm/skills/skill_manager.py +98 -3
  165. claude_mpm/templates/.pre-commit-config.yaml +112 -0
  166. claude_mpm/utils/agent_dependency_loader.py +14 -2
  167. claude_mpm/utils/agent_filters.py +1 -1
  168. claude_mpm/utils/migration.py +4 -4
  169. claude_mpm/utils/robust_installer.py +47 -3
  170. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/METADATA +7 -4
  171. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/RECORD +175 -81
  172. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/WHEEL +0 -0
  173. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/entry_points.txt +0 -0
  174. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE +0 -0
  175. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  176. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/top_level.txt +0 -0
@@ -110,6 +110,26 @@ def sync_agents_on_startup(config: Optional[Dict[str, Any]] = None) -> Dict[str,
110
110
  else:
111
111
  cache_dir = None # Will use default
112
112
 
113
+ # Check for old cache directory names and provide migration guidance
114
+ # This handles users upgrading from older versions
115
+ old_cache_paths = [
116
+ Path.home() / ".claude-mpm" / "cache" / "remote-agents",
117
+ ]
118
+ new_cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
119
+
120
+ for old_cache in old_cache_paths:
121
+ if old_cache.exists() and not new_cache_dir.exists():
122
+ logger.warning(f"Found old cache directory: {old_cache}")
123
+ logger.warning(
124
+ "The cache directory location has changed to: ~/.claude-mpm/cache/agents"
125
+ )
126
+ logger.warning("To migrate your existing cache, run:")
127
+ logger.warning(f" mv {old_cache} {new_cache_dir}")
128
+ logger.info(
129
+ "Agents will be re-synced to the new cache location automatically."
130
+ )
131
+ break # Only show warning once
132
+
113
133
  # Sync each enabled source
114
134
  for source_config in sources:
115
135
  try:
@@ -217,7 +237,7 @@ def get_sync_status() -> Dict[str, Any]:
217
237
  "enabled": agent_sync_config.get("enabled", True),
218
238
  "sources_configured": len(enabled_sources),
219
239
  "cache_dir": agent_sync_config.get(
220
- "cache_dir", "~/.claude-mpm/cache/remote-agents"
240
+ "cache_dir", "~/.claude-mpm/cache/agents"
221
241
  ),
222
242
  }
223
243
 
@@ -233,7 +253,7 @@ def get_sync_status() -> Dict[str, Any]:
233
253
  return {
234
254
  "enabled": False,
235
255
  "sources_configured": 0,
236
- "cache_dir": "~/.claude-mpm/cache/remote-agents",
256
+ "cache_dir": "~/.claude-mpm/cache/agents",
237
257
  "last_sync": None,
238
258
  "error": str(e),
239
259
  }
@@ -66,9 +66,9 @@ class AgentCheck(BaseDiagnosticCheck):
66
66
 
67
67
  if deployed_count == 0:
68
68
  status = ValidationSeverity.ERROR
69
- message = f"No agents deployed (0/{available_count} available)"
69
+ message = f"No agents deployed (0/{available_count} cached)"
70
70
  fix_command = "claude-mpm agents deploy"
71
- fix_description = "Deploy all available agents"
71
+ fix_description = "Deploy all cached agents"
72
72
  elif deployed_count < available_count:
73
73
  status = ValidationSeverity.WARNING
74
74
  message = f"{deployed_count}/{available_count} agents deployed"
@@ -432,7 +432,7 @@ class AgentSourcesCheck(BaseDiagnosticCheck):
432
432
 
433
433
  def _check_cache_directory(self) -> DiagnosticResult:
434
434
  """Check cache directory health."""
435
- cache_dir = Path.home() / ".claude-mpm" / "cache" / "remote-agents"
435
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
436
436
 
437
437
  if not cache_dir.exists():
438
438
  return DiagnosticResult(
@@ -12,10 +12,10 @@ Design Decisions:
12
12
 
13
13
  Example:
14
14
  >>> service = GitOperationsService()
15
- >>> success = service.create_branch(Path("~/.claude-mpm/cache/remote-agents"), "improve/research-memory")
15
+ >>> success = service.create_branch(Path("~/.claude-mpm/cache/agents"), "improve/research-memory")
16
16
  >>> if success:
17
- ... service.stage_files(Path("~/.claude-mpm/cache/remote-agents"), ["agents/research.md"])
18
- ... service.commit(Path("~/.claude-mpm/cache/remote-agents"), "feat: improve research agent memory handling")
17
+ ... service.stage_files(Path("~/.claude-mpm/cache/agents"), ["agents/research.md"])
18
+ ... service.commit(Path("~/.claude-mpm/cache/agents"), "feat: improve research agent memory handling")
19
19
  """
20
20
 
21
21
  import logging
@@ -68,7 +68,7 @@ class GitOperationsService:
68
68
 
69
69
  Example:
70
70
  >>> service = GitOperationsService()
71
- >>> service.is_git_repo(Path("~/.claude-mpm/cache/remote-agents"))
71
+ >>> service.is_git_repo(Path("~/.claude-mpm/cache/agents"))
72
72
  True
73
73
  """
74
74
  try:
@@ -150,7 +150,7 @@ class GitOperationsService:
150
150
  Example:
151
151
  >>> service = GitOperationsService()
152
152
  >>> service.create_and_checkout_branch(
153
- ... Path("~/.claude-mpm/cache/remote-agents"),
153
+ ... Path("~/.claude-mpm/cache/agents"),
154
154
  ... "improve/research-memory",
155
155
  ... "main"
156
156
  ... )
@@ -245,7 +245,7 @@ class GitOperationsService:
245
245
  Example:
246
246
  >>> service = GitOperationsService()
247
247
  >>> service.commit(
248
- ... Path("~/.claude-mpm/cache/remote-agents"),
248
+ ... Path("~/.claude-mpm/cache/agents"),
249
249
  ... "feat(agent): improve research agent memory handling\\n\\n- Add hard limit of 5 files"
250
250
  ... )
251
251
  True
@@ -289,7 +289,7 @@ class GitOperationsService:
289
289
 
290
290
  Example:
291
291
  >>> service = GitOperationsService()
292
- >>> service.push(Path("~/.claude-mpm/cache/remote-agents"), "improve/research-memory")
292
+ >>> service.push(Path("~/.claude-mpm/cache/agents"), "improve/research-memory")
293
293
  True
294
294
  """
295
295
  self._validate_repo(repo_path)
@@ -489,7 +489,7 @@ class GitOperationsService:
489
489
 
490
490
  Example:
491
491
  >>> service = GitOperationsService()
492
- >>> valid, msg = service.validate_repo(Path("~/.claude-mpm/cache/remote-agents"))
492
+ >>> valid, msg = service.validate_repo(Path("~/.claude-mpm/cache/agents"))
493
493
  >>> if not valid:
494
494
  ... print(f"Repository invalid: {msg}")
495
495
  """
@@ -482,7 +482,13 @@ class DaemonLifecycle:
482
482
  # Configure logger to write to file immediately
483
483
  import logging
484
484
 
485
- file_handler = logging.FileHandler(self.log_file)
485
+ # Use RotatingFileHandler for automatic log rotation
486
+ # 5MB max size, 5 backup files (consistent with project logging standards)
487
+ file_handler = logging.handlers.RotatingFileHandler(
488
+ self.log_file,
489
+ maxBytes=5 * 1024 * 1024, # 5MB
490
+ backupCount=5,
491
+ )
486
492
  file_handler.setLevel(logging.DEBUG)
487
493
  formatter = logging.Formatter(
488
494
  "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
@@ -29,7 +29,6 @@ from watchdog.observers import Observer
29
29
 
30
30
  from ...core.enums import ServiceState
31
31
  from ...core.logging_config import get_logger
32
- from ...dashboard.api.simple_directory import list_directory
33
32
  from .event_emitter import get_event_emitter
34
33
  from .handlers.code_analysis import CodeAnalysisHandler
35
34
  from .handlers.dashboard import DashboardHandler
@@ -588,6 +587,243 @@ class UnifiedMonitorServer:
588
587
  {"success": False, "error": str(e)}, status=500
589
588
  )
590
589
 
590
+ # File listing endpoint for file browser
591
+ async def api_files_handler(request):
592
+ """List files in a directory for the file browser."""
593
+ try:
594
+ # Get path from query param, default to working directory
595
+ path = request.query.get("path", str(Path.cwd()))
596
+ dir_path = Path(path)
597
+
598
+ if not dir_path.exists():
599
+ return web.json_response(
600
+ {"success": False, "error": "Directory not found"},
601
+ status=404,
602
+ )
603
+
604
+ if not dir_path.is_dir():
605
+ return web.json_response(
606
+ {"success": False, "error": "Path is not a directory"},
607
+ status=400,
608
+ )
609
+
610
+ # Patterns to exclude
611
+ exclude_patterns = {
612
+ ".git",
613
+ "node_modules",
614
+ "__pycache__",
615
+ ".svelte-kit",
616
+ "venv",
617
+ ".venv",
618
+ "dist",
619
+ "build",
620
+ ".next",
621
+ ".cache",
622
+ ".pytest_cache",
623
+ ".mypy_cache",
624
+ ".ruff_cache",
625
+ "eggs",
626
+ "*.egg-info",
627
+ ".tox",
628
+ ".nox",
629
+ "htmlcov",
630
+ ".coverage",
631
+ }
632
+
633
+ entries = []
634
+ try:
635
+ for entry in sorted(
636
+ dir_path.iterdir(),
637
+ key=lambda x: (not x.is_dir(), x.name.lower()),
638
+ ):
639
+ # Skip hidden files and excluded patterns
640
+ if entry.name.startswith(".") and entry.name not in {
641
+ ".env",
642
+ ".gitignore",
643
+ }:
644
+ if entry.name in {".git", ".svelte-kit", ".cache"}:
645
+ continue
646
+ if entry.name in exclude_patterns:
647
+ continue
648
+ if any(
649
+ entry.name.endswith(p.replace("*", ""))
650
+ for p in exclude_patterns
651
+ if "*" in p
652
+ ):
653
+ continue
654
+
655
+ try:
656
+ stat = entry.stat()
657
+ entries.append(
658
+ {
659
+ "name": entry.name,
660
+ "path": str(entry),
661
+ "type": "directory"
662
+ if entry.is_dir()
663
+ else "file",
664
+ "size": stat.st_size if entry.is_file() else 0,
665
+ "modified": stat.st_mtime,
666
+ "extension": entry.suffix.lstrip(".")
667
+ if entry.is_file()
668
+ else None,
669
+ }
670
+ )
671
+ except (PermissionError, OSError):
672
+ continue
673
+
674
+ except PermissionError:
675
+ return web.json_response(
676
+ {"success": False, "error": "Permission denied"},
677
+ status=403,
678
+ )
679
+
680
+ # Separate directories and files
681
+ directories = [e for e in entries if e["type"] == "directory"]
682
+ files = [e for e in entries if e["type"] == "file"]
683
+
684
+ return web.json_response(
685
+ {
686
+ "success": True,
687
+ "path": str(dir_path),
688
+ "directories": directories,
689
+ "files": files,
690
+ "total_directories": len(directories),
691
+ "total_files": len(files),
692
+ }
693
+ )
694
+
695
+ except Exception as e:
696
+ self.logger.error(f"Error listing directory: {e}")
697
+ return web.json_response(
698
+ {"success": False, "error": str(e)}, status=500
699
+ )
700
+
701
+ # File read endpoint (GET) for file browser
702
+ async def api_file_read_handler(request):
703
+ """Read file content via GET request."""
704
+ import base64
705
+
706
+ try:
707
+ file_path = request.query.get("path", "")
708
+
709
+ if not file_path:
710
+ return web.json_response(
711
+ {"success": False, "error": "Path parameter required"},
712
+ status=400,
713
+ )
714
+
715
+ path = Path(file_path)
716
+
717
+ if not path.exists():
718
+ return web.json_response(
719
+ {"success": False, "error": "File not found"},
720
+ status=404,
721
+ )
722
+
723
+ if not path.is_file():
724
+ return web.json_response(
725
+ {"success": False, "error": "Path is not a file"},
726
+ status=400,
727
+ )
728
+
729
+ # Get file info
730
+ file_size = path.stat().st_size
731
+ file_ext = path.suffix.lstrip(".").lower()
732
+
733
+ # Define image extensions
734
+ image_extensions = {
735
+ "png",
736
+ "jpg",
737
+ "jpeg",
738
+ "gif",
739
+ "svg",
740
+ "webp",
741
+ "ico",
742
+ "bmp",
743
+ }
744
+
745
+ # Check if file is an image
746
+ if file_ext in image_extensions:
747
+ # Read as binary and encode to base64
748
+ try:
749
+ binary_content = path.read_bytes()
750
+ base64_content = base64.b64encode(binary_content).decode(
751
+ "utf-8"
752
+ )
753
+
754
+ # Map extension to MIME type
755
+ mime_types = {
756
+ "png": "image/png",
757
+ "jpg": "image/jpeg",
758
+ "jpeg": "image/jpeg",
759
+ "gif": "image/gif",
760
+ "svg": "image/svg+xml",
761
+ "webp": "image/webp",
762
+ "ico": "image/x-icon",
763
+ "bmp": "image/bmp",
764
+ }
765
+ mime_type = mime_types.get(file_ext, "image/png")
766
+
767
+ return web.json_response(
768
+ {
769
+ "success": True,
770
+ "path": str(path),
771
+ "content": base64_content,
772
+ "size": file_size,
773
+ "type": "image",
774
+ "mime": mime_type,
775
+ "extension": file_ext,
776
+ }
777
+ )
778
+ except Exception as e:
779
+ self.logger.error(f"Error reading image file: {e}")
780
+ return web.json_response(
781
+ {
782
+ "success": False,
783
+ "error": f"Failed to read image: {e!s}",
784
+ },
785
+ status=500,
786
+ )
787
+
788
+ # Read text file content
789
+ try:
790
+ content = path.read_text(encoding="utf-8")
791
+ lines = content.count("\n") + 1
792
+ except UnicodeDecodeError:
793
+ return web.json_response(
794
+ {"success": False, "error": "File is not a text file"},
795
+ status=415,
796
+ )
797
+
798
+ return web.json_response(
799
+ {
800
+ "success": True,
801
+ "path": str(path),
802
+ "content": content,
803
+ "lines": lines,
804
+ "size": file_size,
805
+ "type": file_ext or "text",
806
+ }
807
+ )
808
+
809
+ except Exception as e:
810
+ self.logger.error(f"Error reading file: {e}")
811
+ return web.json_response(
812
+ {"success": False, "error": str(e)}, status=500
813
+ )
814
+
815
+ # Favicon handler
816
+ async def favicon_handler(request):
817
+ """Serve favicon.svg from static directory."""
818
+ from aiohttp.web_fileresponse import FileResponse
819
+
820
+ favicon_path = static_dir / "svelte-build" / "favicon.svg"
821
+ if favicon_path.exists():
822
+ return FileResponse(
823
+ favicon_path, headers={"Content-Type": "image/svg+xml"}
824
+ )
825
+ raise web.HTTPNotFound()
826
+
591
827
  # Version endpoint for dashboard build tracker
592
828
  async def version_handler(request):
593
829
  """Serve version information for dashboard build tracker."""
@@ -653,7 +889,7 @@ class UnifiedMonitorServer:
653
889
  async def working_directory_handler(request):
654
890
  """Return the current working directory."""
655
891
  return web.json_response(
656
- {"working_directory": Path.cwd(), "success": True}
892
+ {"working_directory": str(Path.cwd()), "success": True}
657
893
  )
658
894
 
659
895
  # Monitor page routes
@@ -671,15 +907,249 @@ class UnifiedMonitorServer:
671
907
  return web.Response(text=content, content_type="text/html")
672
908
  return web.Response(text="Page not found", status=404)
673
909
 
910
+ # Git history handler
911
+ async def git_history_handler(request: web.Request) -> web.Response:
912
+ """Get git history for a file."""
913
+ import subprocess
914
+
915
+ try:
916
+ data = await request.json()
917
+ file_path = data.get("path", "")
918
+ limit = data.get("limit", 10)
919
+
920
+ if not file_path:
921
+ return web.json_response(
922
+ {
923
+ "success": False,
924
+ "error": "No path provided",
925
+ "commits": [],
926
+ },
927
+ status=400,
928
+ )
929
+
930
+ path = Path(file_path)
931
+ if not path.exists():
932
+ return web.json_response(
933
+ {
934
+ "success": False,
935
+ "error": "File not found",
936
+ "commits": [],
937
+ },
938
+ status=404,
939
+ )
940
+
941
+ # Get git log for file
942
+ result = subprocess.run(
943
+ [
944
+ "git",
945
+ "log",
946
+ f"-{limit}",
947
+ "--pretty=format:%H|%an|%ar|%s",
948
+ "--",
949
+ str(path),
950
+ ],
951
+ check=False,
952
+ capture_output=True,
953
+ text=True,
954
+ cwd=str(path.parent),
955
+ )
956
+
957
+ commits = []
958
+ if result.returncode == 0 and result.stdout:
959
+ for line in result.stdout.strip().split("\n"):
960
+ if line:
961
+ parts = line.split("|", 3)
962
+ if len(parts) == 4:
963
+ commits.append(
964
+ {
965
+ "hash": parts[0][:7],
966
+ "author": parts[1],
967
+ "date": parts[2],
968
+ "message": parts[3],
969
+ }
970
+ )
971
+
972
+ return web.json_response({"success": True, "commits": commits})
973
+ except Exception as e:
974
+ return web.json_response(
975
+ {"success": False, "error": str(e), "commits": []}, status=500
976
+ )
977
+
978
+ # Git diff handler
979
+ async def git_diff_handler(request: web.Request) -> web.Response:
980
+ """Get git diff for a file with optional commit selection."""
981
+ import subprocess
982
+
983
+ try:
984
+ file_path = request.query.get("path", "")
985
+ commit_hash = request.query.get(
986
+ "commit", ""
987
+ ) # Optional commit hash
988
+
989
+ if not file_path:
990
+ return web.json_response(
991
+ {
992
+ "success": False,
993
+ "error": "No path provided",
994
+ "diff": "",
995
+ "has_changes": False,
996
+ },
997
+ status=400,
998
+ )
999
+
1000
+ path = Path(file_path)
1001
+ if not path.exists():
1002
+ return web.json_response(
1003
+ {
1004
+ "success": False,
1005
+ "error": "File not found",
1006
+ "diff": "",
1007
+ "has_changes": False,
1008
+ },
1009
+ status=404,
1010
+ )
1011
+
1012
+ # Find git repository root
1013
+ git_root_result = subprocess.run(
1014
+ ["git", "rev-parse", "--show-toplevel"],
1015
+ check=False,
1016
+ capture_output=True,
1017
+ text=True,
1018
+ cwd=str(path.parent),
1019
+ )
1020
+
1021
+ if git_root_result.returncode != 0:
1022
+ # Not in a git repository
1023
+ return web.json_response(
1024
+ {
1025
+ "success": True,
1026
+ "diff": "",
1027
+ "has_changes": False,
1028
+ "tracked": False,
1029
+ "history": [],
1030
+ "has_uncommitted": False,
1031
+ }
1032
+ )
1033
+
1034
+ git_root = Path(git_root_result.stdout.strip())
1035
+
1036
+ # Check if file is tracked by git
1037
+ ls_files_result = subprocess.run(
1038
+ ["git", "ls-files", "--error-unmatch", str(path)],
1039
+ check=False,
1040
+ capture_output=True,
1041
+ text=True,
1042
+ cwd=str(git_root),
1043
+ )
1044
+
1045
+ if ls_files_result.returncode != 0:
1046
+ # File is not tracked by git
1047
+ return web.json_response(
1048
+ {
1049
+ "success": True,
1050
+ "diff": "",
1051
+ "has_changes": False,
1052
+ "tracked": False,
1053
+ "history": [],
1054
+ "has_uncommitted": False,
1055
+ }
1056
+ )
1057
+
1058
+ # Get commit history for this file (last 5 commits)
1059
+ history_result = subprocess.run(
1060
+ [
1061
+ "git",
1062
+ "log",
1063
+ "-5",
1064
+ "--pretty=format:%H|%s|%ar",
1065
+ "--",
1066
+ str(path),
1067
+ ],
1068
+ check=False,
1069
+ capture_output=True,
1070
+ text=True,
1071
+ cwd=str(git_root),
1072
+ )
1073
+
1074
+ history = []
1075
+ if history_result.returncode == 0 and history_result.stdout:
1076
+ for line in history_result.stdout.strip().split("\n"):
1077
+ if line:
1078
+ parts = line.split("|", 2)
1079
+ if len(parts) == 3:
1080
+ history.append(
1081
+ {
1082
+ "hash": parts[0][:7], # Short hash
1083
+ "full_hash": parts[0], # Full hash for API
1084
+ "message": parts[1],
1085
+ "time_ago": parts[2],
1086
+ }
1087
+ )
1088
+
1089
+ # Check for uncommitted changes
1090
+ uncommitted_result = subprocess.run(
1091
+ ["git", "diff", "HEAD", str(path)],
1092
+ check=False,
1093
+ capture_output=True,
1094
+ text=True,
1095
+ cwd=str(git_root),
1096
+ )
1097
+
1098
+ has_uncommitted = bool(uncommitted_result.stdout.strip())
1099
+
1100
+ # Get diff based on commit parameter
1101
+ if commit_hash:
1102
+ # Get diff for specific commit
1103
+ result = subprocess.run(
1104
+ ["git", "show", commit_hash, "--", str(path)],
1105
+ check=False,
1106
+ capture_output=True,
1107
+ text=True,
1108
+ cwd=str(git_root),
1109
+ )
1110
+ diff_output = result.stdout if result.returncode == 0 else ""
1111
+ has_changes = bool(diff_output.strip())
1112
+ else:
1113
+ # Get uncommitted diff (default behavior)
1114
+ diff_output = uncommitted_result.stdout
1115
+ has_changes = has_uncommitted
1116
+
1117
+ return web.json_response(
1118
+ {
1119
+ "success": True,
1120
+ "diff": diff_output,
1121
+ "has_changes": has_changes,
1122
+ "tracked": True,
1123
+ "history": history,
1124
+ "has_uncommitted": has_uncommitted,
1125
+ }
1126
+ )
1127
+ except Exception as e:
1128
+ return web.json_response(
1129
+ {
1130
+ "success": False,
1131
+ "error": str(e),
1132
+ "diff": "",
1133
+ "has_changes": False,
1134
+ "history": [],
1135
+ "has_uncommitted": False,
1136
+ },
1137
+ status=500,
1138
+ )
1139
+
674
1140
  # Register routes
675
1141
  self.app.router.add_get("/", dashboard_index)
1142
+ self.app.router.add_get("/favicon.svg", favicon_handler)
676
1143
  self.app.router.add_get("/health", health_check)
677
1144
  self.app.router.add_get("/version.json", version_handler)
678
1145
  self.app.router.add_get("/api/config", config_handler)
679
1146
  self.app.router.add_get("/api/working-directory", working_directory_handler)
680
- self.app.router.add_get("/api/directory", list_directory)
1147
+ self.app.router.add_get("/api/files", api_files_handler)
1148
+ self.app.router.add_get("/api/file/read", api_file_read_handler)
1149
+ self.app.router.add_get("/api/file/diff", git_diff_handler)
681
1150
  self.app.router.add_post("/api/events", api_events_handler)
682
1151
  self.app.router.add_post("/api/file", api_file_handler)
1152
+ self.app.router.add_post("/api/git-history", git_history_handler)
683
1153
 
684
1154
  # Monitor page routes
685
1155
  self.app.router.add_get("/monitor", lambda r: monitor_page_handler(r))