claude-mpm 4.3.19__py3-none-any.whl → 4.3.22__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.
Files changed (76) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/agent_loader.py +2 -2
  3. claude_mpm/agents/agent_loader_integration.py +2 -2
  4. claude_mpm/agents/async_agent_loader.py +2 -2
  5. claude_mpm/agents/base_agent_loader.py +2 -2
  6. claude_mpm/agents/frontmatter_validator.py +2 -2
  7. claude_mpm/agents/system_agent_config.py +2 -2
  8. claude_mpm/agents/templates/clerk-ops.json +6 -4
  9. claude_mpm/agents/templates/data_engineer.json +1 -2
  10. claude_mpm/cli/commands/doctor.py +2 -2
  11. claude_mpm/cli/commands/mpm_init.py +560 -47
  12. claude_mpm/cli/commands/mpm_init_handler.py +6 -0
  13. claude_mpm/cli/parsers/mpm_init_parser.py +39 -1
  14. claude_mpm/cli/startup_logging.py +11 -9
  15. claude_mpm/commands/mpm-init.md +76 -12
  16. claude_mpm/config/agent_config.py +2 -2
  17. claude_mpm/config/paths.py +2 -2
  18. claude_mpm/core/agent_name_normalizer.py +2 -2
  19. claude_mpm/core/config.py +2 -1
  20. claude_mpm/core/config_aliases.py +2 -2
  21. claude_mpm/core/file_utils.py +1 -0
  22. claude_mpm/core/log_manager.py +2 -2
  23. claude_mpm/core/tool_access_control.py +2 -2
  24. claude_mpm/core/unified_agent_registry.py +2 -2
  25. claude_mpm/core/unified_paths.py +2 -2
  26. claude_mpm/experimental/cli_enhancements.py +3 -2
  27. claude_mpm/hooks/base_hook.py +2 -2
  28. claude_mpm/hooks/instruction_reinforcement.py +2 -2
  29. claude_mpm/hooks/validation_hooks.py +2 -2
  30. claude_mpm/scripts/mpm_doctor.py +2 -2
  31. claude_mpm/services/agents/loading/agent_profile_loader.py +2 -2
  32. claude_mpm/services/agents/loading/base_agent_manager.py +2 -2
  33. claude_mpm/services/agents/loading/framework_agent_loader.py +2 -2
  34. claude_mpm/services/agents/management/agent_capabilities_generator.py +2 -2
  35. claude_mpm/services/agents/management/agent_management_service.py +2 -2
  36. claude_mpm/services/agents/memory/memory_categorization_service.py +5 -2
  37. claude_mpm/services/agents/memory/memory_file_service.py +27 -6
  38. claude_mpm/services/agents/memory/memory_format_service.py +5 -2
  39. claude_mpm/services/agents/memory/memory_limits_service.py +3 -2
  40. claude_mpm/services/agents/registry/deployed_agent_discovery.py +2 -2
  41. claude_mpm/services/agents/registry/modification_tracker.py +4 -4
  42. claude_mpm/services/async_session_logger.py +2 -1
  43. claude_mpm/services/claude_session_logger.py +2 -2
  44. claude_mpm/services/core/path_resolver.py +3 -2
  45. claude_mpm/services/diagnostics/diagnostic_runner.py +4 -3
  46. claude_mpm/services/event_bus/direct_relay.py +2 -1
  47. claude_mpm/services/event_bus/event_bus.py +2 -1
  48. claude_mpm/services/event_bus/relay.py +2 -2
  49. claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
  50. claude_mpm/services/infrastructure/daemon_manager.py +2 -2
  51. claude_mpm/services/memory/cache/simple_cache.py +2 -2
  52. claude_mpm/services/project/archive_manager.py +981 -0
  53. claude_mpm/services/project/documentation_manager.py +536 -0
  54. claude_mpm/services/project/enhanced_analyzer.py +491 -0
  55. claude_mpm/services/project/project_organizer.py +904 -0
  56. claude_mpm/services/response_tracker.py +2 -2
  57. claude_mpm/services/socketio/handlers/connection.py +14 -33
  58. claude_mpm/services/socketio/server/eventbus_integration.py +2 -2
  59. claude_mpm/services/version_control/version_parser.py +5 -4
  60. claude_mpm/storage/state_storage.py +2 -2
  61. claude_mpm/utils/agent_dependency_loader.py +49 -0
  62. claude_mpm/utils/common.py +542 -0
  63. claude_mpm/utils/database_connector.py +298 -0
  64. claude_mpm/utils/error_handler.py +2 -1
  65. claude_mpm/utils/log_cleanup.py +2 -2
  66. claude_mpm/utils/path_operations.py +2 -2
  67. claude_mpm/utils/robust_installer.py +56 -0
  68. claude_mpm/utils/session_logging.py +2 -2
  69. claude_mpm/utils/subprocess_utils.py +2 -2
  70. claude_mpm/validation/agent_validator.py +2 -2
  71. {claude_mpm-4.3.19.dist-info → claude_mpm-4.3.22.dist-info}/METADATA +1 -1
  72. {claude_mpm-4.3.19.dist-info → claude_mpm-4.3.22.dist-info}/RECORD +76 -70
  73. {claude_mpm-4.3.19.dist-info → claude_mpm-4.3.22.dist-info}/WHEEL +0 -0
  74. {claude_mpm-4.3.19.dist-info → claude_mpm-4.3.22.dist-info}/entry_points.txt +0 -0
  75. {claude_mpm-4.3.19.dist-info → claude_mpm-4.3.22.dist-info}/licenses/LICENSE +0 -0
  76. {claude_mpm-4.3.19.dist-info → claude_mpm-4.3.22.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,491 @@
1
+ """
2
+ Enhanced Project Analyzer with Git History Support
3
+ =================================================
4
+
5
+ This service extends project analysis with git history analysis,
6
+ recent changes tracking, and intelligent project state detection.
7
+
8
+ Key Features:
9
+ - Git history analysis and recent commits
10
+ - Change frequency detection
11
+ - Author contribution analysis
12
+ - File modification tracking
13
+ - Project lifecycle detection
14
+
15
+ Author: Claude MPM Development Team
16
+ Created: 2025-01-26
17
+ """
18
+
19
+ import json
20
+ import subprocess
21
+ from datetime import datetime, timedelta
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional, Set, Tuple
24
+
25
+ from rich.console import Console
26
+
27
+ from claude_mpm.core.logging_utils import get_logger
28
+ logger = get_logger(__name__)
29
+ console = Console()
30
+
31
+
32
+ class EnhancedProjectAnalyzer:
33
+ """Enhanced project analyzer with git history support."""
34
+
35
+ def __init__(self, project_path: Path):
36
+ """Initialize the enhanced analyzer."""
37
+ self.project_path = project_path
38
+ self.is_git_repo = (project_path / ".git").exists()
39
+
40
+ def analyze_git_history(self, days_back: int = 30) -> Dict:
41
+ """Analyze git history for recent changes and patterns."""
42
+ if not self.is_git_repo:
43
+ return {"git_available": False, "message": "Not a git repository"}
44
+
45
+ analysis = {
46
+ "git_available": True,
47
+ "recent_commits": self._get_recent_commits(days_back),
48
+ "changed_files": self._get_changed_files(days_back),
49
+ "authors": self._get_author_stats(days_back),
50
+ "branch_info": self._get_branch_info(),
51
+ "documentation_changes": self._get_documentation_changes(days_back),
52
+ }
53
+
54
+ # Analyze patterns
55
+ analysis["patterns"] = self._analyze_commit_patterns(analysis["recent_commits"])
56
+ analysis["hot_spots"] = self._identify_hot_spots(analysis["changed_files"])
57
+
58
+ return analysis
59
+
60
+ def _run_git_command(self, args: List[str]) -> Optional[str]:
61
+ """Run a git command and return output."""
62
+ try:
63
+ result = subprocess.run(
64
+ ["git"] + args,
65
+ cwd=self.project_path,
66
+ capture_output=True,
67
+ text=True,
68
+ check=True,
69
+ )
70
+ return result.stdout.strip()
71
+ except subprocess.CalledProcessError as e:
72
+ logger.debug(f"Git command failed: {e}")
73
+ return None
74
+
75
+ def _get_recent_commits(self, days: int) -> List[Dict]:
76
+ """Get recent commits within specified days."""
77
+ since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
78
+
79
+ # Get commit log with structured format
80
+ output = self._run_git_command([
81
+ "log",
82
+ f"--since={since_date}",
83
+ "--pretty=format:%H|%an|%ae|%at|%s",
84
+ "--no-merges",
85
+ ])
86
+
87
+ if not output:
88
+ return []
89
+
90
+ commits = []
91
+ for line in output.splitlines():
92
+ parts = line.split("|", 4)
93
+ if len(parts) == 5:
94
+ commits.append({
95
+ "hash": parts[0][:8],
96
+ "author": parts[1],
97
+ "email": parts[2],
98
+ "timestamp": datetime.fromtimestamp(int(parts[3])).isoformat(),
99
+ "message": parts[4],
100
+ })
101
+
102
+ return commits[:50] # Limit to 50 most recent
103
+
104
+ def _get_changed_files(self, days: int) -> Dict:
105
+ """Get files changed in recent commits."""
106
+ since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
107
+
108
+ output = self._run_git_command([
109
+ "log",
110
+ f"--since={since_date}",
111
+ "--pretty=format:",
112
+ "--name-only",
113
+ "--no-merges",
114
+ ])
115
+
116
+ if not output:
117
+ return {}
118
+
119
+ file_changes = {}
120
+ for line in output.splitlines():
121
+ if line:
122
+ file_changes[line] = file_changes.get(line, 0) + 1
123
+
124
+ # Sort by change frequency
125
+ sorted_files = sorted(file_changes.items(), key=lambda x: x[1], reverse=True)
126
+
127
+ return {
128
+ "total_files": len(file_changes),
129
+ "most_changed": dict(sorted_files[:20]), # Top 20 most changed
130
+ "recently_added": self._get_recently_added_files(days),
131
+ }
132
+
133
+ def _get_recently_added_files(self, days: int) -> List[str]:
134
+ """Get files added in recent commits."""
135
+ since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
136
+
137
+ output = self._run_git_command([
138
+ "log",
139
+ f"--since={since_date}",
140
+ "--pretty=format:",
141
+ "--name-status",
142
+ "--diff-filter=A", # Added files only
143
+ ])
144
+
145
+ if not output:
146
+ return []
147
+
148
+ added_files = []
149
+ for line in output.splitlines():
150
+ if line.startswith("A\t"):
151
+ added_files.append(line[2:])
152
+
153
+ return list(set(added_files))[:20] # Unique files, max 20
154
+
155
+ def _get_author_stats(self, days: int) -> Dict:
156
+ """Get author contribution statistics."""
157
+ since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
158
+
159
+ output = self._run_git_command([
160
+ "shortlog",
161
+ "-sne",
162
+ f"--since={since_date}",
163
+ "--no-merges",
164
+ ])
165
+
166
+ if not output:
167
+ return {}
168
+
169
+ authors = {}
170
+ for line in output.splitlines():
171
+ parts = line.strip().split("\t", 1)
172
+ if len(parts) == 2:
173
+ count = int(parts[0])
174
+ author_info = parts[1]
175
+ authors[author_info] = count
176
+
177
+ return {
178
+ "total_authors": len(authors),
179
+ "contributors": dict(list(authors.items())[:10]), # Top 10 contributors
180
+ }
181
+
182
+ def _get_branch_info(self) -> Dict:
183
+ """Get current branch and remote information."""
184
+ info = {}
185
+
186
+ # Current branch
187
+ branch = self._run_git_command(["branch", "--show-current"])
188
+ info["current_branch"] = branch or "unknown"
189
+
190
+ # All branches
191
+ branches = self._run_git_command(["branch", "-a"])
192
+ if branches:
193
+ info["branches"] = [
194
+ b.strip().replace("* ", "") for b in branches.splitlines()
195
+ ]
196
+
197
+ # Remote info
198
+ remotes = self._run_git_command(["remote", "-v"])
199
+ if remotes:
200
+ info["remotes"] = []
201
+ for line in remotes.splitlines():
202
+ parts = line.split()
203
+ if len(parts) >= 2:
204
+ info["remotes"].append({
205
+ "name": parts[0],
206
+ "url": parts[1],
207
+ })
208
+
209
+ # Check for uncommitted changes
210
+ status = self._run_git_command(["status", "--porcelain"])
211
+ info["has_uncommitted_changes"] = bool(status)
212
+ if status:
213
+ info["uncommitted_files"] = len(status.splitlines())
214
+
215
+ return info
216
+
217
+ def _get_documentation_changes(self, days: int) -> Dict:
218
+ """Track changes to documentation files."""
219
+ since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
220
+
221
+ # Get changes to documentation files
222
+ doc_patterns = ["*.md", "*.rst", "*.txt", "docs/*", "README*", "CLAUDE*"]
223
+
224
+ doc_changes = {}
225
+ for pattern in doc_patterns:
226
+ output = self._run_git_command([
227
+ "log",
228
+ f"--since={since_date}",
229
+ "--pretty=format:%H|%s",
230
+ "--", pattern,
231
+ ])
232
+
233
+ if output:
234
+ for line in output.splitlines():
235
+ parts = line.split("|", 1)
236
+ if len(parts) == 2:
237
+ if pattern not in doc_changes:
238
+ doc_changes[pattern] = []
239
+ doc_changes[pattern].append({
240
+ "commit": parts[0][:8],
241
+ "message": parts[1],
242
+ })
243
+
244
+ # Check CLAUDE.md specifically
245
+ claude_history = self._run_git_command([
246
+ "log",
247
+ f"--since={since_date}",
248
+ "--pretty=format:%H|%at|%s",
249
+ "--", "CLAUDE.md",
250
+ ])
251
+
252
+ claude_updates = []
253
+ if claude_history:
254
+ for line in claude_history.splitlines():
255
+ parts = line.split("|", 2)
256
+ if len(parts) == 3:
257
+ claude_updates.append({
258
+ "commit": parts[0][:8],
259
+ "timestamp": datetime.fromtimestamp(int(parts[1])).isoformat(),
260
+ "message": parts[2],
261
+ })
262
+
263
+ return {
264
+ "documentation_commits": doc_changes,
265
+ "claude_md_updates": claude_updates,
266
+ "has_recent_doc_changes": bool(doc_changes or claude_updates),
267
+ }
268
+
269
+ def _analyze_commit_patterns(self, commits: List[Dict]) -> Dict:
270
+ """Analyze patterns in commit messages."""
271
+ patterns = {
272
+ "features": [],
273
+ "fixes": [],
274
+ "refactoring": [],
275
+ "documentation": [],
276
+ "tests": [],
277
+ "chores": [],
278
+ }
279
+
280
+ for commit in commits:
281
+ msg_lower = commit["message"].lower()
282
+
283
+ if any(kw in msg_lower for kw in ["feat", "feature", "add", "implement"]):
284
+ patterns["features"].append(commit["message"][:100])
285
+ elif any(kw in msg_lower for kw in ["fix", "bug", "resolve", "patch"]):
286
+ patterns["fixes"].append(commit["message"][:100])
287
+ elif any(kw in msg_lower for kw in ["refactor", "restructure", "reorganize"]):
288
+ patterns["refactoring"].append(commit["message"][:100])
289
+ elif any(kw in msg_lower for kw in ["doc", "readme", "comment"]):
290
+ patterns["documentation"].append(commit["message"][:100])
291
+ elif any(kw in msg_lower for kw in ["test", "spec", "coverage"]):
292
+ patterns["tests"].append(commit["message"][:100])
293
+ elif any(kw in msg_lower for kw in ["chore", "build", "ci", "deps"]):
294
+ patterns["chores"].append(commit["message"][:100])
295
+
296
+ # Limit each category to 5 items
297
+ for key in patterns:
298
+ patterns[key] = patterns[key][:5]
299
+
300
+ # Add summary
301
+ patterns["summary"] = {
302
+ "total_commits": len(commits),
303
+ "feature_commits": len(patterns["features"]),
304
+ "bug_fixes": len(patterns["fixes"]),
305
+ "most_active_type": max(
306
+ patterns.keys(),
307
+ key=lambda k: len(patterns[k]) if k != "summary" else 0,
308
+ ),
309
+ }
310
+
311
+ return patterns
312
+
313
+ def _identify_hot_spots(self, changed_files: Dict) -> List[Dict]:
314
+ """Identify hot spots (frequently changed files)."""
315
+ if not changed_files.get("most_changed"):
316
+ return []
317
+
318
+ hot_spots = []
319
+ for file_path, change_count in list(changed_files["most_changed"].items())[:10]:
320
+ file_type = Path(file_path).suffix
321
+ hot_spots.append({
322
+ "file": file_path,
323
+ "changes": change_count,
324
+ "type": file_type,
325
+ "category": self._categorize_file(file_path),
326
+ })
327
+
328
+ return hot_spots
329
+
330
+ def _categorize_file(self, file_path: str) -> str:
331
+ """Categorize file based on path and extension."""
332
+ path = Path(file_path)
333
+
334
+ # Check directory
335
+ if "test" in str(path).lower():
336
+ return "test"
337
+ elif "docs" in str(path).lower():
338
+ return "documentation"
339
+ elif "src" in str(path) or "lib" in str(path):
340
+ return "source"
341
+ elif "scripts" in str(path):
342
+ return "scripts"
343
+ elif path.suffix in [".yml", ".yaml", ".json", ".toml", ".ini"]:
344
+ return "configuration"
345
+ elif path.suffix in [".md", ".rst", ".txt"]:
346
+ return "documentation"
347
+ else:
348
+ return "other"
349
+
350
+ def detect_project_state(self) -> Dict:
351
+ """Detect the current state and lifecycle phase of the project."""
352
+ state = {
353
+ "phase": "unknown",
354
+ "indicators": [],
355
+ "recommendations": [],
356
+ }
357
+
358
+ # Check various indicators
359
+ indicators = []
360
+
361
+ # Check for version files
362
+ version_files = ["VERSION", "version.txt", "package.json", "pyproject.toml"]
363
+ for vf in version_files:
364
+ if (self.project_path / vf).exists():
365
+ indicators.append(f"Has {vf} file")
366
+
367
+ # Check for CI/CD
368
+ if (self.project_path / ".github" / "workflows").exists():
369
+ indicators.append("Has GitHub Actions")
370
+ if (self.project_path / ".gitlab-ci.yml").exists():
371
+ indicators.append("Has GitLab CI")
372
+
373
+ # Check for tests
374
+ if (self.project_path / "tests").exists() or (self.project_path / "test").exists():
375
+ indicators.append("Has test directory")
376
+
377
+ # Check for documentation
378
+ if (self.project_path / "docs").exists():
379
+ indicators.append("Has documentation directory")
380
+ if (self.project_path / "CLAUDE.md").exists():
381
+ indicators.append("Has CLAUDE.md")
382
+
383
+ # Check git history if available
384
+ if self.is_git_repo:
385
+ # Count total commits
386
+ commit_count = self._run_git_command(["rev-list", "--count", "HEAD"])
387
+ if commit_count:
388
+ count = int(commit_count)
389
+ indicators.append(f"{count} total commits")
390
+
391
+ # Determine phase based on commit count
392
+ if count < 10:
393
+ state["phase"] = "initial"
394
+ state["recommendations"].append("Focus on establishing core structure")
395
+ elif count < 50:
396
+ state["phase"] = "early_development"
397
+ state["recommendations"].append("Consider adding tests and documentation")
398
+ elif count < 200:
399
+ state["phase"] = "active_development"
400
+ state["recommendations"].append("Ensure CI/CD and testing are in place")
401
+ elif count < 1000:
402
+ state["phase"] = "maturing"
403
+ state["recommendations"].append("Focus on optimization and documentation")
404
+ else:
405
+ state["phase"] = "mature"
406
+ state["recommendations"].append("Maintain backward compatibility")
407
+
408
+ # Check age
409
+ first_commit = self._run_git_command([
410
+ "log", "--reverse", "--format=%at", "-1"
411
+ ])
412
+ if first_commit:
413
+ age_days = (datetime.now() - datetime.fromtimestamp(int(first_commit))).days
414
+ indicators.append(f"{age_days} days old")
415
+
416
+ state["indicators"] = indicators
417
+
418
+ # Add phase-specific recommendations
419
+ if not (self.project_path / "CLAUDE.md").exists():
420
+ state["recommendations"].append("Create CLAUDE.md for AI agent documentation")
421
+ if not (self.project_path / "tests").exists():
422
+ state["recommendations"].append("Add tests directory for test organization")
423
+ if not (self.project_path / ".gitignore").exists():
424
+ state["recommendations"].append("Create .gitignore file")
425
+
426
+ return state
427
+
428
+ def generate_analysis_report(self, include_git: bool = True) -> Dict:
429
+ """Generate comprehensive project analysis report."""
430
+ report = {
431
+ "project_path": str(self.project_path),
432
+ "timestamp": datetime.now().isoformat(),
433
+ }
434
+
435
+ # Basic project info
436
+ report["project_info"] = {
437
+ "name": self.project_path.name,
438
+ "is_git_repo": self.is_git_repo,
439
+ }
440
+
441
+ # Project state
442
+ report["state"] = self.detect_project_state()
443
+
444
+ # Git analysis if available
445
+ if include_git and self.is_git_repo:
446
+ report["git_analysis"] = self.analyze_git_history()
447
+
448
+ # File statistics
449
+ report["statistics"] = self._get_project_statistics()
450
+
451
+ return report
452
+
453
+ def _get_project_statistics(self) -> Dict:
454
+ """Get basic project statistics."""
455
+ stats = {
456
+ "total_files": 0,
457
+ "total_directories": 0,
458
+ "file_types": {},
459
+ "largest_files": [],
460
+ }
461
+
462
+ # Count files and directories
463
+ for path in self.project_path.rglob("*"):
464
+ # Skip hidden and git files
465
+ if any(part.startswith(".") for part in path.parts):
466
+ continue
467
+
468
+ if path.is_file():
469
+ stats["total_files"] += 1
470
+ ext = path.suffix or "no_extension"
471
+ stats["file_types"][ext] = stats["file_types"].get(ext, 0) + 1
472
+
473
+ # Track large files
474
+ try:
475
+ size = path.stat().st_size
476
+ if size > 1024 * 1024: # Files over 1MB
477
+ stats["largest_files"].append({
478
+ "path": str(path.relative_to(self.project_path)),
479
+ "size_mb": round(size / (1024 * 1024), 2),
480
+ })
481
+ except (OSError, PermissionError):
482
+ pass
483
+
484
+ elif path.is_dir():
485
+ stats["total_directories"] += 1
486
+
487
+ # Sort largest files
488
+ stats["largest_files"].sort(key=lambda x: x["size_mb"], reverse=True)
489
+ stats["largest_files"] = stats["largest_files"][:10] # Top 10
490
+
491
+ return stats