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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT.md +164 -0
- claude_mpm/agents/BASE_ENGINEER.md +658 -0
- claude_mpm/agents/MEMORY.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +771 -1019
- claude_mpm/agents/WORKFLOW.md +5 -254
- claude_mpm/agents/agent_loader.py +1 -1
- claude_mpm/agents/base_agent.json +31 -0
- claude_mpm/agents/frontmatter_validator.py +2 -2
- claude_mpm/cli/commands/agent_state_manager.py +10 -10
- claude_mpm/cli/commands/agents.py +9 -9
- claude_mpm/cli/commands/auto_configure.py +4 -4
- claude_mpm/cli/commands/configure.py +1 -1
- claude_mpm/cli/commands/configure_agent_display.py +12 -0
- claude_mpm/cli/commands/mpm_init/core.py +72 -0
- claude_mpm/cli/commands/postmortem.py +1 -1
- claude_mpm/cli/commands/profile.py +276 -0
- claude_mpm/cli/commands/skills.py +14 -18
- claude_mpm/cli/executor.py +10 -0
- claude_mpm/cli/interactive/agent_wizard.py +2 -2
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/profile_parser.py +147 -0
- claude_mpm/cli/parsers/skills_parser.py +0 -6
- claude_mpm/cli/startup.py +506 -180
- claude_mpm/commands/mpm-config.md +13 -250
- claude_mpm/commands/mpm-doctor.md +9 -22
- claude_mpm/commands/mpm-help.md +5 -206
- claude_mpm/commands/mpm-init.md +81 -507
- claude_mpm/commands/mpm-monitor.md +15 -402
- claude_mpm/commands/mpm-organize.md +61 -441
- claude_mpm/commands/mpm-postmortem.md +6 -108
- claude_mpm/commands/mpm-session-resume.md +12 -363
- claude_mpm/commands/mpm-status.md +5 -69
- claude_mpm/commands/mpm-ticket-view.md +52 -495
- claude_mpm/commands/mpm-version.md +5 -107
- claude_mpm/core/config.py +2 -4
- claude_mpm/core/framework/loaders/agent_loader.py +1 -1
- claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
- claude_mpm/core/optimized_startup.py +61 -0
- claude_mpm/core/shared/config_loader.py +3 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BSNlmTZj.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/NqQ1dWOy.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Vzk33B_K.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.m1gL8KXf.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
- claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
- claude_mpm/dashboard/static/svelte-build/index.html +36 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
- claude_mpm/hooks/kuzu_memory_hook.py +5 -5
- claude_mpm/init.py +276 -0
- claude_mpm/models/git_repository.py +3 -3
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/cache_git_manager.py +6 -6
- claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -2
- claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
- claude_mpm/services/agents/deployment/agent_template_builder.py +31 -19
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
- claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
- claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +169 -26
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +98 -75
- claude_mpm/services/agents/git_source_manager.py +23 -4
- claude_mpm/services/agents/recommender.py +5 -3
- claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +121 -10
- claude_mpm/services/agents/startup_sync.py +22 -2
- claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
- claude_mpm/services/git/git_operations_service.py +8 -8
- claude_mpm/services/monitor/management/lifecycle.py +7 -1
- claude_mpm/services/monitor/server.py +473 -3
- claude_mpm/services/pm_skills_deployer.py +711 -0
- claude_mpm/services/profile_manager.py +337 -0
- claude_mpm/services/skills/git_skill_source_manager.py +148 -11
- claude_mpm/services/skills/selective_skill_deployer.py +97 -48
- claude_mpm/services/skills_deployer.py +161 -65
- claude_mpm/services/socketio/dashboard_server.py +1 -0
- claude_mpm/services/socketio/event_normalizer.py +37 -6
- claude_mpm/services/socketio/server/core.py +262 -123
- claude_mpm/skills/bundled/security-scanning.md +112 -0
- claude_mpm/skills/skill_manager.py +98 -3
- claude_mpm/templates/.pre-commit-config.yaml +112 -0
- claude_mpm/utils/agent_dependency_loader.py +14 -2
- claude_mpm/utils/agent_filters.py +1 -1
- claude_mpm/utils/migration.py +4 -4
- claude_mpm/utils/robust_installer.py +47 -3
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/METADATA +7 -4
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/RECORD +175 -81
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {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/
|
|
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/
|
|
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}
|
|
69
|
+
message = f"No agents deployed (0/{available_count} cached)"
|
|
70
70
|
fix_command = "claude-mpm agents deploy"
|
|
71
|
-
fix_description = "Deploy all
|
|
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" / "
|
|
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/
|
|
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/
|
|
18
|
-
... service.commit(Path("~/.claude-mpm/cache/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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))
|