claude-mpm 4.0.29__py3-none-any.whl → 4.0.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +48 -3
- claude_mpm/agents/BASE_PM.md +20 -15
- claude_mpm/agents/INSTRUCTIONS.md +12 -2
- claude_mpm/agents/templates/documentation.json +16 -3
- claude_mpm/agents/templates/engineer.json +19 -5
- claude_mpm/agents/templates/ops.json +19 -5
- claude_mpm/agents/templates/qa.json +16 -3
- claude_mpm/agents/templates/refactoring_engineer.json +25 -7
- claude_mpm/agents/templates/research.json +19 -5
- claude_mpm/cli/__init__.py +2 -0
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_manager.py +10 -6
- claude_mpm/cli/commands/agents.py +2 -1
- claude_mpm/cli/commands/cleanup.py +1 -1
- claude_mpm/cli/commands/doctor.py +209 -0
- claude_mpm/cli/commands/mcp.py +3 -3
- claude_mpm/cli/commands/mcp_install_commands.py +12 -30
- claude_mpm/cli/commands/mcp_server_commands.py +9 -9
- claude_mpm/cli/commands/run.py +31 -2
- claude_mpm/cli/commands/run_config_checker.py +1 -1
- claude_mpm/cli/parsers/agent_manager_parser.py +3 -3
- claude_mpm/cli/parsers/base_parser.py +5 -1
- claude_mpm/cli/parsers/mcp_parser.py +1 -1
- claude_mpm/cli/parsers/run_parser.py +1 -1
- claude_mpm/cli/startup_logging.py +463 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +78 -0
- claude_mpm/core/framework_loader.py +45 -11
- claude_mpm/core/interactive_session.py +82 -3
- claude_mpm/core/output_style_manager.py +6 -6
- claude_mpm/core/unified_paths.py +128 -0
- claude_mpm/scripts/mcp_server.py +2 -2
- claude_mpm/services/agents/deployment/agent_validator.py +1 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +69 -1
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +2 -2
- claude_mpm/services/diagnostics/__init__.py +18 -0
- claude_mpm/services/diagnostics/checks/__init__.py +30 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +319 -0
- claude_mpm/services/diagnostics/checks/base_check.py +64 -0
- claude_mpm/services/diagnostics/checks/claude_desktop_check.py +283 -0
- claude_mpm/services/diagnostics/checks/common_issues_check.py +354 -0
- claude_mpm/services/diagnostics/checks/configuration_check.py +300 -0
- claude_mpm/services/diagnostics/checks/filesystem_check.py +233 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +255 -0
- claude_mpm/services/diagnostics/checks/mcp_check.py +315 -0
- claude_mpm/services/diagnostics/checks/monitor_check.py +282 -0
- claude_mpm/services/diagnostics/checks/startup_log_check.py +322 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +247 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +283 -0
- claude_mpm/services/diagnostics/models.py +120 -0
- claude_mpm/services/mcp_gateway/core/interfaces.py +1 -1
- claude_mpm/services/mcp_gateway/main.py +1 -1
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +3 -3
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +1 -1
- claude_mpm/services/mcp_gateway/server/stdio_server.py +3 -3
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +2 -2
- claude_mpm/services/socketio/handlers/registry.py +39 -7
- claude_mpm/services/socketio/server/core.py +72 -22
- claude_mpm/validation/frontmatter_validator.py +1 -1
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.31.dist-info}/METADATA +4 -1
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.31.dist-info}/RECORD +65 -48
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.31.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.31.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.31.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.31.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check startup log health and analyze for common issues.
|
|
3
|
+
|
|
4
|
+
WHY: Startup logs contain valuable diagnostic information about agent deployment,
|
|
5
|
+
MCP configuration, memory loading, and other startup issues that may not be
|
|
6
|
+
immediately visible to users.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Parse the most recent startup log file
|
|
10
|
+
- Identify common error patterns and provide fixes
|
|
11
|
+
- Count errors, warnings, and info messages
|
|
12
|
+
- Provide actionable recommendations based on log content
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from ..models import DiagnosticResult, DiagnosticStatus
|
|
21
|
+
from .base_check import BaseDiagnosticCheck
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StartupLogCheck(BaseDiagnosticCheck):
|
|
25
|
+
"""Analyze startup logs for errors and issues."""
|
|
26
|
+
|
|
27
|
+
# Common error patterns and their fixes
|
|
28
|
+
ERROR_PATTERNS = {
|
|
29
|
+
r"Agent deployment.*failed": (
|
|
30
|
+
"Agent deployment failure",
|
|
31
|
+
"Check agent configuration in .claude/agents/ and run 'claude-mpm deploy'"
|
|
32
|
+
),
|
|
33
|
+
r"MCP.*not found": (
|
|
34
|
+
"MCP server not found",
|
|
35
|
+
"Install MCP server: npm install -g @modelcontextprotocol/server"
|
|
36
|
+
),
|
|
37
|
+
r"Port \d+ .*in use": (
|
|
38
|
+
"Port binding conflict",
|
|
39
|
+
"Kill the process using the port or use --websocket-port to specify a different port"
|
|
40
|
+
),
|
|
41
|
+
r"Memory loading.*error": (
|
|
42
|
+
"Memory loading failure",
|
|
43
|
+
"Check .claude-mpm/memory/ permissions and run 'claude-mpm memory validate'"
|
|
44
|
+
),
|
|
45
|
+
r"Permission denied": (
|
|
46
|
+
"Permission error",
|
|
47
|
+
"Check file permissions in project directory and .claude-mpm/"
|
|
48
|
+
),
|
|
49
|
+
r"ModuleNotFoundError|ImportError": (
|
|
50
|
+
"Missing Python dependency",
|
|
51
|
+
"Install missing dependencies: pip install -e . or pip install claude-mpm[agents]"
|
|
52
|
+
),
|
|
53
|
+
r"Socket\.IO.*failed|socketio.*error": (
|
|
54
|
+
"Socket.IO initialization failure",
|
|
55
|
+
"Install monitor dependencies: pip install claude-mpm[monitor]"
|
|
56
|
+
),
|
|
57
|
+
r"Configuration.*invalid|yaml.*error": (
|
|
58
|
+
"Configuration file error",
|
|
59
|
+
"Validate configuration: claude-mpm config validate"
|
|
60
|
+
),
|
|
61
|
+
r"claude\.json.*large|memory.*issue": (
|
|
62
|
+
"Large .claude.json file",
|
|
63
|
+
"Run 'claude-mpm cleanup-memory' to archive old conversations"
|
|
64
|
+
),
|
|
65
|
+
r"Failed to start.*daemon": (
|
|
66
|
+
"Daemon startup failure",
|
|
67
|
+
"Check system resources and try: claude-mpm monitor stop && claude-mpm monitor start"
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Warning patterns that should be noted
|
|
72
|
+
WARNING_PATTERNS = {
|
|
73
|
+
r"agent.*source.*tracking": "Agent source tracking warning",
|
|
74
|
+
r"dependency.*missing": "Missing optional dependency",
|
|
75
|
+
r"deprecated": "Deprecation warning",
|
|
76
|
+
r"Socket\.IO.*not available": "Monitor mode unavailable",
|
|
77
|
+
r"Response logging.*disabled": "Response logging is disabled",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def name(self) -> str:
|
|
82
|
+
return "startup_log_check"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def category(self) -> str:
|
|
86
|
+
return "Startup Log"
|
|
87
|
+
|
|
88
|
+
def run(self) -> DiagnosticResult:
|
|
89
|
+
"""Run startup log diagnostics."""
|
|
90
|
+
try:
|
|
91
|
+
# Find the latest startup log
|
|
92
|
+
log_file = self._find_latest_log()
|
|
93
|
+
|
|
94
|
+
if not log_file:
|
|
95
|
+
return DiagnosticResult(
|
|
96
|
+
category=self.category,
|
|
97
|
+
status=DiagnosticStatus.WARNING,
|
|
98
|
+
message="No startup logs found",
|
|
99
|
+
details={
|
|
100
|
+
"recommendation": "Startup logging will be created on next run"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Parse the log file
|
|
105
|
+
analysis = self._analyze_log_file(log_file)
|
|
106
|
+
|
|
107
|
+
# Determine status based on findings
|
|
108
|
+
status = self._determine_status(analysis)
|
|
109
|
+
message = self._create_message(analysis)
|
|
110
|
+
|
|
111
|
+
# Build details
|
|
112
|
+
details = {
|
|
113
|
+
"log_file": str(log_file),
|
|
114
|
+
"last_startup": analysis["timestamp"],
|
|
115
|
+
"errors": analysis["error_count"],
|
|
116
|
+
"warnings": analysis["warning_count"],
|
|
117
|
+
"info_messages": analysis["info_count"],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Add specific issues found
|
|
121
|
+
if analysis["errors_found"]:
|
|
122
|
+
details["errors_found"] = analysis["errors_found"]
|
|
123
|
+
|
|
124
|
+
if analysis["warnings_found"]:
|
|
125
|
+
details["warnings_found"] = analysis["warnings_found"]
|
|
126
|
+
|
|
127
|
+
# Add recommendations
|
|
128
|
+
if analysis["recommendations"]:
|
|
129
|
+
details["recommendations"] = analysis["recommendations"]
|
|
130
|
+
|
|
131
|
+
# Create sub-results if verbose
|
|
132
|
+
sub_results = []
|
|
133
|
+
if self.verbose and analysis["errors_found"]:
|
|
134
|
+
for error_type, fix in analysis["errors_found"]:
|
|
135
|
+
sub_results.append(
|
|
136
|
+
DiagnosticResult(
|
|
137
|
+
category="Error",
|
|
138
|
+
status=DiagnosticStatus.ERROR,
|
|
139
|
+
message=error_type,
|
|
140
|
+
details={"fix": fix}
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return DiagnosticResult(
|
|
145
|
+
category=self.category,
|
|
146
|
+
status=status,
|
|
147
|
+
message=message,
|
|
148
|
+
details=details,
|
|
149
|
+
sub_results=sub_results if self.verbose else [],
|
|
150
|
+
fix_command=self._get_fix_command(analysis)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return DiagnosticResult(
|
|
155
|
+
category=self.category,
|
|
156
|
+
status=DiagnosticStatus.ERROR,
|
|
157
|
+
message=f"Startup log check failed: {str(e)}",
|
|
158
|
+
details={"error": str(e)}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _find_latest_log(self) -> Optional[Path]:
|
|
162
|
+
"""Find the most recent startup log file."""
|
|
163
|
+
log_dir = Path.cwd() / ".claude-mpm" / "logs" / "startup"
|
|
164
|
+
|
|
165
|
+
if not log_dir.exists():
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
log_files = sorted(
|
|
169
|
+
log_dir.glob("startup-*.log"),
|
|
170
|
+
key=lambda p: p.stat().st_mtime,
|
|
171
|
+
reverse=True
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return log_files[0] if log_files else None
|
|
175
|
+
|
|
176
|
+
def _analyze_log_file(self, log_file: Path) -> Dict[str, Any]:
|
|
177
|
+
"""Analyze the contents of a startup log file."""
|
|
178
|
+
analysis = {
|
|
179
|
+
"timestamp": None,
|
|
180
|
+
"error_count": 0,
|
|
181
|
+
"warning_count": 0,
|
|
182
|
+
"info_count": 0,
|
|
183
|
+
"debug_count": 0,
|
|
184
|
+
"errors_found": [],
|
|
185
|
+
"warnings_found": [],
|
|
186
|
+
"recommendations": [],
|
|
187
|
+
"startup_successful": False,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
with open(log_file, 'r', encoding='utf-8') as f:
|
|
192
|
+
lines = f.readlines()
|
|
193
|
+
|
|
194
|
+
# Extract timestamp from filename or first line
|
|
195
|
+
match = re.search(r'(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2})', log_file.name)
|
|
196
|
+
if match:
|
|
197
|
+
timestamp_str = match.group(1)
|
|
198
|
+
# Convert to readable format
|
|
199
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H-%M-%S")
|
|
200
|
+
analysis["timestamp"] = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
201
|
+
|
|
202
|
+
# Process each line
|
|
203
|
+
for line in lines:
|
|
204
|
+
# Count log levels
|
|
205
|
+
if " - ERROR - " in line:
|
|
206
|
+
analysis["error_count"] += 1
|
|
207
|
+
self._check_error_patterns(line, analysis)
|
|
208
|
+
elif " - WARNING - " in line:
|
|
209
|
+
analysis["warning_count"] += 1
|
|
210
|
+
self._check_warning_patterns(line, analysis)
|
|
211
|
+
elif " - INFO - " in line:
|
|
212
|
+
analysis["info_count"] += 1
|
|
213
|
+
elif " - DEBUG - " in line:
|
|
214
|
+
analysis["debug_count"] += 1
|
|
215
|
+
|
|
216
|
+
# Check for successful startup indicators
|
|
217
|
+
if "Claude session completed successfully" in line:
|
|
218
|
+
analysis["startup_successful"] = True
|
|
219
|
+
elif "Starting Claude MPM session" in line:
|
|
220
|
+
analysis["startup_successful"] = True # At least startup began
|
|
221
|
+
|
|
222
|
+
# Generate recommendations based on findings
|
|
223
|
+
self._generate_recommendations(analysis)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
analysis["error_count"] += 1
|
|
227
|
+
analysis["errors_found"].append(("Failed to parse log", str(e)))
|
|
228
|
+
|
|
229
|
+
return analysis
|
|
230
|
+
|
|
231
|
+
def _check_error_patterns(self, line: str, analysis: Dict[str, Any]) -> None:
|
|
232
|
+
"""Check line for known error patterns."""
|
|
233
|
+
for pattern, (error_type, fix) in self.ERROR_PATTERNS.items():
|
|
234
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
235
|
+
# Avoid duplicates
|
|
236
|
+
if (error_type, fix) not in analysis["errors_found"]:
|
|
237
|
+
analysis["errors_found"].append((error_type, fix))
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
def _check_warning_patterns(self, line: str, analysis: Dict[str, Any]) -> None:
|
|
241
|
+
"""Check line for known warning patterns."""
|
|
242
|
+
for pattern, warning_type in self.WARNING_PATTERNS.items():
|
|
243
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
244
|
+
# Avoid duplicates
|
|
245
|
+
if warning_type not in analysis["warnings_found"]:
|
|
246
|
+
analysis["warnings_found"].append(warning_type)
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
def _generate_recommendations(self, analysis: Dict[str, Any]) -> None:
|
|
250
|
+
"""Generate recommendations based on analysis."""
|
|
251
|
+
recommendations = []
|
|
252
|
+
|
|
253
|
+
# High error count
|
|
254
|
+
if analysis["error_count"] > 5:
|
|
255
|
+
recommendations.append(
|
|
256
|
+
"High error count detected. Review full log for systematic issues."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# No successful startup
|
|
260
|
+
if not analysis["startup_successful"] and analysis["error_count"] > 0:
|
|
261
|
+
recommendations.append(
|
|
262
|
+
"Startup may have failed. Check error messages above."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Specific error recommendations
|
|
266
|
+
if any("MCP" in error[0] for error in analysis["errors_found"]):
|
|
267
|
+
recommendations.append(
|
|
268
|
+
"MCP issues detected. Run 'claude-mpm doctor' for full diagnostics."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if any("Agent" in error[0] for error in analysis["errors_found"]):
|
|
272
|
+
recommendations.append(
|
|
273
|
+
"Agent issues detected. Run 'claude-mpm agents validate' to check agents."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if any("memory" in error[0].lower() for error in analysis["errors_found"]):
|
|
277
|
+
recommendations.append(
|
|
278
|
+
"Memory issues detected. Run 'claude-mpm cleanup-memory' to free space."
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
analysis["recommendations"] = recommendations
|
|
282
|
+
|
|
283
|
+
def _determine_status(self, analysis: Dict[str, Any]) -> DiagnosticStatus:
|
|
284
|
+
"""Determine overall status based on analysis."""
|
|
285
|
+
if analysis["error_count"] > 0:
|
|
286
|
+
return DiagnosticStatus.ERROR
|
|
287
|
+
elif analysis["warning_count"] > 3:
|
|
288
|
+
return DiagnosticStatus.WARNING
|
|
289
|
+
elif analysis["warning_count"] > 0:
|
|
290
|
+
return DiagnosticStatus.WARNING
|
|
291
|
+
else:
|
|
292
|
+
return DiagnosticStatus.OK
|
|
293
|
+
|
|
294
|
+
def _create_message(self, analysis: Dict[str, Any]) -> str:
|
|
295
|
+
"""Create summary message based on analysis."""
|
|
296
|
+
if analysis["error_count"] > 0:
|
|
297
|
+
return f"Startup has {analysis['error_count']} error(s)"
|
|
298
|
+
elif analysis["warning_count"] > 0:
|
|
299
|
+
return f"Startup successful with {analysis['warning_count']} warning(s)"
|
|
300
|
+
elif analysis["startup_successful"]:
|
|
301
|
+
return "Last startup was successful"
|
|
302
|
+
else:
|
|
303
|
+
return "Startup log is clean"
|
|
304
|
+
|
|
305
|
+
def _get_fix_command(self, analysis: Dict[str, Any]) -> Optional[str]:
|
|
306
|
+
"""Get the most relevant fix command based on errors found."""
|
|
307
|
+
if not analysis["errors_found"]:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
# Priority order for fix commands
|
|
311
|
+
for error_type, fix in analysis["errors_found"]:
|
|
312
|
+
if "claude-mpm" in fix:
|
|
313
|
+
# Extract claude-mpm command from fix suggestion
|
|
314
|
+
match = re.search(r'(claude-mpm [^\'"\n]+)', fix)
|
|
315
|
+
if match:
|
|
316
|
+
return match.group(1)
|
|
317
|
+
elif "pip install" in fix:
|
|
318
|
+
match = re.search(r'(pip install [^\'"\n]+)', fix)
|
|
319
|
+
if match:
|
|
320
|
+
return match.group(1)
|
|
321
|
+
|
|
322
|
+
return None
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diagnostic runner service for orchestrating health checks.
|
|
3
|
+
|
|
4
|
+
WHY: Coordinate execution of all diagnostic checks, handle errors gracefully,
|
|
5
|
+
and aggregate results for reporting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
11
|
+
from typing import List, Optional, Type
|
|
12
|
+
|
|
13
|
+
from .checks import (
|
|
14
|
+
AgentCheck,
|
|
15
|
+
BaseDiagnosticCheck,
|
|
16
|
+
ClaudeDesktopCheck,
|
|
17
|
+
CommonIssuesCheck,
|
|
18
|
+
ConfigurationCheck,
|
|
19
|
+
FilesystemCheck,
|
|
20
|
+
InstallationCheck,
|
|
21
|
+
MCPCheck,
|
|
22
|
+
MonitorCheck,
|
|
23
|
+
StartupLogCheck,
|
|
24
|
+
)
|
|
25
|
+
from .models import DiagnosticResult, DiagnosticStatus, DiagnosticSummary
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DiagnosticRunner:
|
|
29
|
+
"""Orchestrate diagnostic checks and aggregate results.
|
|
30
|
+
|
|
31
|
+
WHY: Provides a single entry point for running all diagnostics with
|
|
32
|
+
proper error handling, parallel execution, and result aggregation.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, verbose: bool = False, fix: bool = False):
|
|
36
|
+
"""Initialize diagnostic runner.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
verbose: Include detailed information in results
|
|
40
|
+
fix: Attempt to fix issues automatically (future feature)
|
|
41
|
+
"""
|
|
42
|
+
self.verbose = verbose
|
|
43
|
+
self.fix = fix
|
|
44
|
+
self.logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
# Define check order (dependencies first)
|
|
47
|
+
self.check_classes: List[Type[BaseDiagnosticCheck]] = [
|
|
48
|
+
InstallationCheck,
|
|
49
|
+
ConfigurationCheck,
|
|
50
|
+
FilesystemCheck,
|
|
51
|
+
ClaudeDesktopCheck,
|
|
52
|
+
AgentCheck,
|
|
53
|
+
MCPCheck,
|
|
54
|
+
MonitorCheck,
|
|
55
|
+
StartupLogCheck, # Check startup logs for recent issues
|
|
56
|
+
CommonIssuesCheck,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
def run_diagnostics(self) -> DiagnosticSummary:
|
|
60
|
+
"""Run all diagnostic checks synchronously.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
DiagnosticSummary with all results
|
|
64
|
+
"""
|
|
65
|
+
summary = DiagnosticSummary()
|
|
66
|
+
|
|
67
|
+
# Run checks in order
|
|
68
|
+
for check_class in self.check_classes:
|
|
69
|
+
try:
|
|
70
|
+
check = check_class(verbose=self.verbose)
|
|
71
|
+
|
|
72
|
+
# Skip if check shouldn't run
|
|
73
|
+
if not check.should_run():
|
|
74
|
+
self.logger.debug(f"Skipping {check.name}")
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
self.logger.debug(f"Running {check.name}")
|
|
78
|
+
result = check.run()
|
|
79
|
+
summary.add_result(result)
|
|
80
|
+
|
|
81
|
+
# If fix mode is enabled and there's a fix available
|
|
82
|
+
if self.fix and result.has_issues and result.fix_command:
|
|
83
|
+
self._attempt_fix(result)
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
self.logger.error(f"Check {check_class.__name__} failed: {e}")
|
|
87
|
+
error_result = DiagnosticResult(
|
|
88
|
+
category=check_class.__name__.replace("Check", ""),
|
|
89
|
+
status=DiagnosticStatus.ERROR,
|
|
90
|
+
message=f"Check failed: {str(e)}",
|
|
91
|
+
details={"error": str(e)}
|
|
92
|
+
)
|
|
93
|
+
summary.add_result(error_result)
|
|
94
|
+
|
|
95
|
+
return summary
|
|
96
|
+
|
|
97
|
+
def run_diagnostics_parallel(self) -> DiagnosticSummary:
|
|
98
|
+
"""Run diagnostic checks in parallel for faster execution.
|
|
99
|
+
|
|
100
|
+
WHY: Some checks may involve I/O or network operations, running them
|
|
101
|
+
in parallel can significantly speed up the overall diagnostic process.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
DiagnosticSummary with all results
|
|
105
|
+
"""
|
|
106
|
+
summary = DiagnosticSummary()
|
|
107
|
+
|
|
108
|
+
# Group checks by dependency level
|
|
109
|
+
# Level 1: No dependencies
|
|
110
|
+
level1 = [InstallationCheck, FilesystemCheck, ConfigurationCheck]
|
|
111
|
+
# Level 2: May depend on level 1
|
|
112
|
+
level2 = [ClaudeDesktopCheck, AgentCheck, MCPCheck, MonitorCheck]
|
|
113
|
+
# Level 3: Depends on others
|
|
114
|
+
level3 = [CommonIssuesCheck]
|
|
115
|
+
|
|
116
|
+
for level in [level1, level2, level3]:
|
|
117
|
+
level_results = self._run_level_parallel(level)
|
|
118
|
+
for result in level_results:
|
|
119
|
+
summary.add_result(result)
|
|
120
|
+
|
|
121
|
+
return summary
|
|
122
|
+
|
|
123
|
+
def _run_level_parallel(self, check_classes: List[Type[BaseDiagnosticCheck]]) -> List[DiagnosticResult]:
|
|
124
|
+
"""Run a group of checks in parallel.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
check_classes: List of check classes to run
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of DiagnosticResults
|
|
131
|
+
"""
|
|
132
|
+
results = []
|
|
133
|
+
|
|
134
|
+
with ThreadPoolExecutor(max_workers=len(check_classes)) as executor:
|
|
135
|
+
future_to_check = {}
|
|
136
|
+
|
|
137
|
+
for check_class in check_classes:
|
|
138
|
+
try:
|
|
139
|
+
check = check_class(verbose=self.verbose)
|
|
140
|
+
if check.should_run():
|
|
141
|
+
future = executor.submit(check.run)
|
|
142
|
+
future_to_check[future] = check_class.__name__
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.logger.error(f"Failed to create check {check_class.__name__}: {e}")
|
|
145
|
+
results.append(DiagnosticResult(
|
|
146
|
+
category=check_class.__name__.replace("Check", ""),
|
|
147
|
+
status=DiagnosticStatus.ERROR,
|
|
148
|
+
message=f"Check initialization failed: {str(e)}",
|
|
149
|
+
details={"error": str(e)}
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
for future in as_completed(future_to_check):
|
|
153
|
+
check_name = future_to_check[future]
|
|
154
|
+
try:
|
|
155
|
+
result = future.result(timeout=10)
|
|
156
|
+
results.append(result)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
self.logger.error(f"Check {check_name} failed: {e}")
|
|
159
|
+
results.append(DiagnosticResult(
|
|
160
|
+
category=check_name.replace("Check", ""),
|
|
161
|
+
status=DiagnosticStatus.ERROR,
|
|
162
|
+
message=f"Check execution failed: {str(e)}",
|
|
163
|
+
details={"error": str(e)}
|
|
164
|
+
))
|
|
165
|
+
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
def run_specific_checks(self, check_names: List[str]) -> DiagnosticSummary:
|
|
169
|
+
"""Run only specific diagnostic checks.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
check_names: List of check names to run (e.g., ["installation", "agents"])
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
DiagnosticSummary with results from specified checks
|
|
176
|
+
"""
|
|
177
|
+
summary = DiagnosticSummary()
|
|
178
|
+
|
|
179
|
+
# Map check names to classes
|
|
180
|
+
check_map = {
|
|
181
|
+
"installation": InstallationCheck,
|
|
182
|
+
"configuration": ConfigurationCheck,
|
|
183
|
+
"config": ConfigurationCheck,
|
|
184
|
+
"filesystem": FilesystemCheck,
|
|
185
|
+
"fs": FilesystemCheck,
|
|
186
|
+
"claude": ClaudeDesktopCheck,
|
|
187
|
+
"claude_desktop": ClaudeDesktopCheck,
|
|
188
|
+
"agents": AgentCheck,
|
|
189
|
+
"agent": AgentCheck,
|
|
190
|
+
"mcp": MCPCheck,
|
|
191
|
+
"monitor": MonitorCheck,
|
|
192
|
+
"monitoring": MonitorCheck,
|
|
193
|
+
"common": CommonIssuesCheck,
|
|
194
|
+
"issues": CommonIssuesCheck,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for name in check_names:
|
|
198
|
+
check_class = check_map.get(name.lower())
|
|
199
|
+
if not check_class:
|
|
200
|
+
self.logger.warning(f"Unknown check: {name}")
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
check = check_class(verbose=self.verbose)
|
|
205
|
+
if check.should_run():
|
|
206
|
+
result = check.run()
|
|
207
|
+
summary.add_result(result)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
self.logger.error(f"Check {name} failed: {e}")
|
|
210
|
+
error_result = DiagnosticResult(
|
|
211
|
+
category=check_class.__name__.replace("Check", ""),
|
|
212
|
+
status=DiagnosticStatus.ERROR,
|
|
213
|
+
message=f"Check failed: {str(e)}",
|
|
214
|
+
details={"error": str(e)}
|
|
215
|
+
)
|
|
216
|
+
summary.add_result(error_result)
|
|
217
|
+
|
|
218
|
+
return summary
|
|
219
|
+
|
|
220
|
+
def _attempt_fix(self, result: DiagnosticResult):
|
|
221
|
+
"""Attempt to fix an issue automatically.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
result: DiagnosticResult with fix_command
|
|
225
|
+
"""
|
|
226
|
+
if not result.fix_command:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
self.logger.info(f"Attempting to fix: {result.message}")
|
|
230
|
+
self.logger.info(f"Running: {result.fix_command}")
|
|
231
|
+
|
|
232
|
+
# In a real implementation, this would execute the fix command
|
|
233
|
+
# For now, we just log it
|
|
234
|
+
# TODO: Implement actual fix execution with proper safeguards
|
|
235
|
+
|
|
236
|
+
async def run_diagnostics_async(self) -> DiagnosticSummary:
|
|
237
|
+
"""Run diagnostics asynchronously (future enhancement).
|
|
238
|
+
|
|
239
|
+
WHY: For integration with async frameworks and better performance
|
|
240
|
+
with I/O-bound checks.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
DiagnosticSummary with all results
|
|
244
|
+
"""
|
|
245
|
+
# Convert sync execution to async for now
|
|
246
|
+
loop = asyncio.get_event_loop()
|
|
247
|
+
return await loop.run_in_executor(None, self.run_diagnostics)
|