claude-mpm 4.1.8__py3-none-any.whl → 4.1.10__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 (103) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +26 -1
  3. claude_mpm/agents/agents_metadata.py +57 -0
  4. claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
  5. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
  6. claude_mpm/agents/templates/agent-manager.json +263 -17
  7. claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
  8. claude_mpm/agents/templates/code_analyzer.json +18 -8
  9. claude_mpm/agents/templates/engineer.json +1 -1
  10. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
  11. claude_mpm/agents/templates/qa.json +1 -1
  12. claude_mpm/agents/templates/research.json +1 -1
  13. claude_mpm/cli/__init__.py +4 -0
  14. claude_mpm/cli/commands/__init__.py +6 -0
  15. claude_mpm/cli/commands/analyze.py +547 -0
  16. claude_mpm/cli/commands/analyze_code.py +524 -0
  17. claude_mpm/cli/commands/configure.py +77 -28
  18. claude_mpm/cli/commands/configure_tui.py +60 -60
  19. claude_mpm/cli/commands/debug.py +1387 -0
  20. claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
  21. claude_mpm/cli/parsers/analyze_parser.py +135 -0
  22. claude_mpm/cli/parsers/base_parser.py +29 -0
  23. claude_mpm/cli/parsers/debug_parser.py +319 -0
  24. claude_mpm/constants.py +3 -1
  25. claude_mpm/core/framework_loader.py +148 -6
  26. claude_mpm/core/log_manager.py +16 -13
  27. claude_mpm/core/logger.py +1 -1
  28. claude_mpm/core/unified_agent_registry.py +1 -1
  29. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
  30. claude_mpm/dashboard/analysis_runner.py +428 -0
  31. claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
  32. claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
  33. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  34. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
  35. claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
  36. claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
  37. claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
  38. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  39. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  40. claude_mpm/dashboard/static/css/activity.css +549 -0
  41. claude_mpm/dashboard/static/css/code-tree.css +846 -0
  42. claude_mpm/dashboard/static/css/dashboard.css +245 -0
  43. claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
  44. claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
  45. claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
  46. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  47. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  48. claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
  49. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  50. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  51. claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
  52. claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
  53. claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
  54. claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
  55. claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
  56. claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
  57. claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
  58. claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
  59. claude_mpm/dashboard/static/js/dashboard.js +39 -0
  60. claude_mpm/dashboard/static/js/socket-client.js +414 -20
  61. claude_mpm/dashboard/templates/index.html +184 -4
  62. claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
  63. claude_mpm/hooks/claude_hooks/installer.py +386 -113
  64. claude_mpm/scripts/claude-hook-handler.sh +161 -0
  65. claude_mpm/scripts/socketio_daemon.py +121 -8
  66. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
  67. claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
  68. claude_mpm/services/agents/memory/memory_format_service.py +1 -5
  69. claude_mpm/services/cli/agent_cleanup_service.py +1 -2
  70. claude_mpm/services/cli/agent_dependency_service.py +1 -1
  71. claude_mpm/services/cli/agent_validation_service.py +3 -4
  72. claude_mpm/services/cli/dashboard_launcher.py +2 -3
  73. claude_mpm/services/cli/startup_checker.py +0 -10
  74. claude_mpm/services/core/cache_manager.py +1 -2
  75. claude_mpm/services/core/path_resolver.py +1 -4
  76. claude_mpm/services/core/service_container.py +2 -2
  77. claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
  78. claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
  79. claude_mpm/services/infrastructure/monitoring.py +11 -11
  80. claude_mpm/services/project/architecture_analyzer.py +1 -1
  81. claude_mpm/services/project/dependency_analyzer.py +4 -4
  82. claude_mpm/services/project/language_analyzer.py +3 -3
  83. claude_mpm/services/project/metrics_collector.py +3 -6
  84. claude_mpm/services/socketio/handlers/__init__.py +2 -0
  85. claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
  86. claude_mpm/services/socketio/handlers/registry.py +2 -0
  87. claude_mpm/services/socketio/server/connection_manager.py +4 -4
  88. claude_mpm/services/socketio/server/core.py +100 -11
  89. claude_mpm/services/socketio/server/main.py +8 -2
  90. claude_mpm/services/visualization/__init__.py +19 -0
  91. claude_mpm/services/visualization/mermaid_generator.py +938 -0
  92. claude_mpm/tools/__main__.py +208 -0
  93. claude_mpm/tools/code_tree_analyzer.py +778 -0
  94. claude_mpm/tools/code_tree_builder.py +632 -0
  95. claude_mpm/tools/code_tree_events.py +318 -0
  96. claude_mpm/tools/socketio_debug.py +671 -0
  97. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
  98. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +102 -73
  99. claude_mpm/agents/schema/agent_schema.json +0 -314
  100. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
  101. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
  102. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
  103. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,778 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Code Tree Analyzer
4
+ ==================
5
+
6
+ WHY: Analyzes source code using AST to extract structure and metrics,
7
+ supporting multiple languages and emitting incremental events for visualization.
8
+
9
+ DESIGN DECISIONS:
10
+ - Use Python's ast module for Python files
11
+ - Use tree-sitter for multi-language support
12
+ - Extract comprehensive metadata (complexity, docstrings, etc.)
13
+ - Cache parsed results to avoid re-processing
14
+ - Support incremental processing with checkpoints
15
+ """
16
+
17
+ import ast
18
+ import hashlib
19
+ import json
20
+ import time
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional
24
+
25
+ try:
26
+ import tree_sitter
27
+ import tree_sitter_javascript
28
+ import tree_sitter_python
29
+ import tree_sitter_typescript
30
+
31
+ TREE_SITTER_AVAILABLE = True
32
+ except ImportError:
33
+ TREE_SITTER_AVAILABLE = False
34
+ tree_sitter = None
35
+
36
+ from ..core.logging_config import get_logger
37
+ from .code_tree_events import CodeNodeEvent, CodeTreeEventEmitter
38
+
39
+
40
+ @dataclass
41
+ class CodeNode:
42
+ """Represents a node in the code tree."""
43
+
44
+ file_path: str
45
+ node_type: str
46
+ name: str
47
+ line_start: int
48
+ line_end: int
49
+ complexity: int = 0
50
+ has_docstring: bool = False
51
+ decorators: List[str] = None
52
+ parent: Optional[str] = None
53
+ children: List["CodeNode"] = None
54
+ language: str = "python"
55
+ signature: str = ""
56
+ metrics: Dict[str, Any] = None
57
+
58
+ def __post_init__(self):
59
+ if self.decorators is None:
60
+ self.decorators = []
61
+ if self.children is None:
62
+ self.children = []
63
+ if self.metrics is None:
64
+ self.metrics = {}
65
+
66
+
67
+ class PythonAnalyzer:
68
+ """Analyzes Python source code using AST.
69
+
70
+ WHY: Python's built-in AST module provides rich structural information
71
+ that we can leverage for detailed analysis.
72
+ """
73
+
74
+ def __init__(self, emitter: Optional[CodeTreeEventEmitter] = None):
75
+ self.logger = get_logger(__name__)
76
+ self.emitter = emitter
77
+
78
+ def analyze_file(self, file_path: Path) -> List[CodeNode]:
79
+ """Analyze a Python file and extract code structure.
80
+
81
+ Args:
82
+ file_path: Path to Python file
83
+
84
+ Returns:
85
+ List of code nodes found in the file
86
+ """
87
+ nodes = []
88
+
89
+ try:
90
+ with open(file_path, encoding="utf-8") as f:
91
+ source = f.read()
92
+
93
+ tree = ast.parse(source, filename=str(file_path))
94
+ nodes = self._extract_nodes(tree, file_path, source)
95
+
96
+ except SyntaxError as e:
97
+ self.logger.warning(f"Syntax error in {file_path}: {e}")
98
+ if self.emitter:
99
+ self.emitter.emit_error(str(file_path), f"Syntax error: {e}")
100
+ except Exception as e:
101
+ self.logger.error(f"Error analyzing {file_path}: {e}")
102
+ if self.emitter:
103
+ self.emitter.emit_error(str(file_path), str(e))
104
+
105
+ return nodes
106
+
107
+ def _extract_nodes(
108
+ self, tree: ast.AST, file_path: Path, source: str
109
+ ) -> List[CodeNode]:
110
+ """Extract code nodes from AST tree.
111
+
112
+ Args:
113
+ tree: AST tree
114
+ file_path: Source file path
115
+ source: Source code text
116
+
117
+ Returns:
118
+ List of extracted code nodes
119
+ """
120
+ nodes = []
121
+ source.splitlines()
122
+
123
+ class NodeVisitor(ast.NodeVisitor):
124
+ def __init__(self, parent_name: Optional[str] = None):
125
+ self.parent_name = parent_name
126
+ self.current_class = None
127
+
128
+ def visit_ClassDef(self, node):
129
+ # Extract class information
130
+ class_node = CodeNode(
131
+ file_path=str(file_path),
132
+ node_type="class",
133
+ name=node.name,
134
+ line_start=node.lineno,
135
+ line_end=node.end_lineno or node.lineno,
136
+ has_docstring=bool(ast.get_docstring(node)),
137
+ decorators=[self._decorator_name(d) for d in node.decorator_list],
138
+ parent=self.parent_name,
139
+ complexity=self._calculate_complexity(node),
140
+ signature=self._get_class_signature(node),
141
+ )
142
+
143
+ nodes.append(class_node)
144
+
145
+ # Emit event if emitter is available
146
+ if self.emitter:
147
+ self.emitter.emit_node(
148
+ CodeNodeEvent(
149
+ file_path=str(file_path),
150
+ node_type="class",
151
+ name=node.name,
152
+ line_start=node.lineno,
153
+ line_end=node.end_lineno or node.lineno,
154
+ complexity=class_node.complexity,
155
+ has_docstring=class_node.has_docstring,
156
+ decorators=class_node.decorators,
157
+ parent=self.parent_name,
158
+ children_count=len(node.body),
159
+ )
160
+ )
161
+
162
+ # Visit class members
163
+ old_class = self.current_class
164
+ self.current_class = node.name
165
+ for child in node.body:
166
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
167
+ self.visit_FunctionDef(child, is_method=True)
168
+ self.current_class = old_class
169
+
170
+ def visit_FunctionDef(self, node, is_method=False):
171
+ # Determine node type
172
+ node_type = "method" if is_method else "function"
173
+ parent = self.current_class if is_method else self.parent_name
174
+
175
+ # Extract function information
176
+ func_node = CodeNode(
177
+ file_path=str(file_path),
178
+ node_type=node_type,
179
+ name=node.name,
180
+ line_start=node.lineno,
181
+ line_end=node.end_lineno or node.lineno,
182
+ has_docstring=bool(ast.get_docstring(node)),
183
+ decorators=[self._decorator_name(d) for d in node.decorator_list],
184
+ parent=parent,
185
+ complexity=self._calculate_complexity(node),
186
+ signature=self._get_function_signature(node),
187
+ )
188
+
189
+ nodes.append(func_node)
190
+
191
+ # Emit event if emitter is available
192
+ if self.emitter:
193
+ self.emitter.emit_node(
194
+ CodeNodeEvent(
195
+ file_path=str(file_path),
196
+ node_type=node_type,
197
+ name=node.name,
198
+ line_start=node.lineno,
199
+ line_end=node.end_lineno or node.lineno,
200
+ complexity=func_node.complexity,
201
+ has_docstring=func_node.has_docstring,
202
+ decorators=func_node.decorators,
203
+ parent=parent,
204
+ children_count=0,
205
+ )
206
+ )
207
+
208
+ def visit_AsyncFunctionDef(self, node):
209
+ self.visit_FunctionDef(node)
210
+
211
+ def _decorator_name(self, decorator):
212
+ """Extract decorator name from AST node."""
213
+ if isinstance(decorator, ast.Name):
214
+ return decorator.id
215
+ if isinstance(decorator, ast.Call):
216
+ if isinstance(decorator.func, ast.Name):
217
+ return decorator.func.id
218
+ if isinstance(decorator.func, ast.Attribute):
219
+ return decorator.func.attr
220
+ return "unknown"
221
+
222
+ def _calculate_complexity(self, node):
223
+ """Calculate cyclomatic complexity of a node."""
224
+ complexity = 1 # Base complexity
225
+
226
+ for child in ast.walk(node):
227
+ if isinstance(
228
+ child, (ast.If, ast.While, ast.For, ast.ExceptHandler)
229
+ ):
230
+ complexity += 1
231
+ elif isinstance(child, ast.BoolOp):
232
+ complexity += len(child.values) - 1
233
+
234
+ return complexity
235
+
236
+ def _get_function_signature(self, node):
237
+ """Extract function signature."""
238
+ args = []
239
+ for arg in node.args.args:
240
+ args.append(arg.arg)
241
+ return f"{node.name}({', '.join(args)})"
242
+
243
+ def _get_class_signature(self, node):
244
+ """Extract class signature."""
245
+ bases = []
246
+ for base in node.bases:
247
+ if isinstance(base, ast.Name):
248
+ bases.append(base.id)
249
+ base_str = f"({', '.join(bases)})" if bases else ""
250
+ return f"class {node.name}{base_str}"
251
+
252
+ # Extract imports
253
+ for node in ast.walk(tree):
254
+ if isinstance(node, ast.Import):
255
+ for alias in node.names:
256
+ import_node = CodeNode(
257
+ file_path=str(file_path),
258
+ node_type="import",
259
+ name=alias.name,
260
+ line_start=node.lineno,
261
+ line_end=node.end_lineno or node.lineno,
262
+ signature=f"import {alias.name}",
263
+ )
264
+ nodes.append(import_node)
265
+
266
+ elif isinstance(node, ast.ImportFrom):
267
+ module = node.module or ""
268
+ for alias in node.names:
269
+ import_node = CodeNode(
270
+ file_path=str(file_path),
271
+ node_type="import",
272
+ name=f"{module}.{alias.name}",
273
+ line_start=node.lineno,
274
+ line_end=node.end_lineno or node.lineno,
275
+ signature=f"from {module} import {alias.name}",
276
+ )
277
+ nodes.append(import_node)
278
+
279
+ # Visit all nodes
280
+ visitor = NodeVisitor()
281
+ visitor.emitter = self.emitter
282
+ visitor.visit(tree)
283
+
284
+ return nodes
285
+
286
+
287
+ class MultiLanguageAnalyzer:
288
+ """Analyzes multiple programming languages using tree-sitter.
289
+
290
+ WHY: Tree-sitter provides consistent parsing across multiple languages,
291
+ allowing us to support JavaScript, TypeScript, and other languages.
292
+ """
293
+
294
+ LANGUAGE_PARSERS = {
295
+ "python": "tree_sitter_python",
296
+ "javascript": "tree_sitter_javascript",
297
+ "typescript": "tree_sitter_typescript",
298
+ }
299
+
300
+ def __init__(self, emitter: Optional[CodeTreeEventEmitter] = None):
301
+ self.logger = get_logger(__name__)
302
+ self.emitter = emitter
303
+ self.parsers = {}
304
+ self._init_parsers()
305
+
306
+ def _init_parsers(self):
307
+ """Initialize tree-sitter parsers for supported languages."""
308
+ if not TREE_SITTER_AVAILABLE:
309
+ self.logger.warning(
310
+ "tree-sitter not available - multi-language support disabled"
311
+ )
312
+ return
313
+
314
+ for lang, module_name in self.LANGUAGE_PARSERS.items():
315
+ try:
316
+ # Dynamic import of language module
317
+ module = __import__(module_name)
318
+ parser = tree_sitter.Parser()
319
+ # Different tree-sitter versions have different APIs
320
+ if hasattr(parser, "set_language"):
321
+ parser.set_language(tree_sitter.Language(module.language()))
322
+ else:
323
+ # Newer API
324
+ lang_obj = tree_sitter.Language(module.language())
325
+ parser = tree_sitter.Parser(lang_obj)
326
+ self.parsers[lang] = parser
327
+ except (ImportError, AttributeError) as e:
328
+ self.logger.warning(f"Language parser not available for {lang}: {e}")
329
+
330
+ def analyze_file(self, file_path: Path, language: str) -> List[CodeNode]:
331
+ """Analyze a file using tree-sitter.
332
+
333
+ Args:
334
+ file_path: Path to source file
335
+ language: Programming language
336
+
337
+ Returns:
338
+ List of code nodes found in the file
339
+ """
340
+ if language not in self.parsers:
341
+ self.logger.warning(f"No parser available for language: {language}")
342
+ return []
343
+
344
+ nodes = []
345
+
346
+ try:
347
+ with open(file_path, "rb") as f:
348
+ source = f.read()
349
+
350
+ parser = self.parsers[language]
351
+ tree = parser.parse(source)
352
+
353
+ # Extract nodes based on language
354
+ if language in {"javascript", "typescript"}:
355
+ nodes = self._extract_js_nodes(tree, file_path, source)
356
+ else:
357
+ nodes = self._extract_generic_nodes(tree, file_path, source, language)
358
+
359
+ except Exception as e:
360
+ self.logger.error(f"Error analyzing {file_path}: {e}")
361
+ if self.emitter:
362
+ self.emitter.emit_error(str(file_path), str(e))
363
+
364
+ return nodes
365
+
366
+ def _extract_js_nodes(self, tree, file_path: Path, source: bytes) -> List[CodeNode]:
367
+ """Extract nodes from JavaScript/TypeScript files."""
368
+ nodes = []
369
+
370
+ def walk_tree(node, parent_name=None):
371
+ if node.type == "class_declaration":
372
+ # Extract class
373
+ name_node = node.child_by_field_name("name")
374
+ if name_node:
375
+ class_node = CodeNode(
376
+ file_path=str(file_path),
377
+ node_type="class",
378
+ name=source[name_node.start_byte : name_node.end_byte].decode(
379
+ "utf-8"
380
+ ),
381
+ line_start=node.start_point[0] + 1,
382
+ line_end=node.end_point[0] + 1,
383
+ parent=parent_name,
384
+ language="javascript",
385
+ )
386
+ nodes.append(class_node)
387
+
388
+ if self.emitter:
389
+ self.emitter.emit_node(
390
+ CodeNodeEvent(
391
+ file_path=str(file_path),
392
+ node_type="class",
393
+ name=class_node.name,
394
+ line_start=class_node.line_start,
395
+ line_end=class_node.line_end,
396
+ parent=parent_name,
397
+ language="javascript",
398
+ )
399
+ )
400
+
401
+ elif node.type in (
402
+ "function_declaration",
403
+ "arrow_function",
404
+ "method_definition",
405
+ ):
406
+ # Extract function
407
+ name_node = node.child_by_field_name("name")
408
+ if name_node:
409
+ func_name = source[
410
+ name_node.start_byte : name_node.end_byte
411
+ ].decode("utf-8")
412
+ func_node = CodeNode(
413
+ file_path=str(file_path),
414
+ node_type=(
415
+ "function" if node.type != "method_definition" else "method"
416
+ ),
417
+ name=func_name,
418
+ line_start=node.start_point[0] + 1,
419
+ line_end=node.end_point[0] + 1,
420
+ parent=parent_name,
421
+ language="javascript",
422
+ )
423
+ nodes.append(func_node)
424
+
425
+ if self.emitter:
426
+ self.emitter.emit_node(
427
+ CodeNodeEvent(
428
+ file_path=str(file_path),
429
+ node_type=func_node.node_type,
430
+ name=func_name,
431
+ line_start=func_node.line_start,
432
+ line_end=func_node.line_end,
433
+ parent=parent_name,
434
+ language="javascript",
435
+ )
436
+ )
437
+
438
+ # Recursively walk children
439
+ for child in node.children:
440
+ walk_tree(child, parent_name)
441
+
442
+ walk_tree(tree.root_node)
443
+ return nodes
444
+
445
+ def _extract_generic_nodes(
446
+ self, tree, file_path: Path, source: bytes, language: str
447
+ ) -> List[CodeNode]:
448
+ """Generic node extraction for other languages."""
449
+ # Simple generic extraction - can be enhanced per language
450
+ nodes = []
451
+
452
+ def walk_tree(node):
453
+ # Look for common patterns
454
+ if "class" in node.type or "struct" in node.type:
455
+ nodes.append(
456
+ CodeNode(
457
+ file_path=str(file_path),
458
+ node_type="class",
459
+ name=f"{node.type}_{node.start_point[0]}",
460
+ line_start=node.start_point[0] + 1,
461
+ line_end=node.end_point[0] + 1,
462
+ language=language,
463
+ )
464
+ )
465
+ elif "function" in node.type or "method" in node.type:
466
+ nodes.append(
467
+ CodeNode(
468
+ file_path=str(file_path),
469
+ node_type="function",
470
+ name=f"{node.type}_{node.start_point[0]}",
471
+ line_start=node.start_point[0] + 1,
472
+ line_end=node.end_point[0] + 1,
473
+ language=language,
474
+ )
475
+ )
476
+
477
+ for child in node.children:
478
+ walk_tree(child)
479
+
480
+ walk_tree(tree.root_node)
481
+ return nodes
482
+
483
+
484
+ class CodeTreeAnalyzer:
485
+ """Main analyzer that coordinates language-specific analyzers.
486
+
487
+ WHY: Provides a unified interface for analyzing codebases with multiple
488
+ languages, handling caching and incremental processing.
489
+ """
490
+
491
+ # File extensions to language mapping
492
+ LANGUAGE_MAP = {
493
+ ".py": "python",
494
+ ".js": "javascript",
495
+ ".jsx": "javascript",
496
+ ".ts": "typescript",
497
+ ".tsx": "typescript",
498
+ ".mjs": "javascript",
499
+ ".cjs": "javascript",
500
+ }
501
+
502
+ def __init__(self, emit_events: bool = True, cache_dir: Optional[Path] = None):
503
+ """Initialize the code tree analyzer.
504
+
505
+ Args:
506
+ emit_events: Whether to emit Socket.IO events
507
+ cache_dir: Directory for caching analysis results
508
+ """
509
+ self.logger = get_logger(__name__)
510
+ self.emit_events = emit_events
511
+ self.cache_dir = cache_dir or Path.home() / ".claude-mpm" / "code-cache"
512
+
513
+ # Initialize event emitter - use stdout mode for subprocess communication
514
+ self.emitter = CodeTreeEventEmitter(use_stdout=True) if emit_events else None
515
+
516
+ # Initialize language analyzers
517
+ self.python_analyzer = PythonAnalyzer(self.emitter)
518
+ self.multi_lang_analyzer = MultiLanguageAnalyzer(self.emitter)
519
+
520
+ # Cache for processed files
521
+ self.cache = {}
522
+ self._load_cache()
523
+
524
+ def analyze_directory(
525
+ self,
526
+ directory: Path,
527
+ languages: Optional[List[str]] = None,
528
+ ignore_patterns: Optional[List[str]] = None,
529
+ max_depth: Optional[int] = None,
530
+ ) -> Dict[str, Any]:
531
+ """Analyze a directory and build code tree.
532
+
533
+ Args:
534
+ directory: Directory to analyze
535
+ languages: Languages to include (None for all)
536
+ ignore_patterns: Patterns to ignore
537
+ max_depth: Maximum directory depth
538
+
539
+ Returns:
540
+ Dictionary containing the code tree and statistics
541
+ """
542
+ if self.emitter:
543
+ self.emitter.start()
544
+
545
+ start_time = time.time()
546
+ all_nodes = []
547
+ files_processed = 0
548
+ total_files = 0
549
+
550
+ # Collect files to process
551
+ files_to_process = []
552
+ for ext, lang in self.LANGUAGE_MAP.items():
553
+ if languages and lang not in languages:
554
+ continue
555
+
556
+ for file_path in directory.rglob(f"*{ext}"):
557
+ # Apply ignore patterns
558
+ if self._should_ignore(file_path, ignore_patterns):
559
+ continue
560
+
561
+ # Check max depth
562
+ if max_depth:
563
+ depth = len(file_path.relative_to(directory).parts) - 1
564
+ if depth > max_depth:
565
+ continue
566
+
567
+ files_to_process.append((file_path, lang))
568
+
569
+ total_files = len(files_to_process)
570
+
571
+ # Process files
572
+ for file_path, language in files_to_process:
573
+ # Check cache
574
+ file_hash = self._get_file_hash(file_path)
575
+ cache_key = f"{file_path}:{file_hash}"
576
+
577
+ if cache_key in self.cache:
578
+ nodes = self.cache[cache_key]
579
+ self.logger.debug(f"Using cached results for {file_path}")
580
+ else:
581
+ # Emit file start event
582
+ if self.emitter:
583
+ self.emitter.emit_file_start(str(file_path), language)
584
+
585
+ file_start = time.time()
586
+
587
+ # Analyze based on language
588
+ if language == "python":
589
+ nodes = self.python_analyzer.analyze_file(file_path)
590
+ else:
591
+ nodes = self.multi_lang_analyzer.analyze_file(file_path, language)
592
+
593
+ # Cache results
594
+ self.cache[cache_key] = nodes
595
+
596
+ # Emit file complete event
597
+ if self.emitter:
598
+ self.emitter.emit_file_complete(
599
+ str(file_path), len(nodes), time.time() - file_start
600
+ )
601
+
602
+ all_nodes.extend(nodes)
603
+ files_processed += 1
604
+
605
+ # Emit progress
606
+ if self.emitter and files_processed % 10 == 0:
607
+ self.emitter.emit_progress(
608
+ files_processed, total_files, f"Processing {file_path.name}"
609
+ )
610
+
611
+ # Build tree structure
612
+ tree = self._build_tree(all_nodes, directory)
613
+
614
+ # Calculate statistics
615
+ duration = time.time() - start_time
616
+ stats = {
617
+ "files_processed": files_processed,
618
+ "total_nodes": len(all_nodes),
619
+ "duration": duration,
620
+ "classes": sum(1 for n in all_nodes if n.node_type == "class"),
621
+ "functions": sum(
622
+ 1 for n in all_nodes if n.node_type in ("function", "method")
623
+ ),
624
+ "imports": sum(1 for n in all_nodes if n.node_type == "import"),
625
+ "languages": list(
626
+ {n.language for n in all_nodes if hasattr(n, "language")}
627
+ ),
628
+ "avg_complexity": (
629
+ sum(n.complexity for n in all_nodes) / len(all_nodes)
630
+ if all_nodes
631
+ else 0
632
+ ),
633
+ }
634
+
635
+ # Save cache
636
+ self._save_cache()
637
+
638
+ # Stop emitter
639
+ if self.emitter:
640
+ self.emitter.stop()
641
+
642
+ return {"tree": tree, "nodes": all_nodes, "stats": stats}
643
+
644
+ def _should_ignore(self, file_path: Path, patterns: Optional[List[str]]) -> bool:
645
+ """Check if file should be ignored."""
646
+ if not patterns:
647
+ patterns = []
648
+
649
+ # Default ignore patterns
650
+ default_ignores = [
651
+ "__pycache__",
652
+ ".git",
653
+ "node_modules",
654
+ ".venv",
655
+ "venv",
656
+ "dist",
657
+ "build",
658
+ ".pytest_cache",
659
+ ".mypy_cache",
660
+ ]
661
+
662
+ all_patterns = default_ignores + patterns
663
+
664
+ return any(pattern in str(file_path) for pattern in all_patterns)
665
+
666
+ def _get_file_hash(self, file_path: Path) -> str:
667
+ """Get hash of file contents for caching."""
668
+ hasher = hashlib.md5()
669
+ with open(file_path, "rb") as f:
670
+ hasher.update(f.read())
671
+ return hasher.hexdigest()
672
+
673
+ def _build_tree(self, nodes: List[CodeNode], root_dir: Path) -> Dict[str, Any]:
674
+ """Build hierarchical tree structure from flat nodes list."""
675
+ tree = {
676
+ "name": root_dir.name,
677
+ "type": "directory",
678
+ "path": str(root_dir),
679
+ "children": [],
680
+ }
681
+
682
+ # Group nodes by file
683
+ files_map = {}
684
+ for node in nodes:
685
+ if node.file_path not in files_map:
686
+ files_map[node.file_path] = {
687
+ "name": Path(node.file_path).name,
688
+ "type": "file",
689
+ "path": node.file_path,
690
+ "children": [],
691
+ }
692
+
693
+ # Add node to file
694
+ node_dict = {
695
+ "name": node.name,
696
+ "type": node.node_type,
697
+ "line_start": node.line_start,
698
+ "line_end": node.line_end,
699
+ "complexity": node.complexity,
700
+ "has_docstring": node.has_docstring,
701
+ "decorators": node.decorators,
702
+ "signature": node.signature,
703
+ }
704
+ files_map[node.file_path]["children"].append(node_dict)
705
+
706
+ # Build directory structure
707
+ for file_path, file_node in files_map.items():
708
+ rel_path = Path(file_path).relative_to(root_dir)
709
+ parts = rel_path.parts
710
+
711
+ current = tree
712
+ for part in parts[:-1]:
713
+ # Find or create directory
714
+ dir_node = None
715
+ for child in current["children"]:
716
+ if child["type"] == "directory" and child["name"] == part:
717
+ dir_node = child
718
+ break
719
+
720
+ if not dir_node:
721
+ dir_node = {"name": part, "type": "directory", "children": []}
722
+ current["children"].append(dir_node)
723
+
724
+ current = dir_node
725
+
726
+ # Add file to current directory
727
+ current["children"].append(file_node)
728
+
729
+ return tree
730
+
731
+ def _load_cache(self):
732
+ """Load cache from disk."""
733
+ cache_file = self.cache_dir / "code_tree_cache.json"
734
+ if cache_file.exists():
735
+ try:
736
+ with open(cache_file) as f:
737
+ cache_data = json.load(f)
738
+ # Reconstruct CodeNode objects
739
+ for key, nodes_data in cache_data.items():
740
+ self.cache[key] = [
741
+ CodeNode(**node_data) for node_data in nodes_data
742
+ ]
743
+ self.logger.info(f"Loaded cache with {len(self.cache)} entries")
744
+ except Exception as e:
745
+ self.logger.warning(f"Failed to load cache: {e}")
746
+
747
+ def _save_cache(self):
748
+ """Save cache to disk."""
749
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
750
+ cache_file = self.cache_dir / "code_tree_cache.json"
751
+
752
+ try:
753
+ # Convert CodeNode objects to dictionaries
754
+ cache_data = {}
755
+ for key, nodes in self.cache.items():
756
+ cache_data[key] = [
757
+ {
758
+ "file_path": n.file_path,
759
+ "node_type": n.node_type,
760
+ "name": n.name,
761
+ "line_start": n.line_start,
762
+ "line_end": n.line_end,
763
+ "complexity": n.complexity,
764
+ "has_docstring": n.has_docstring,
765
+ "decorators": n.decorators,
766
+ "parent": n.parent,
767
+ "language": n.language,
768
+ "signature": n.signature,
769
+ }
770
+ for n in nodes
771
+ ]
772
+
773
+ with open(cache_file, "w") as f:
774
+ json.dump(cache_data, f, indent=2)
775
+
776
+ self.logger.info(f"Saved cache with {len(self.cache)} entries")
777
+ except Exception as e:
778
+ self.logger.warning(f"Failed to save cache: {e}")