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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +26 -1
- 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/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 +77 -28
- claude_mpm/cli/commands/configure_tui.py +60 -60
- 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/debug_parser.py +319 -0
- 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 +386 -113
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- 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 +1 -2
- 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 +4 -4
- claude_mpm/services/socketio/server/core.py +100 -11
- claude_mpm/services/socketio/server/main.py +8 -2
- 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.8.dist-info ā claude_mpm-4.1.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.8.dist-info ā claude_mpm-4.1.10.dist-info}/RECORD +102 -73
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.8.dist-info ā claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.8.dist-info ā claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.8.dist-info ā claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.8.dist-info ā claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code analysis command implementation for claude-mpm.
|
|
3
|
+
|
|
4
|
+
WHY: This module provides code analysis capabilities with mermaid diagram
|
|
5
|
+
generation, allowing users to visualize and understand their codebase
|
|
6
|
+
architecture through automated analysis.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Use async for better performance with multiple diagram generation
|
|
10
|
+
- Extract mermaid blocks from agent responses automatically
|
|
11
|
+
- Save diagrams with timestamps for versioning
|
|
12
|
+
- Support multiple diagram types in single run
|
|
13
|
+
- Integrate with existing session management
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
from ...core.logging_config import get_logger
|
|
27
|
+
from ...services.cli.session_manager import SessionManager
|
|
28
|
+
from ..shared import BaseCommand, CommandResult
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AnalyzeCommand(BaseCommand):
|
|
32
|
+
"""Analyze command for code analysis with mermaid generation."""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
super().__init__("analyze")
|
|
36
|
+
self.logger = get_logger(__name__)
|
|
37
|
+
self.session_manager = SessionManager()
|
|
38
|
+
|
|
39
|
+
def validate_args(self, args) -> Optional[str]:
|
|
40
|
+
"""Validate command arguments.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
args: Command arguments
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Error message if validation fails, None otherwise
|
|
47
|
+
"""
|
|
48
|
+
# Validate target exists
|
|
49
|
+
if not args.target.exists():
|
|
50
|
+
return f"Target path does not exist: {args.target}"
|
|
51
|
+
|
|
52
|
+
# Validate diagram output directory if saving
|
|
53
|
+
if args.save_diagrams:
|
|
54
|
+
diagram_dir = args.diagram_output or Path("./diagrams")
|
|
55
|
+
if not diagram_dir.exists():
|
|
56
|
+
try:
|
|
57
|
+
diagram_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return f"Cannot create diagram output directory: {e}"
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def run(self, args) -> CommandResult:
|
|
64
|
+
"""Execute the analyze command.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
args: Command arguments
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
CommandResult with analysis results
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
# Run async analysis
|
|
74
|
+
return asyncio.run(self._run_analysis(args))
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
return CommandResult.error_result("Analysis interrupted by user")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.logger.error(f"Analysis failed: {e}", exc_info=True)
|
|
79
|
+
return CommandResult.error_result(f"Analysis failed: {e}")
|
|
80
|
+
|
|
81
|
+
async def _run_analysis(self, args) -> CommandResult:
|
|
82
|
+
"""Run the actual analysis asynchronously.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
args: Command arguments
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
CommandResult with analysis results
|
|
89
|
+
"""
|
|
90
|
+
# Build analysis prompt
|
|
91
|
+
prompt = self._build_analysis_prompt(args)
|
|
92
|
+
|
|
93
|
+
# Setup session if needed
|
|
94
|
+
session_id = None
|
|
95
|
+
if not args.no_session:
|
|
96
|
+
session_id = args.session_id or self._create_analysis_session()
|
|
97
|
+
|
|
98
|
+
# Execute analysis via agent
|
|
99
|
+
self.logger.info(f"Starting code analysis of {args.target}")
|
|
100
|
+
response = await self._execute_agent_analysis(
|
|
101
|
+
agent=args.agent, prompt=prompt, session_id=session_id, args=args
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not response:
|
|
105
|
+
return CommandResult.error_result("No response from analysis agent")
|
|
106
|
+
|
|
107
|
+
# Extract mermaid diagrams if enabled
|
|
108
|
+
diagrams = []
|
|
109
|
+
if args.mermaid:
|
|
110
|
+
diagrams = self._extract_mermaid_diagrams(response)
|
|
111
|
+
|
|
112
|
+
if args.save_diagrams:
|
|
113
|
+
saved_files = self._save_diagrams(diagrams, args)
|
|
114
|
+
self.logger.info(f"Saved {len(saved_files)} diagrams")
|
|
115
|
+
|
|
116
|
+
# Format and return results
|
|
117
|
+
result_data = {
|
|
118
|
+
"target": str(args.target),
|
|
119
|
+
"analysis": response,
|
|
120
|
+
"diagrams_found": len(diagrams),
|
|
121
|
+
"session_id": session_id,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if args.save_diagrams and diagrams:
|
|
125
|
+
result_data["saved_diagrams"] = [str(f) for f in saved_files]
|
|
126
|
+
|
|
127
|
+
# Handle output format
|
|
128
|
+
output = self._format_output(result_data, args.format, diagrams)
|
|
129
|
+
|
|
130
|
+
# Save to file if requested
|
|
131
|
+
if args.output:
|
|
132
|
+
self._save_output(output, args.output)
|
|
133
|
+
|
|
134
|
+
return CommandResult.success_result(
|
|
135
|
+
message=output if args.format == "text" else "Analysis completed",
|
|
136
|
+
data=result_data,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _build_analysis_prompt(self, args) -> str:
|
|
140
|
+
"""Build the analysis prompt based on arguments.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
args: Command arguments
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Formatted prompt string
|
|
147
|
+
"""
|
|
148
|
+
prompt_parts = []
|
|
149
|
+
|
|
150
|
+
# Base analysis request
|
|
151
|
+
prompt_parts.append(f"Analyze the code at: {args.target}")
|
|
152
|
+
|
|
153
|
+
# Add custom prompt if provided
|
|
154
|
+
if args.prompt:
|
|
155
|
+
prompt_parts.append(f"\n{args.prompt}")
|
|
156
|
+
|
|
157
|
+
# Add focus areas
|
|
158
|
+
if args.focus:
|
|
159
|
+
focus_list = args.focus if isinstance(args.focus, list) else [args.focus]
|
|
160
|
+
focus_str = ", ".join(focus_list)
|
|
161
|
+
prompt_parts.append(f"\nFocus on: {focus_str}")
|
|
162
|
+
|
|
163
|
+
# Add mermaid diagram requests
|
|
164
|
+
if args.mermaid:
|
|
165
|
+
types_list = (
|
|
166
|
+
args.mermaid_types
|
|
167
|
+
if isinstance(args.mermaid_types, list)
|
|
168
|
+
else [args.mermaid_types]
|
|
169
|
+
)
|
|
170
|
+
diagram_types = ", ".join(types_list)
|
|
171
|
+
prompt_parts.append(
|
|
172
|
+
f"\nGenerate mermaid diagrams for: {diagram_types}\n"
|
|
173
|
+
"Ensure each diagram is in a separate ```mermaid code block."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Add specific instructions per diagram type
|
|
177
|
+
diagram_instructions = self._get_diagram_instructions(args.mermaid_types)
|
|
178
|
+
if diagram_instructions:
|
|
179
|
+
prompt_parts.append(diagram_instructions)
|
|
180
|
+
|
|
181
|
+
return "\n".join(prompt_parts)
|
|
182
|
+
|
|
183
|
+
def _get_diagram_instructions(self, diagram_types: List[str]) -> str:
|
|
184
|
+
"""Get specific instructions for requested diagram types.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
diagram_types: List of diagram types
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Formatted instructions string
|
|
191
|
+
"""
|
|
192
|
+
instructions = []
|
|
193
|
+
|
|
194
|
+
type_instructions = {
|
|
195
|
+
"entry_points": "Identify and map all entry points in the codebase",
|
|
196
|
+
"class_diagram": "Create UML class diagrams showing relationships",
|
|
197
|
+
"sequence": "Show sequence diagrams for key workflows",
|
|
198
|
+
"flowchart": "Create flowcharts for main processes",
|
|
199
|
+
"state": "Show state diagrams for stateful components",
|
|
200
|
+
"entity_relationship": "Map database entities and relationships",
|
|
201
|
+
"component": "Show high-level component architecture",
|
|
202
|
+
"dependency_graph": "Map module and package dependencies",
|
|
203
|
+
"call_graph": "Show function/method call relationships",
|
|
204
|
+
"architecture": "Create overall system architecture diagram",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for dtype in diagram_types:
|
|
208
|
+
if dtype in type_instructions:
|
|
209
|
+
instructions.append(f"- {type_instructions[dtype]}")
|
|
210
|
+
|
|
211
|
+
if instructions:
|
|
212
|
+
return "\nDiagram requirements:\n" + "\n".join(instructions)
|
|
213
|
+
return ""
|
|
214
|
+
|
|
215
|
+
async def _execute_agent_analysis(
|
|
216
|
+
self, agent: str, prompt: str, session_id: Optional[str], args
|
|
217
|
+
) -> Optional[str]:
|
|
218
|
+
"""Execute analysis using the specified agent.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
agent: Agent ID to use
|
|
222
|
+
prompt: Analysis prompt
|
|
223
|
+
session_id: Session ID if using sessions
|
|
224
|
+
args: Command arguments
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Agent response text or None if failed
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
# Import required modules
|
|
231
|
+
from ...core.claude_runner import ClaudeRunner
|
|
232
|
+
from ...services.agents.deployment.agent_deployment import (
|
|
233
|
+
AgentDeploymentService,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Deploy the analysis agent if not already deployed
|
|
237
|
+
deployment_service = AgentDeploymentService()
|
|
238
|
+
deployment_result = deployment_service.deploy_agent(agent, force=False)
|
|
239
|
+
|
|
240
|
+
if not deployment_result["success"]:
|
|
241
|
+
self.logger.error(
|
|
242
|
+
f"Failed to deploy agent {agent}: {deployment_result.get('error')}"
|
|
243
|
+
)
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
# Create a temporary file with the prompt
|
|
247
|
+
import tempfile
|
|
248
|
+
|
|
249
|
+
with tempfile.NamedTemporaryFile(
|
|
250
|
+
mode="w", suffix=".txt", delete=False
|
|
251
|
+
) as f:
|
|
252
|
+
f.write(prompt)
|
|
253
|
+
prompt_file = f.name
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
# Build Claude args for analysis
|
|
257
|
+
claude_args = []
|
|
258
|
+
|
|
259
|
+
# Add input file
|
|
260
|
+
claude_args.extend(["--input", prompt_file])
|
|
261
|
+
|
|
262
|
+
# Add session if specified
|
|
263
|
+
if session_id:
|
|
264
|
+
claude_args.extend(["--session", session_id])
|
|
265
|
+
|
|
266
|
+
# Set working directory
|
|
267
|
+
claude_args.extend(["--cwd", str(args.target)])
|
|
268
|
+
|
|
269
|
+
# Disable hooks for cleaner output capture
|
|
270
|
+
no_hooks = True
|
|
271
|
+
|
|
272
|
+
# Initialize and run Claude runner
|
|
273
|
+
ClaudeRunner(
|
|
274
|
+
enable_tickets=False,
|
|
275
|
+
launch_method="subprocess",
|
|
276
|
+
claude_args=claude_args,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Set up environment
|
|
280
|
+
env = os.environ.copy()
|
|
281
|
+
env["CLAUDE_MPM_AGENT"] = agent
|
|
282
|
+
|
|
283
|
+
# Build the full command
|
|
284
|
+
scripts_dir = (
|
|
285
|
+
Path(__file__).parent.parent.parent.parent.parent / "scripts"
|
|
286
|
+
)
|
|
287
|
+
claude_mpm_script = scripts_dir / "claude-mpm"
|
|
288
|
+
|
|
289
|
+
cmd = []
|
|
290
|
+
if claude_mpm_script.exists():
|
|
291
|
+
cmd = [str(claude_mpm_script)]
|
|
292
|
+
else:
|
|
293
|
+
# Fallback to using module execution
|
|
294
|
+
cmd = [sys.executable, "-m", "claude_mpm"]
|
|
295
|
+
|
|
296
|
+
# Add subcommand and options
|
|
297
|
+
cmd.extend(["run", "--no-tickets", "--input", prompt_file])
|
|
298
|
+
|
|
299
|
+
if no_hooks:
|
|
300
|
+
cmd.append("--no-hooks")
|
|
301
|
+
|
|
302
|
+
# Execute the command
|
|
303
|
+
result = subprocess.run(
|
|
304
|
+
cmd,
|
|
305
|
+
capture_output=True,
|
|
306
|
+
text=True,
|
|
307
|
+
cwd=str(args.target),
|
|
308
|
+
env=env,
|
|
309
|
+
timeout=600, check=False, # 10 minute timeout
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if result.returncode != 0:
|
|
313
|
+
self.logger.error(f"Claude execution failed: {result.stderr}")
|
|
314
|
+
# Return stdout even on error as it may contain partial results
|
|
315
|
+
return result.stdout if result.stdout else result.stderr
|
|
316
|
+
|
|
317
|
+
return result.stdout
|
|
318
|
+
|
|
319
|
+
finally:
|
|
320
|
+
# Clean up temp file
|
|
321
|
+
Path(prompt_file).unlink(missing_ok=True)
|
|
322
|
+
|
|
323
|
+
except subprocess.TimeoutExpired:
|
|
324
|
+
self.logger.error("Analysis timed out after 10 minutes")
|
|
325
|
+
return None
|
|
326
|
+
except Exception as e:
|
|
327
|
+
self.logger.error(f"Agent execution failed: {e}", exc_info=True)
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def _extract_mermaid_diagrams(self, response: str) -> List[Dict[str, str]]:
|
|
331
|
+
"""Extract mermaid diagram blocks from response.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
response: Agent response text
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of diagram dictionaries with content and optional titles
|
|
338
|
+
"""
|
|
339
|
+
diagrams = []
|
|
340
|
+
|
|
341
|
+
# Pattern to match mermaid code blocks
|
|
342
|
+
pattern = r"```mermaid\n(.*?)```"
|
|
343
|
+
matches = re.findall(pattern, response, re.DOTALL)
|
|
344
|
+
|
|
345
|
+
for i, match in enumerate(matches):
|
|
346
|
+
# Try to extract title from preceding line
|
|
347
|
+
title = f"diagram_{i+1}"
|
|
348
|
+
|
|
349
|
+
# Look for a title pattern before the diagram
|
|
350
|
+
title_pattern = r"(?:#+\s*)?([^\n]+)\n+```mermaid"
|
|
351
|
+
title_matches = re.findall(title_pattern, response)
|
|
352
|
+
if i < len(title_matches):
|
|
353
|
+
title = self._sanitize_filename(title_matches[i])
|
|
354
|
+
|
|
355
|
+
diagrams.append({"title": title, "content": match.strip(), "index": i + 1})
|
|
356
|
+
|
|
357
|
+
self.logger.info(f"Extracted {len(diagrams)} mermaid diagrams")
|
|
358
|
+
return diagrams
|
|
359
|
+
|
|
360
|
+
def _save_diagrams(self, diagrams: List[Dict[str, str]], args) -> List[Path]:
|
|
361
|
+
"""Save mermaid diagrams to files.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
diagrams: List of diagram dictionaries
|
|
365
|
+
args: Command arguments
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
List of saved file paths
|
|
369
|
+
"""
|
|
370
|
+
saved_files = []
|
|
371
|
+
diagram_dir = args.diagram_output or Path("./diagrams")
|
|
372
|
+
diagram_dir.mkdir(parents=True, exist_ok=True)
|
|
373
|
+
|
|
374
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
375
|
+
|
|
376
|
+
for diagram in diagrams:
|
|
377
|
+
filename = f"{timestamp}_{diagram['title']}.mermaid"
|
|
378
|
+
filepath = diagram_dir / filename
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
with open(filepath, "w") as f:
|
|
382
|
+
# Write mermaid header comment
|
|
383
|
+
f.write("// Generated by Claude MPM Code Analyzer\n")
|
|
384
|
+
f.write(f"// Timestamp: {timestamp}\n")
|
|
385
|
+
f.write(f"// Target: {args.target}\n")
|
|
386
|
+
f.write(f"// Title: {diagram['title']}\n\n")
|
|
387
|
+
f.write(diagram["content"])
|
|
388
|
+
|
|
389
|
+
saved_files.append(filepath)
|
|
390
|
+
self.logger.debug(f"Saved diagram to {filepath}")
|
|
391
|
+
|
|
392
|
+
except Exception as e:
|
|
393
|
+
self.logger.error(f"Failed to save diagram {diagram['title']}: {e}")
|
|
394
|
+
|
|
395
|
+
return saved_files
|
|
396
|
+
|
|
397
|
+
def _sanitize_filename(self, title: str) -> str:
|
|
398
|
+
"""Sanitize a string to be safe for filename.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
title: Original title string
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Sanitized filename string
|
|
405
|
+
"""
|
|
406
|
+
# Remove or replace unsafe characters
|
|
407
|
+
safe_chars = re.sub(r"[^\w\s-]", "", title)
|
|
408
|
+
safe_chars = re.sub(r"[-\s]+", "_", safe_chars)
|
|
409
|
+
return safe_chars[:50].strip("_").lower()
|
|
410
|
+
|
|
411
|
+
def _format_output(
|
|
412
|
+
self, result_data: Dict, format_type: str, diagrams: List[Dict]
|
|
413
|
+
) -> str:
|
|
414
|
+
"""Format output based on requested format.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
result_data: Analysis results
|
|
418
|
+
format_type: Output format (text, json, markdown)
|
|
419
|
+
diagrams: List of extracted diagrams
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Formatted output string
|
|
423
|
+
"""
|
|
424
|
+
if format_type == "json":
|
|
425
|
+
result_data["diagrams"] = diagrams
|
|
426
|
+
return json.dumps(result_data, indent=2)
|
|
427
|
+
|
|
428
|
+
if format_type == "markdown":
|
|
429
|
+
output = "# Code Analysis Report\n\n"
|
|
430
|
+
output += f"**Target:** `{result_data['target']}`\n"
|
|
431
|
+
output += f"**Timestamp:** {datetime.now().isoformat()}\n"
|
|
432
|
+
|
|
433
|
+
if result_data.get("session_id"):
|
|
434
|
+
output += f"**Session ID:** {result_data['session_id']}\n"
|
|
435
|
+
|
|
436
|
+
output += "\n## Analysis Results\n\n"
|
|
437
|
+
output += result_data.get("analysis", "No analysis results")
|
|
438
|
+
|
|
439
|
+
if diagrams:
|
|
440
|
+
output += f"\n## Generated Diagrams ({len(diagrams)})\n\n"
|
|
441
|
+
for diagram in diagrams:
|
|
442
|
+
output += f"### {diagram['title']}\n\n"
|
|
443
|
+
output += f"```mermaid\n{diagram['content']}\n```\n\n"
|
|
444
|
+
|
|
445
|
+
if result_data.get("saved_diagrams"):
|
|
446
|
+
output += "\n## Saved Files\n\n"
|
|
447
|
+
for filepath in result_data["saved_diagrams"]:
|
|
448
|
+
output += f"- `{filepath}`\n"
|
|
449
|
+
|
|
450
|
+
return output
|
|
451
|
+
|
|
452
|
+
# text format
|
|
453
|
+
output = f"Code Analysis Report\n{'='*50}\n\n"
|
|
454
|
+
output += f"Target: {result_data['target']}\n"
|
|
455
|
+
|
|
456
|
+
if diagrams:
|
|
457
|
+
output += f"\nš Extracted {len(diagrams)} mermaid diagrams:\n"
|
|
458
|
+
for diagram in diagrams:
|
|
459
|
+
output += f" ⢠{diagram['title']}\n"
|
|
460
|
+
|
|
461
|
+
if result_data.get("saved_diagrams"):
|
|
462
|
+
output += "\nš¾ Saved diagrams to:\n"
|
|
463
|
+
for filepath in result_data["saved_diagrams"]:
|
|
464
|
+
output += f" ⢠{filepath}\n"
|
|
465
|
+
|
|
466
|
+
output += f"\n{'-'*50}\nAnalysis Results:\n{'-'*50}\n"
|
|
467
|
+
output += result_data.get("analysis", "No analysis results")
|
|
468
|
+
|
|
469
|
+
return output
|
|
470
|
+
|
|
471
|
+
def _save_output(self, content: str, filepath: Path):
|
|
472
|
+
"""Save output content to file.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
content: Content to save
|
|
476
|
+
filepath: Target file path
|
|
477
|
+
"""
|
|
478
|
+
try:
|
|
479
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
480
|
+
with open(filepath, "w") as f:
|
|
481
|
+
f.write(content)
|
|
482
|
+
self.logger.info(f"Saved output to {filepath}")
|
|
483
|
+
except Exception as e:
|
|
484
|
+
self.logger.error(f"Failed to save output: {e}")
|
|
485
|
+
|
|
486
|
+
def _create_analysis_session(self) -> str:
|
|
487
|
+
"""Create a new analysis session.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Session ID
|
|
491
|
+
"""
|
|
492
|
+
session_data = {
|
|
493
|
+
"context": "code_analysis",
|
|
494
|
+
"created_at": datetime.now().isoformat(),
|
|
495
|
+
"type": "analysis",
|
|
496
|
+
}
|
|
497
|
+
session_id = self.session_manager.create_session(session_data)
|
|
498
|
+
self.logger.debug(f"Created analysis session: {session_id}")
|
|
499
|
+
return session_id
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def analyze_command(args):
|
|
503
|
+
"""Entry point for analyze command.
|
|
504
|
+
|
|
505
|
+
WHY: Provides a single entry point for code analysis with mermaid
|
|
506
|
+
diagram generation, helping users visualize and understand their codebase.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
args: Parsed command-line arguments
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Exit code (0 for success, non-zero for errors)
|
|
513
|
+
"""
|
|
514
|
+
command = AnalyzeCommand()
|
|
515
|
+
result = command.run(args)
|
|
516
|
+
|
|
517
|
+
if result.success:
|
|
518
|
+
if args.format == "json":
|
|
519
|
+
print(json.dumps(result.data, indent=2))
|
|
520
|
+
else:
|
|
521
|
+
print(result.message)
|
|
522
|
+
return 0
|
|
523
|
+
print(f"ā {result.message}", file=sys.stderr)
|
|
524
|
+
return 1
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
# Optional: Standalone execution for testing
|
|
528
|
+
if __name__ == "__main__":
|
|
529
|
+
import argparse
|
|
530
|
+
|
|
531
|
+
parser = argparse.ArgumentParser(description="Claude MPM Code Analyzer")
|
|
532
|
+
parser.add_argument("--target", type=Path, default=Path.cwd())
|
|
533
|
+
parser.add_argument("--mermaid", action="store_true")
|
|
534
|
+
parser.add_argument("--mermaid-types", nargs="+", default=["entry_points"])
|
|
535
|
+
parser.add_argument("--save-diagrams", action="store_true")
|
|
536
|
+
parser.add_argument("--diagram-output", type=Path)
|
|
537
|
+
parser.add_argument("--agent", default="code-analyzer")
|
|
538
|
+
parser.add_argument("--prompt", type=str)
|
|
539
|
+
parser.add_argument("--focus", nargs="+")
|
|
540
|
+
parser.add_argument("--session-id", type=str)
|
|
541
|
+
parser.add_argument("--no-session", action="store_true")
|
|
542
|
+
parser.add_argument("--format", default="text")
|
|
543
|
+
parser.add_argument("--output", type=Path)
|
|
544
|
+
parser.add_argument("--verbose", action="store_true")
|
|
545
|
+
|
|
546
|
+
args = parser.parse_args()
|
|
547
|
+
sys.exit(analyze_command(args))
|