claude-mpm 4.0.28__py3-none-any.whl → 4.0.30__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/agent-manager.json +24 -0
- claude_mpm/agents/templates/agent-manager.md +304 -0
- 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 +4 -0
- claude_mpm/cli/commands/__init__.py +4 -0
- claude_mpm/cli/commands/agent_manager.py +521 -0
- 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/memory.py +1 -1
- 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 +247 -0
- claude_mpm/cli/parsers/base_parser.py +12 -1
- claude_mpm/cli/parsers/mcp_parser.py +1 -1
- claude_mpm/cli/parsers/run_parser.py +1 -1
- claude_mpm/cli/shared/__init__.py +1 -1
- claude_mpm/cli/startup_logging.py +463 -0
- claude_mpm/constants.py +2 -0
- claude_mpm/core/claude_runner.py +81 -2
- claude_mpm/core/constants.py +2 -2
- 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/socketio_pool.py +2 -2
- claude_mpm/core/unified_paths.py +128 -0
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/dashboard.css +170 -0
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +21 -3
- claude_mpm/dashboard/static/js/components/module-viewer.js +129 -1
- claude_mpm/dashboard/static/js/dashboard.js +116 -0
- claude_mpm/dashboard/static/js/socket-client.js +0 -1
- claude_mpm/hooks/claude_hooks/connection_pool.py +1 -1
- claude_mpm/hooks/claude_hooks/hook_handler.py +1 -1
- claude_mpm/scripts/mcp_server.py +2 -2
- claude_mpm/services/agents/agent_builder.py +455 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +10 -3
- 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/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/memory/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/connection.py +27 -33
- 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.28.dist-info → claude_mpm-4.0.30.dist-info}/METADATA +4 -1
- {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.30.dist-info}/RECORD +89 -67
- /claude_mpm/cli/shared/{command_base.py → base_command.py} +0 -0
- {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.30.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.30.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.30.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.30.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check for common known issues.
|
|
3
|
+
|
|
4
|
+
WHY: Proactively detect and report common problems that users encounter,
|
|
5
|
+
providing specific fixes for each issue.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, List
|
|
11
|
+
|
|
12
|
+
from ..models import DiagnosticResult, DiagnosticStatus
|
|
13
|
+
from .base_check import BaseDiagnosticCheck
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommonIssuesCheck(BaseDiagnosticCheck):
|
|
17
|
+
"""Check for common known issues."""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return "common_issues_check"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def category(self) -> str:
|
|
25
|
+
return "Common Issues"
|
|
26
|
+
|
|
27
|
+
def run(self) -> DiagnosticResult:
|
|
28
|
+
"""Run common issues diagnostics."""
|
|
29
|
+
try:
|
|
30
|
+
issues_found = []
|
|
31
|
+
warnings_found = []
|
|
32
|
+
details = {}
|
|
33
|
+
|
|
34
|
+
# Check for large .claude.json file
|
|
35
|
+
claude_json_result = self._check_claude_json_size()
|
|
36
|
+
if claude_json_result.has_issues:
|
|
37
|
+
if claude_json_result.status == DiagnosticStatus.ERROR:
|
|
38
|
+
issues_found.append(claude_json_result)
|
|
39
|
+
else:
|
|
40
|
+
warnings_found.append(claude_json_result)
|
|
41
|
+
|
|
42
|
+
# Check for memory leaks
|
|
43
|
+
memory_result = self._check_memory_issues()
|
|
44
|
+
if memory_result.has_issues:
|
|
45
|
+
warnings_found.append(memory_result)
|
|
46
|
+
|
|
47
|
+
# Check for stale lock files
|
|
48
|
+
lock_result = self._check_lock_files()
|
|
49
|
+
if lock_result.has_issues:
|
|
50
|
+
warnings_found.append(lock_result)
|
|
51
|
+
|
|
52
|
+
# Check for conflicting configurations
|
|
53
|
+
conflict_result = self._check_config_conflicts()
|
|
54
|
+
if conflict_result.has_issues:
|
|
55
|
+
warnings_found.append(conflict_result)
|
|
56
|
+
|
|
57
|
+
# Check for outdated cache
|
|
58
|
+
cache_result = self._check_cache_issues()
|
|
59
|
+
if cache_result.has_issues:
|
|
60
|
+
warnings_found.append(cache_result)
|
|
61
|
+
|
|
62
|
+
# Aggregate results
|
|
63
|
+
total_issues = len(issues_found) + len(warnings_found)
|
|
64
|
+
|
|
65
|
+
if issues_found:
|
|
66
|
+
status = DiagnosticStatus.ERROR
|
|
67
|
+
message = f"{len(issues_found)} critical issue(s), {len(warnings_found)} warning(s)"
|
|
68
|
+
elif warnings_found:
|
|
69
|
+
status = DiagnosticStatus.WARNING
|
|
70
|
+
message = f"{len(warnings_found)} known issue(s) detected"
|
|
71
|
+
else:
|
|
72
|
+
status = DiagnosticStatus.OK
|
|
73
|
+
message = "No known issues detected"
|
|
74
|
+
|
|
75
|
+
details = {
|
|
76
|
+
"total_issues": total_issues,
|
|
77
|
+
"critical": len(issues_found),
|
|
78
|
+
"warnings": len(warnings_found)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
sub_results = issues_found + warnings_found
|
|
82
|
+
|
|
83
|
+
return DiagnosticResult(
|
|
84
|
+
category=self.category,
|
|
85
|
+
status=status,
|
|
86
|
+
message=message,
|
|
87
|
+
details=details,
|
|
88
|
+
sub_results=sub_results if self.verbose else []
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return DiagnosticResult(
|
|
93
|
+
category=self.category,
|
|
94
|
+
status=DiagnosticStatus.ERROR,
|
|
95
|
+
message=f"Common issues check failed: {str(e)}",
|
|
96
|
+
details={"error": str(e)}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _check_claude_json_size(self) -> DiagnosticResult:
|
|
100
|
+
"""Check for large .claude.json file causing memory issues."""
|
|
101
|
+
claude_json_path = Path.cwd() / ".claude.json"
|
|
102
|
+
|
|
103
|
+
if not claude_json_path.exists():
|
|
104
|
+
return DiagnosticResult(
|
|
105
|
+
category="Large .claude.json",
|
|
106
|
+
status=DiagnosticStatus.OK,
|
|
107
|
+
message="No .claude.json file",
|
|
108
|
+
details={"exists": False}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
size_bytes = claude_json_path.stat().st_size
|
|
113
|
+
size_mb = size_bytes / (1024 * 1024)
|
|
114
|
+
|
|
115
|
+
# Try to count conversations
|
|
116
|
+
conversation_count = 0
|
|
117
|
+
try:
|
|
118
|
+
with open(claude_json_path, 'r') as f:
|
|
119
|
+
data = json.load(f)
|
|
120
|
+
if isinstance(data, dict) and "conversations" in data:
|
|
121
|
+
conversation_count = len(data["conversations"])
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
details = {
|
|
126
|
+
"path": str(claude_json_path),
|
|
127
|
+
"size_mb": round(size_mb, 2),
|
|
128
|
+
"size_bytes": size_bytes,
|
|
129
|
+
"conversations": conversation_count
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if size_mb > 10: # Critical: >10MB
|
|
133
|
+
return DiagnosticResult(
|
|
134
|
+
category="Large .claude.json",
|
|
135
|
+
status=DiagnosticStatus.ERROR,
|
|
136
|
+
message=f"Critical: .claude.json is {size_mb:.1f}MB (causes memory issues)",
|
|
137
|
+
details=details,
|
|
138
|
+
fix_command="claude-mpm cleanup-memory",
|
|
139
|
+
fix_description="Clean up conversation history to prevent memory issues"
|
|
140
|
+
)
|
|
141
|
+
elif size_mb > 1: # Warning: >1MB
|
|
142
|
+
return DiagnosticResult(
|
|
143
|
+
category="Large .claude.json",
|
|
144
|
+
status=DiagnosticStatus.WARNING,
|
|
145
|
+
message=f".claude.json is {size_mb:.1f}MB (may cause memory issues)",
|
|
146
|
+
details=details,
|
|
147
|
+
fix_command="claude-mpm cleanup-memory",
|
|
148
|
+
fix_description="Consider cleaning up old conversations"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return DiagnosticResult(
|
|
152
|
+
category="Large .claude.json",
|
|
153
|
+
status=DiagnosticStatus.OK,
|
|
154
|
+
message=f".claude.json size acceptable ({size_mb:.2f}MB)",
|
|
155
|
+
details=details
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return DiagnosticResult(
|
|
160
|
+
category="Large .claude.json",
|
|
161
|
+
status=DiagnosticStatus.WARNING,
|
|
162
|
+
message=f"Could not check .claude.json: {str(e)}",
|
|
163
|
+
details={"error": str(e)}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _check_memory_issues(self) -> DiagnosticResult:
|
|
167
|
+
"""Check for potential memory leaks."""
|
|
168
|
+
memory_dir = Path.home() / ".claude" / "memory"
|
|
169
|
+
|
|
170
|
+
if not memory_dir.exists():
|
|
171
|
+
return DiagnosticResult(
|
|
172
|
+
category="Memory Usage",
|
|
173
|
+
status=DiagnosticStatus.OK,
|
|
174
|
+
message="No memory directory",
|
|
175
|
+
details={"exists": False}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Count and size memory files
|
|
180
|
+
memory_files = list(memory_dir.glob("**/*.json"))
|
|
181
|
+
total_size = sum(f.stat().st_size for f in memory_files)
|
|
182
|
+
size_mb = total_size / (1024 * 1024)
|
|
183
|
+
|
|
184
|
+
# Check for old memory files
|
|
185
|
+
import time
|
|
186
|
+
current_time = time.time()
|
|
187
|
+
old_files = []
|
|
188
|
+
for f in memory_files:
|
|
189
|
+
age_days = (current_time - f.stat().st_mtime) / (24 * 3600)
|
|
190
|
+
if age_days > 30:
|
|
191
|
+
old_files.append(f.name)
|
|
192
|
+
|
|
193
|
+
details = {
|
|
194
|
+
"file_count": len(memory_files),
|
|
195
|
+
"total_size_mb": round(size_mb, 2),
|
|
196
|
+
"old_files": len(old_files)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if size_mb > 100: # >100MB of memory files
|
|
200
|
+
return DiagnosticResult(
|
|
201
|
+
category="Memory Usage",
|
|
202
|
+
status=DiagnosticStatus.WARNING,
|
|
203
|
+
message=f"High memory usage: {size_mb:.1f}MB in {len(memory_files)} files",
|
|
204
|
+
details=details,
|
|
205
|
+
fix_command="claude-mpm memory clean --days 30",
|
|
206
|
+
fix_description="Clean up old memory files"
|
|
207
|
+
)
|
|
208
|
+
elif old_files:
|
|
209
|
+
return DiagnosticResult(
|
|
210
|
+
category="Memory Usage",
|
|
211
|
+
status=DiagnosticStatus.WARNING,
|
|
212
|
+
message=f"{len(old_files)} memory file(s) older than 30 days",
|
|
213
|
+
details=details,
|
|
214
|
+
fix_command="claude-mpm memory clean --days 30",
|
|
215
|
+
fix_description="Clean up old memory files"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return DiagnosticResult(
|
|
219
|
+
category="Memory Usage",
|
|
220
|
+
status=DiagnosticStatus.OK,
|
|
221
|
+
message=f"Memory usage normal ({size_mb:.1f}MB)",
|
|
222
|
+
details=details
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return DiagnosticResult(
|
|
227
|
+
category="Memory Usage",
|
|
228
|
+
status=DiagnosticStatus.WARNING,
|
|
229
|
+
message=f"Could not check memory: {str(e)}",
|
|
230
|
+
details={"error": str(e)}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _check_lock_files(self) -> DiagnosticResult:
|
|
234
|
+
"""Check for stale lock files."""
|
|
235
|
+
lock_locations = [
|
|
236
|
+
Path.home() / ".claude" / "*.lock",
|
|
237
|
+
Path.cwd() / ".claude" / "*.lock",
|
|
238
|
+
Path("/tmp") / "claude-mpm-*.lock"
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
stale_locks = []
|
|
242
|
+
|
|
243
|
+
for pattern in lock_locations:
|
|
244
|
+
try:
|
|
245
|
+
for lock_file in pattern.parent.glob(pattern.name):
|
|
246
|
+
# Check if lock is stale (older than 1 hour)
|
|
247
|
+
import time
|
|
248
|
+
age_hours = (time.time() - lock_file.stat().st_mtime) / 3600
|
|
249
|
+
if age_hours > 1:
|
|
250
|
+
stale_locks.append(str(lock_file))
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
if stale_locks:
|
|
255
|
+
return DiagnosticResult(
|
|
256
|
+
category="Lock Files",
|
|
257
|
+
status=DiagnosticStatus.WARNING,
|
|
258
|
+
message=f"{len(stale_locks)} stale lock file(s) found",
|
|
259
|
+
details={"stale_locks": stale_locks},
|
|
260
|
+
fix_command=f"rm {' '.join(stale_locks)}",
|
|
261
|
+
fix_description="Remove stale lock files"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return DiagnosticResult(
|
|
265
|
+
category="Lock Files",
|
|
266
|
+
status=DiagnosticStatus.OK,
|
|
267
|
+
message="No stale lock files",
|
|
268
|
+
details={"stale_locks": []}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _check_config_conflicts(self) -> DiagnosticResult:
|
|
272
|
+
"""Check for conflicting configurations."""
|
|
273
|
+
conflicts = []
|
|
274
|
+
|
|
275
|
+
# Check for both user and project configs with same settings
|
|
276
|
+
user_config = Path.home() / ".claude" / "claude-mpm.yaml"
|
|
277
|
+
project_config = Path.cwd() / ".claude" / "claude-mpm.yaml"
|
|
278
|
+
|
|
279
|
+
if user_config.exists() and project_config.exists():
|
|
280
|
+
try:
|
|
281
|
+
import yaml
|
|
282
|
+
|
|
283
|
+
with open(user_config, 'r') as f:
|
|
284
|
+
user_data = yaml.safe_load(f) or {}
|
|
285
|
+
|
|
286
|
+
with open(project_config, 'r') as f:
|
|
287
|
+
project_data = yaml.safe_load(f) or {}
|
|
288
|
+
|
|
289
|
+
# Check for conflicting keys
|
|
290
|
+
for key in user_data:
|
|
291
|
+
if key in project_data and user_data[key] != project_data[key]:
|
|
292
|
+
conflicts.append(f"'{key}' defined in both user and project config")
|
|
293
|
+
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
if conflicts:
|
|
298
|
+
return DiagnosticResult(
|
|
299
|
+
category="Config Conflicts",
|
|
300
|
+
status=DiagnosticStatus.WARNING,
|
|
301
|
+
message=f"{len(conflicts)} configuration conflict(s)",
|
|
302
|
+
details={"conflicts": conflicts},
|
|
303
|
+
fix_description="Resolve conflicting configurations"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return DiagnosticResult(
|
|
307
|
+
category="Config Conflicts",
|
|
308
|
+
status=DiagnosticStatus.OK,
|
|
309
|
+
message="No configuration conflicts",
|
|
310
|
+
details={"conflicts": []}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def _check_cache_issues(self) -> DiagnosticResult:
|
|
314
|
+
"""Check for cache-related issues."""
|
|
315
|
+
cache_dir = Path.home() / ".cache" / "claude-mpm"
|
|
316
|
+
|
|
317
|
+
if not cache_dir.exists():
|
|
318
|
+
return DiagnosticResult(
|
|
319
|
+
category="Cache",
|
|
320
|
+
status=DiagnosticStatus.OK,
|
|
321
|
+
message="No cache directory",
|
|
322
|
+
details={"exists": False}
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Check cache size
|
|
327
|
+
cache_files = list(cache_dir.glob("**/*"))
|
|
328
|
+
total_size = sum(f.stat().st_size for f in cache_files if f.is_file())
|
|
329
|
+
size_mb = total_size / (1024 * 1024)
|
|
330
|
+
|
|
331
|
+
if size_mb > 500: # >500MB cache
|
|
332
|
+
return DiagnosticResult(
|
|
333
|
+
category="Cache",
|
|
334
|
+
status=DiagnosticStatus.WARNING,
|
|
335
|
+
message=f"Large cache: {size_mb:.1f}MB",
|
|
336
|
+
details={"size_mb": round(size_mb, 2), "file_count": len(cache_files)},
|
|
337
|
+
fix_command=f"rm -rf {cache_dir}",
|
|
338
|
+
fix_description="Clear cache to free up space"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return DiagnosticResult(
|
|
342
|
+
category="Cache",
|
|
343
|
+
status=DiagnosticStatus.OK,
|
|
344
|
+
message=f"Cache size normal ({size_mb:.1f}MB)",
|
|
345
|
+
details={"size_mb": round(size_mb, 2), "file_count": len(cache_files)}
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
return DiagnosticResult(
|
|
350
|
+
category="Cache",
|
|
351
|
+
status=DiagnosticStatus.WARNING,
|
|
352
|
+
message=f"Could not check cache: {str(e)}",
|
|
353
|
+
details={"error": str(e)}
|
|
354
|
+
)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check claude-mpm configuration files.
|
|
3
|
+
|
|
4
|
+
WHY: Verify that user and project configurations are valid, accessible,
|
|
5
|
+
and properly structured.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from ..models import DiagnosticResult, DiagnosticStatus
|
|
15
|
+
from .base_check import BaseDiagnosticCheck
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigurationCheck(BaseDiagnosticCheck):
|
|
19
|
+
"""Check configuration files and settings."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "configuration_check"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def category(self) -> str:
|
|
27
|
+
return "Configuration"
|
|
28
|
+
|
|
29
|
+
def run(self) -> DiagnosticResult:
|
|
30
|
+
"""Run configuration diagnostics."""
|
|
31
|
+
try:
|
|
32
|
+
sub_results = []
|
|
33
|
+
details = {}
|
|
34
|
+
|
|
35
|
+
# Check user configuration
|
|
36
|
+
user_result = self._check_user_config()
|
|
37
|
+
sub_results.append(user_result)
|
|
38
|
+
details["user_config"] = user_result.details
|
|
39
|
+
|
|
40
|
+
# Check project configuration
|
|
41
|
+
project_result = self._check_project_config()
|
|
42
|
+
sub_results.append(project_result)
|
|
43
|
+
details["project_config"] = project_result.details
|
|
44
|
+
|
|
45
|
+
# Check environment variables
|
|
46
|
+
env_result = self._check_environment_variables()
|
|
47
|
+
sub_results.append(env_result)
|
|
48
|
+
details["environment"] = env_result.details
|
|
49
|
+
|
|
50
|
+
# Check configuration permissions
|
|
51
|
+
perm_result = self._check_config_permissions()
|
|
52
|
+
sub_results.append(perm_result)
|
|
53
|
+
|
|
54
|
+
# Determine overall status
|
|
55
|
+
if any(r.status == DiagnosticStatus.ERROR for r in sub_results):
|
|
56
|
+
status = DiagnosticStatus.ERROR
|
|
57
|
+
message = "Configuration has critical issues"
|
|
58
|
+
elif any(r.status == DiagnosticStatus.WARNING for r in sub_results):
|
|
59
|
+
status = DiagnosticStatus.WARNING
|
|
60
|
+
message = "Configuration has minor issues"
|
|
61
|
+
else:
|
|
62
|
+
status = DiagnosticStatus.OK
|
|
63
|
+
message = "Configuration is valid"
|
|
64
|
+
|
|
65
|
+
return DiagnosticResult(
|
|
66
|
+
category=self.category,
|
|
67
|
+
status=status,
|
|
68
|
+
message=message,
|
|
69
|
+
details=details,
|
|
70
|
+
sub_results=sub_results if self.verbose else []
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return DiagnosticResult(
|
|
75
|
+
category=self.category,
|
|
76
|
+
status=DiagnosticStatus.ERROR,
|
|
77
|
+
message=f"Configuration check failed: {str(e)}",
|
|
78
|
+
details={"error": str(e)}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _check_user_config(self) -> DiagnosticResult:
|
|
82
|
+
"""Check user-level configuration."""
|
|
83
|
+
config_path = Path.home() / ".claude" / "claude-mpm.yaml"
|
|
84
|
+
|
|
85
|
+
if not config_path.exists():
|
|
86
|
+
return DiagnosticResult(
|
|
87
|
+
category="User Config",
|
|
88
|
+
status=DiagnosticStatus.OK,
|
|
89
|
+
message="No user configuration (using defaults)",
|
|
90
|
+
details={"path": str(config_path), "exists": False}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
with open(config_path, 'r') as f:
|
|
95
|
+
config = yaml.safe_load(f)
|
|
96
|
+
|
|
97
|
+
issues = self._validate_config_structure(config)
|
|
98
|
+
if issues:
|
|
99
|
+
return DiagnosticResult(
|
|
100
|
+
category="User Config",
|
|
101
|
+
status=DiagnosticStatus.WARNING,
|
|
102
|
+
message=f"User config has issues: {', '.join(issues)}",
|
|
103
|
+
details={"path": str(config_path), "issues": issues}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return DiagnosticResult(
|
|
107
|
+
category="User Config",
|
|
108
|
+
status=DiagnosticStatus.OK,
|
|
109
|
+
message="User configuration valid",
|
|
110
|
+
details={
|
|
111
|
+
"path": str(config_path),
|
|
112
|
+
"size": config_path.stat().st_size,
|
|
113
|
+
"exists": True
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
except yaml.YAMLError as e:
|
|
118
|
+
return DiagnosticResult(
|
|
119
|
+
category="User Config",
|
|
120
|
+
status=DiagnosticStatus.ERROR,
|
|
121
|
+
message="User config has invalid YAML",
|
|
122
|
+
details={"path": str(config_path), "error": str(e)},
|
|
123
|
+
fix_description="Fix YAML syntax errors in the configuration file"
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return DiagnosticResult(
|
|
127
|
+
category="User Config",
|
|
128
|
+
status=DiagnosticStatus.WARNING,
|
|
129
|
+
message=f"Could not read user config: {str(e)}",
|
|
130
|
+
details={"path": str(config_path), "error": str(e)}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _check_project_config(self) -> DiagnosticResult:
|
|
134
|
+
"""Check project-level configuration."""
|
|
135
|
+
config_path = Path.cwd() / ".claude" / "claude-mpm.yaml"
|
|
136
|
+
|
|
137
|
+
if not config_path.exists():
|
|
138
|
+
return DiagnosticResult(
|
|
139
|
+
category="Project Config",
|
|
140
|
+
status=DiagnosticStatus.OK,
|
|
141
|
+
message="No project configuration (using defaults)",
|
|
142
|
+
details={"path": str(config_path), "exists": False}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with open(config_path, 'r') as f:
|
|
147
|
+
config = yaml.safe_load(f)
|
|
148
|
+
|
|
149
|
+
issues = self._validate_config_structure(config)
|
|
150
|
+
if issues:
|
|
151
|
+
return DiagnosticResult(
|
|
152
|
+
category="Project Config",
|
|
153
|
+
status=DiagnosticStatus.WARNING,
|
|
154
|
+
message=f"Project config has issues: {', '.join(issues)}",
|
|
155
|
+
details={"path": str(config_path), "issues": issues}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Check for deprecated keys
|
|
159
|
+
deprecated = self._check_deprecated_keys(config)
|
|
160
|
+
if deprecated:
|
|
161
|
+
return DiagnosticResult(
|
|
162
|
+
category="Project Config",
|
|
163
|
+
status=DiagnosticStatus.WARNING,
|
|
164
|
+
message=f"Using deprecated config keys: {', '.join(deprecated)}",
|
|
165
|
+
details={
|
|
166
|
+
"path": str(config_path),
|
|
167
|
+
"deprecated_keys": deprecated
|
|
168
|
+
},
|
|
169
|
+
fix_description="Remove deprecated configuration keys"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return DiagnosticResult(
|
|
173
|
+
category="Project Config",
|
|
174
|
+
status=DiagnosticStatus.OK,
|
|
175
|
+
message="Project configuration valid",
|
|
176
|
+
details={
|
|
177
|
+
"path": str(config_path),
|
|
178
|
+
"size": config_path.stat().st_size,
|
|
179
|
+
"exists": True
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except yaml.YAMLError as e:
|
|
184
|
+
return DiagnosticResult(
|
|
185
|
+
category="Project Config",
|
|
186
|
+
status=DiagnosticStatus.ERROR,
|
|
187
|
+
message="Project config has invalid YAML",
|
|
188
|
+
details={"path": str(config_path), "error": str(e)},
|
|
189
|
+
fix_description="Fix YAML syntax errors in the configuration file"
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return DiagnosticResult(
|
|
193
|
+
category="Project Config",
|
|
194
|
+
status=DiagnosticStatus.WARNING,
|
|
195
|
+
message=f"Could not read project config: {str(e)}",
|
|
196
|
+
details={"path": str(config_path), "error": str(e)}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _check_environment_variables(self) -> DiagnosticResult:
|
|
200
|
+
"""Check relevant environment variables."""
|
|
201
|
+
env_vars = {
|
|
202
|
+
"CLAUDE_MPM_CONFIG": os.environ.get("CLAUDE_MPM_CONFIG"),
|
|
203
|
+
"CLAUDE_MPM_LOG_LEVEL": os.environ.get("CLAUDE_MPM_LOG_LEVEL"),
|
|
204
|
+
"CLAUDE_MPM_PROJECT_DIR": os.environ.get("CLAUDE_MPM_PROJECT_DIR"),
|
|
205
|
+
"PYTHONPATH": os.environ.get("PYTHONPATH"),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
set_vars = {k: v for k, v in env_vars.items() if v is not None}
|
|
209
|
+
|
|
210
|
+
if not set_vars:
|
|
211
|
+
return DiagnosticResult(
|
|
212
|
+
category="Environment Variables",
|
|
213
|
+
status=DiagnosticStatus.OK,
|
|
214
|
+
message="No claude-mpm environment variables set",
|
|
215
|
+
details={"variables": {}}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return DiagnosticResult(
|
|
219
|
+
category="Environment Variables",
|
|
220
|
+
status=DiagnosticStatus.OK,
|
|
221
|
+
message=f"{len(set_vars)} environment variable(s) configured",
|
|
222
|
+
details={"variables": set_vars}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _check_config_permissions(self) -> DiagnosticResult:
|
|
226
|
+
"""Check configuration file permissions."""
|
|
227
|
+
issues = []
|
|
228
|
+
paths_checked = []
|
|
229
|
+
|
|
230
|
+
for config_path in [
|
|
231
|
+
Path.home() / ".claude" / "claude-mpm.yaml",
|
|
232
|
+
Path.cwd() / ".claude" / "claude-mpm.yaml"
|
|
233
|
+
]:
|
|
234
|
+
if config_path.exists():
|
|
235
|
+
paths_checked.append(str(config_path))
|
|
236
|
+
|
|
237
|
+
# Check readability
|
|
238
|
+
if not os.access(config_path, os.R_OK):
|
|
239
|
+
issues.append(f"{config_path.name} is not readable")
|
|
240
|
+
|
|
241
|
+
# Check if world-writable (security concern)
|
|
242
|
+
stat_info = config_path.stat()
|
|
243
|
+
if stat_info.st_mode & 0o002:
|
|
244
|
+
issues.append(f"{config_path.name} is world-writable (security risk)")
|
|
245
|
+
|
|
246
|
+
if not paths_checked:
|
|
247
|
+
return DiagnosticResult(
|
|
248
|
+
category="Config Permissions",
|
|
249
|
+
status=DiagnosticStatus.OK,
|
|
250
|
+
message="No configuration files to check",
|
|
251
|
+
details={"paths_checked": []}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if issues:
|
|
255
|
+
return DiagnosticResult(
|
|
256
|
+
category="Config Permissions",
|
|
257
|
+
status=DiagnosticStatus.WARNING,
|
|
258
|
+
message=f"Permission issues: {', '.join(issues)}",
|
|
259
|
+
details={"issues": issues, "paths_checked": paths_checked},
|
|
260
|
+
fix_command="chmod 644 ~/.claude/claude-mpm.yaml",
|
|
261
|
+
fix_description="Fix file permissions for configuration files"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return DiagnosticResult(
|
|
265
|
+
category="Config Permissions",
|
|
266
|
+
status=DiagnosticStatus.OK,
|
|
267
|
+
message="Configuration file permissions are correct",
|
|
268
|
+
details={"paths_checked": paths_checked}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _validate_config_structure(self, config: Dict[str, Any]) -> list:
|
|
272
|
+
"""Validate configuration structure and return issues."""
|
|
273
|
+
issues = []
|
|
274
|
+
|
|
275
|
+
if not isinstance(config, dict):
|
|
276
|
+
issues.append("Configuration is not a dictionary")
|
|
277
|
+
return issues
|
|
278
|
+
|
|
279
|
+
# Check for invalid top-level keys
|
|
280
|
+
valid_keys = {
|
|
281
|
+
"response_logging", "agent_deployment", "memory_management",
|
|
282
|
+
"monitoring", "mcp", "hooks", "paths", "debug"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
invalid_keys = set(config.keys()) - valid_keys
|
|
286
|
+
if invalid_keys:
|
|
287
|
+
issues.append(f"Unknown keys: {', '.join(invalid_keys)}")
|
|
288
|
+
|
|
289
|
+
return issues
|
|
290
|
+
|
|
291
|
+
def _check_deprecated_keys(self, config: Dict[str, Any]) -> list:
|
|
292
|
+
"""Check for deprecated configuration keys."""
|
|
293
|
+
deprecated_keys = ["legacy_mode", "old_agent_format", "deprecated_logging"]
|
|
294
|
+
found_deprecated = []
|
|
295
|
+
|
|
296
|
+
for key in deprecated_keys:
|
|
297
|
+
if key in config:
|
|
298
|
+
found_deprecated.append(key)
|
|
299
|
+
|
|
300
|
+
return found_deprecated
|