claude-mpm 4.1.7__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +26 -1
- claude_mpm/agents/OUTPUT_STYLE.md +73 -0
- claude_mpm/agents/agents_metadata.py +57 -0
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
- claude_mpm/agents/templates/agent-manager.json +263 -17
- claude_mpm/agents/templates/agent-manager.md +248 -10
- claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
- claude_mpm/agents/templates/code_analyzer.json +18 -8
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/cli/__init__.py +4 -0
- claude_mpm/cli/commands/__init__.py +6 -0
- claude_mpm/cli/commands/analyze.py +547 -0
- claude_mpm/cli/commands/analyze_code.py +524 -0
- claude_mpm/cli/commands/configure.py +223 -25
- claude_mpm/cli/commands/configure_tui.py +65 -61
- claude_mpm/cli/commands/debug.py +1387 -0
- claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
- claude_mpm/cli/parsers/analyze_parser.py +135 -0
- claude_mpm/cli/parsers/base_parser.py +29 -0
- claude_mpm/cli/parsers/configure_parser.py +23 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/config/socketio_config.py +21 -21
- claude_mpm/constants.py +3 -1
- claude_mpm/core/framework_loader.py +148 -6
- claude_mpm/core/log_manager.py +16 -13
- claude_mpm/core/logger.py +1 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
- claude_mpm/dashboard/analysis_runner.py +428 -0
- claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/built/components/working-directory.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/activity.css +549 -0
- claude_mpm/dashboard/static/css/code-tree.css +846 -0
- claude_mpm/dashboard/static/css/dashboard.css +245 -0
- claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/components/working-directory.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/activity-tree.js +1139 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
- claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
- claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
- claude_mpm/dashboard/static/js/dashboard.js +39 -0
- claude_mpm/dashboard/static/js/socket-client.js +414 -20
- claude_mpm/dashboard/templates/index.html +184 -4
- claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
- claude_mpm/hooks/claude_hooks/installer.py +728 -0
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
- claude_mpm/services/agents/memory/memory_format_service.py +1 -5
- claude_mpm/services/cli/agent_cleanup_service.py +1 -2
- claude_mpm/services/cli/agent_dependency_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +3 -4
- claude_mpm/services/cli/dashboard_launcher.py +2 -3
- claude_mpm/services/cli/startup_checker.py +0 -10
- claude_mpm/services/core/cache_manager.py +1 -2
- claude_mpm/services/core/path_resolver.py +1 -4
- claude_mpm/services/core/service_container.py +2 -2
- claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
- claude_mpm/services/event_bus/direct_relay.py +98 -20
- claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
- claude_mpm/services/infrastructure/monitoring.py +11 -11
- claude_mpm/services/project/architecture_analyzer.py +1 -1
- claude_mpm/services/project/dependency_analyzer.py +4 -4
- claude_mpm/services/project/language_analyzer.py +3 -3
- claude_mpm/services/project/metrics_collector.py +3 -6
- claude_mpm/services/socketio/handlers/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
- claude_mpm/services/socketio/handlers/registry.py +2 -0
- claude_mpm/services/socketio/server/connection_manager.py +95 -65
- claude_mpm/services/socketio/server/core.py +125 -17
- claude_mpm/services/socketio/server/main.py +44 -5
- claude_mpm/services/visualization/__init__.py +19 -0
- claude_mpm/services/visualization/mermaid_generator.py +938 -0
- claude_mpm/tools/__main__.py +208 -0
- claude_mpm/tools/code_tree_analyzer.py +778 -0
- claude_mpm/tools/code_tree_builder.py +632 -0
- claude_mpm/tools/code_tree_events.py +318 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.7.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}")
|