claude-mpm 4.1.8__py3-none-any.whl → 4.1.11__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 +15 -0
- claude_mpm/cli/commands/__init__.py +6 -0
- claude_mpm/cli/commands/analyze.py +548 -0
- claude_mpm/cli/commands/analyze_code.py +524 -0
- claude_mpm/cli/commands/configure.py +78 -28
- claude_mpm/cli/commands/configure_tui.py +62 -60
- claude_mpm/cli/commands/dashboard.py +288 -0
- claude_mpm/cli/commands/debug.py +1386 -0
- claude_mpm/cli/commands/mpm_init.py +427 -0
- claude_mpm/cli/commands/mpm_init_handler.py +83 -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 +44 -0
- claude_mpm/cli/parsers/dashboard_parser.py +113 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +122 -0
- claude_mpm/constants.py +13 -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 +455 -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/code-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +2 -0
- 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 +1175 -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 +1338 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +2535 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +59 -9
- 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 +51 -0
- claude_mpm/dashboard/static/js/socket-client.js +465 -29
- claude_mpm/dashboard/templates/index.html +182 -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 -3
- claude_mpm/services/cli/agent_cleanup_service.py +1 -5
- 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 -11
- claude_mpm/services/core/cache_manager.py +1 -3
- 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/event_normalizer.py +64 -0
- claude_mpm/services/socketio/handlers/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +672 -0
- claude_mpm/services/socketio/handlers/registry.py +2 -0
- claude_mpm/services/socketio/server/connection_manager.py +6 -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 +1596 -0
- claude_mpm/tools/code_tree_builder.py +631 -0
- claude_mpm/tools/code_tree_events.py +416 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/METADATA +2 -1
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/RECORD +110 -74
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code Analysis Event Handler for Socket.IO
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
WHY: Handles code analysis requests from the dashboard, managing the analysis
|
|
6
|
+
runner subprocess and streaming results back to connected clients.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Single analysis runner instance per server
|
|
10
|
+
- Queue multiple requests for sequential processing
|
|
11
|
+
- Support cancellation of running analysis
|
|
12
|
+
- Stream events in real-time to all connected clients
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict
|
|
19
|
+
|
|
20
|
+
from ....core.logging_config import get_logger
|
|
21
|
+
from ....dashboard.analysis_runner import CodeAnalysisRunner
|
|
22
|
+
from ....tools.code_tree_analyzer import CodeTreeAnalyzer
|
|
23
|
+
from ....tools.code_tree_events import CodeTreeEventEmitter
|
|
24
|
+
from .base import BaseEventHandler
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CodeAnalysisEventHandler(BaseEventHandler):
|
|
28
|
+
"""Handles code analysis events from dashboard clients.
|
|
29
|
+
|
|
30
|
+
WHY: Provides a clean interface between the dashboard UI and the
|
|
31
|
+
code analysis subprocess, managing requests and responses.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, server):
|
|
35
|
+
"""Initialize the code analysis event handler.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
server: The SocketIOServer instance
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(server)
|
|
41
|
+
self.logger = get_logger(__name__)
|
|
42
|
+
self.analysis_runner = None
|
|
43
|
+
self.code_analyzer = None # For lazy loading operations
|
|
44
|
+
|
|
45
|
+
def initialize(self):
|
|
46
|
+
"""Initialize the analysis runner."""
|
|
47
|
+
if not self.analysis_runner:
|
|
48
|
+
self.analysis_runner = CodeAnalysisRunner(self.server)
|
|
49
|
+
self.analysis_runner.start()
|
|
50
|
+
self.logger.info("Code analysis runner initialized")
|
|
51
|
+
|
|
52
|
+
def cleanup(self):
|
|
53
|
+
"""Cleanup the analysis runner on shutdown."""
|
|
54
|
+
if self.analysis_runner:
|
|
55
|
+
self.analysis_runner.stop()
|
|
56
|
+
self.analysis_runner = None
|
|
57
|
+
self.logger.info("Code analysis runner stopped")
|
|
58
|
+
|
|
59
|
+
def get_events(self) -> Dict[str, Any]:
|
|
60
|
+
"""Get the events this handler manages.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dictionary mapping event names to handler methods
|
|
64
|
+
"""
|
|
65
|
+
return {
|
|
66
|
+
# Legacy full analysis
|
|
67
|
+
"code:analyze:request": self.handle_analyze_request,
|
|
68
|
+
"code:analyze:cancel": self.handle_cancel_request,
|
|
69
|
+
"code:analyze:status": self.handle_status_request,
|
|
70
|
+
# Lazy loading operations
|
|
71
|
+
"code:discover:top_level": self.handle_discover_top_level,
|
|
72
|
+
"code:discover:directory": self.handle_discover_directory,
|
|
73
|
+
"code:analyze:file": self.handle_analyze_file,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def register_events(self) -> None:
|
|
77
|
+
"""Register Socket.IO event handlers.
|
|
78
|
+
|
|
79
|
+
WHY: Required by BaseEventHandler to register events with the Socket.IO server.
|
|
80
|
+
"""
|
|
81
|
+
events = self.get_events()
|
|
82
|
+
for event_name, handler_method in events.items():
|
|
83
|
+
self.server.core.sio.on(event_name, handler_method)
|
|
84
|
+
self.logger.info(f"Registered event handler: {event_name}")
|
|
85
|
+
|
|
86
|
+
async def handle_analyze_request(self, sid: str, data: Dict[str, Any]):
|
|
87
|
+
"""Handle code analysis request from client.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
sid: Socket ID of the requesting client
|
|
91
|
+
data: Request data containing path and options
|
|
92
|
+
"""
|
|
93
|
+
self.logger.info(f"Code analysis requested from {sid}: {data}")
|
|
94
|
+
|
|
95
|
+
# Initialize runner if needed
|
|
96
|
+
if not self.analysis_runner:
|
|
97
|
+
self.initialize()
|
|
98
|
+
|
|
99
|
+
# Validate request
|
|
100
|
+
path = data.get("path")
|
|
101
|
+
if not path:
|
|
102
|
+
await self.server.sio.emit(
|
|
103
|
+
"code:analysis:error",
|
|
104
|
+
{
|
|
105
|
+
"message": "Path is required for analysis",
|
|
106
|
+
"request_id": data.get("request_id"),
|
|
107
|
+
},
|
|
108
|
+
room=sid,
|
|
109
|
+
)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Generate request ID if not provided
|
|
113
|
+
request_id = data.get("request_id") or str(uuid.uuid4())
|
|
114
|
+
|
|
115
|
+
# Extract options
|
|
116
|
+
languages = data.get("languages")
|
|
117
|
+
max_depth = data.get("max_depth")
|
|
118
|
+
ignore_patterns = data.get("ignore_patterns")
|
|
119
|
+
|
|
120
|
+
# Queue analysis request
|
|
121
|
+
success = self.analysis_runner.request_analysis(
|
|
122
|
+
request_id=request_id,
|
|
123
|
+
path=path,
|
|
124
|
+
languages=languages,
|
|
125
|
+
max_depth=max_depth,
|
|
126
|
+
ignore_patterns=ignore_patterns,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if success:
|
|
130
|
+
# Send acknowledgment to requesting client
|
|
131
|
+
await self.server.sio.emit(
|
|
132
|
+
"code:analysis:accepted",
|
|
133
|
+
{
|
|
134
|
+
"request_id": request_id,
|
|
135
|
+
"path": path,
|
|
136
|
+
"message": "Analysis request queued",
|
|
137
|
+
},
|
|
138
|
+
room=sid,
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
# Send error if request failed
|
|
142
|
+
await self.server.sio.emit(
|
|
143
|
+
"code:analysis:error",
|
|
144
|
+
{
|
|
145
|
+
"request_id": request_id,
|
|
146
|
+
"message": "Failed to queue analysis request",
|
|
147
|
+
},
|
|
148
|
+
room=sid,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def handle_cancel_request(self, sid: str, data: Dict[str, Any]):
|
|
152
|
+
"""Handle analysis cancellation request.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
sid: Socket ID of the requesting client
|
|
156
|
+
data: Request data (may contain request_id)
|
|
157
|
+
"""
|
|
158
|
+
self.logger.info(f"Analysis cancellation requested from {sid}")
|
|
159
|
+
|
|
160
|
+
# Cancel current analysis
|
|
161
|
+
self.analysis_runner.cancel_current()
|
|
162
|
+
|
|
163
|
+
# Send confirmation
|
|
164
|
+
await self.server.sio.emit(
|
|
165
|
+
"code:analysis:cancelled",
|
|
166
|
+
{"message": "Analysis cancelled", "request_id": data.get("request_id")},
|
|
167
|
+
room=sid,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def handle_status_request(self, sid: str, data: Dict[str, Any]):
|
|
171
|
+
"""Handle status request from client.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
sid: Socket ID of the requesting client
|
|
175
|
+
data: Request data (unused)
|
|
176
|
+
"""
|
|
177
|
+
status = self.analysis_runner.get_status()
|
|
178
|
+
|
|
179
|
+
# Send status to requesting client
|
|
180
|
+
await self.server.sio.emit("code:analysis:status", status, room=sid)
|
|
181
|
+
|
|
182
|
+
async def handle_discover_top_level(self, sid: str, data: Dict[str, Any]):
|
|
183
|
+
"""Handle top-level directory discovery request for lazy loading.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
sid: Socket ID of the requesting client
|
|
187
|
+
data: Request data containing path and options
|
|
188
|
+
"""
|
|
189
|
+
self.logger.info(f"Top-level discovery requested from {sid}: {data}")
|
|
190
|
+
|
|
191
|
+
# Get path - this MUST be an absolute path from the frontend
|
|
192
|
+
path = data.get("path")
|
|
193
|
+
if not path:
|
|
194
|
+
await self.server.core.sio.emit(
|
|
195
|
+
"code:analysis:error",
|
|
196
|
+
{
|
|
197
|
+
"error": "Path is required for top-level discovery",
|
|
198
|
+
"request_id": data.get("request_id"),
|
|
199
|
+
},
|
|
200
|
+
room=sid,
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# CRITICAL: Never use "." or allow relative paths
|
|
205
|
+
# The frontend must send the absolute working directory
|
|
206
|
+
if path in (".", "..", "/") or not Path(path).is_absolute():
|
|
207
|
+
self.logger.warning(f"Invalid path for discovery: {path}")
|
|
208
|
+
await self.server.core.sio.emit(
|
|
209
|
+
"code:analysis:error",
|
|
210
|
+
{
|
|
211
|
+
"error": f"Invalid path for discovery: {path}. Must be an absolute path.",
|
|
212
|
+
"request_id": data.get("request_id"),
|
|
213
|
+
"path": path,
|
|
214
|
+
},
|
|
215
|
+
room=sid,
|
|
216
|
+
)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
# ADDITIONAL SECURITY: Ensure path is within working directory bounds
|
|
220
|
+
# This prevents access to system directories like /Users, /System, etc.
|
|
221
|
+
working_dir = Path.cwd().absolute()
|
|
222
|
+
try:
|
|
223
|
+
requested_path = Path(path).absolute()
|
|
224
|
+
# This will raise ValueError if path is not within working_dir
|
|
225
|
+
requested_path.relative_to(working_dir)
|
|
226
|
+
except ValueError:
|
|
227
|
+
self.logger.warning(
|
|
228
|
+
f"Access denied - path outside working directory: {path}"
|
|
229
|
+
)
|
|
230
|
+
await self.server.core.sio.emit(
|
|
231
|
+
"code:analysis:error",
|
|
232
|
+
{
|
|
233
|
+
"error": f"Access denied: Path outside working directory: {path}",
|
|
234
|
+
"request_id": data.get("request_id"),
|
|
235
|
+
"path": path,
|
|
236
|
+
},
|
|
237
|
+
room=sid,
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
ignore_patterns = data.get("ignore_patterns", [])
|
|
242
|
+
request_id = data.get("request_id")
|
|
243
|
+
show_hidden_files = data.get("show_hidden_files", False)
|
|
244
|
+
|
|
245
|
+
# Extensive debug logging
|
|
246
|
+
self.logger.info(f"[DEBUG] handle_discover_top_level START")
|
|
247
|
+
self.logger.info(f"[DEBUG] Received show_hidden_files={show_hidden_files} (type: {type(show_hidden_files)})")
|
|
248
|
+
self.logger.info(f"[DEBUG] Current analyzer exists: {self.code_analyzer is not None}")
|
|
249
|
+
if self.code_analyzer:
|
|
250
|
+
current_value = getattr(self.code_analyzer, 'show_hidden_files', 'NOT_FOUND')
|
|
251
|
+
self.logger.info(f"[DEBUG] Current analyzer show_hidden_files={current_value}")
|
|
252
|
+
self.logger.info(f"[DEBUG] Full request data: {data}")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# Create analyzer if needed or recreate if show_hidden_files changed
|
|
256
|
+
current_show_hidden = getattr(self.code_analyzer, 'show_hidden_files', None) if self.code_analyzer else None
|
|
257
|
+
need_recreate = (
|
|
258
|
+
not self.code_analyzer or
|
|
259
|
+
current_show_hidden != show_hidden_files
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
self.logger.info(f"[DEBUG] Analyzer recreation check:")
|
|
263
|
+
self.logger.info(f"[DEBUG] - Analyzer exists: {self.code_analyzer is not None}")
|
|
264
|
+
self.logger.info(f"[DEBUG] - Current show_hidden: {current_show_hidden}")
|
|
265
|
+
self.logger.info(f"[DEBUG] - Requested show_hidden: {show_hidden_files}")
|
|
266
|
+
self.logger.info(f"[DEBUG] - Need recreate: {need_recreate}")
|
|
267
|
+
|
|
268
|
+
if need_recreate:
|
|
269
|
+
# Create a custom emitter that sends to Socket.IO
|
|
270
|
+
emitter = CodeTreeEventEmitter(use_stdout=False)
|
|
271
|
+
# Override emit method to send to Socket.IO
|
|
272
|
+
original_emit = emitter.emit
|
|
273
|
+
|
|
274
|
+
def socket_emit(
|
|
275
|
+
event_type: str, event_data: Dict[str, Any], batch: bool = False
|
|
276
|
+
):
|
|
277
|
+
# Keep the original event format with colons - frontend expects this!
|
|
278
|
+
# The frontend listens for 'code:directory:discovered' not 'code.directory.discovered'
|
|
279
|
+
|
|
280
|
+
# Special handling for 'info' events - they should be passed through directly
|
|
281
|
+
if event_type == 'info':
|
|
282
|
+
# INFO events for granular tracking
|
|
283
|
+
loop = asyncio.get_event_loop()
|
|
284
|
+
loop.create_task(
|
|
285
|
+
self.server.core.sio.emit(
|
|
286
|
+
'info', {"request_id": request_id, **event_data}
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
# Regular code analysis events
|
|
291
|
+
loop = asyncio.get_event_loop()
|
|
292
|
+
loop.create_task(
|
|
293
|
+
self.server.core.sio.emit(
|
|
294
|
+
event_type, {"request_id": request_id, **event_data}
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
# Call original for stats tracking
|
|
298
|
+
original_emit(event_type, event_data, batch)
|
|
299
|
+
|
|
300
|
+
emitter.emit = socket_emit
|
|
301
|
+
# Initialize CodeTreeAnalyzer with emitter keyword argument and show_hidden_files
|
|
302
|
+
self.logger.info(f"[DEBUG] Creating new CodeTreeAnalyzer with show_hidden_files={show_hidden_files}")
|
|
303
|
+
self.code_analyzer = CodeTreeAnalyzer(emitter=emitter, show_hidden_files=show_hidden_files)
|
|
304
|
+
self.logger.info(f"[DEBUG] CodeTreeAnalyzer created:")
|
|
305
|
+
self.logger.info(f"[DEBUG] - analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
|
|
306
|
+
self.logger.info(f"[DEBUG] - gitignore_manager.show_hidden_files={self.code_analyzer.gitignore_manager.show_hidden_files}")
|
|
307
|
+
else:
|
|
308
|
+
self.logger.info(f"[DEBUG] Reusing existing analyzer with show_hidden_files={self.code_analyzer.show_hidden_files}")
|
|
309
|
+
|
|
310
|
+
# Use the provided path as-is - the frontend sends the absolute path
|
|
311
|
+
# Make sure we're using an absolute path
|
|
312
|
+
directory = Path(path)
|
|
313
|
+
|
|
314
|
+
# Validate that the path exists and is a directory
|
|
315
|
+
if not directory.exists():
|
|
316
|
+
await self.server.core.sio.emit(
|
|
317
|
+
"code:analysis:error",
|
|
318
|
+
{
|
|
319
|
+
"request_id": request_id,
|
|
320
|
+
"path": path,
|
|
321
|
+
"error": f"Directory does not exist: {path}",
|
|
322
|
+
},
|
|
323
|
+
room=sid,
|
|
324
|
+
)
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
if not directory.is_dir():
|
|
328
|
+
await self.server.core.sio.emit(
|
|
329
|
+
"code:analysis:error",
|
|
330
|
+
{
|
|
331
|
+
"request_id": request_id,
|
|
332
|
+
"path": path,
|
|
333
|
+
"error": f"Path is not a directory: {path}",
|
|
334
|
+
},
|
|
335
|
+
room=sid,
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Log what we're actually discovering
|
|
340
|
+
self.logger.info(
|
|
341
|
+
f"Discovering top-level contents of: {directory.absolute()}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Log before discovery
|
|
345
|
+
self.logger.info(f"[DEBUG] About to discover with analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
|
|
346
|
+
|
|
347
|
+
result = self.code_analyzer.discover_top_level(directory, ignore_patterns)
|
|
348
|
+
|
|
349
|
+
# Log what we got back
|
|
350
|
+
num_items = len(result.get("children", []))
|
|
351
|
+
dotfiles = [c for c in result.get("children", []) if c.get("name", "").startswith(".")]
|
|
352
|
+
self.logger.info(f"[DEBUG] Discovery result: {num_items} items, {len(dotfiles)} dotfiles")
|
|
353
|
+
if dotfiles:
|
|
354
|
+
self.logger.info(f"[DEBUG] Dotfiles found: {[d.get('name') for d in dotfiles]}")
|
|
355
|
+
|
|
356
|
+
# Send result to client with correct event name for top level discovery
|
|
357
|
+
await self.server.core.sio.emit(
|
|
358
|
+
"code:top_level:discovered",
|
|
359
|
+
{
|
|
360
|
+
"request_id": request_id,
|
|
361
|
+
"path": str(directory),
|
|
362
|
+
"items": result.get("children", []),
|
|
363
|
+
"stats": {
|
|
364
|
+
"files": len(
|
|
365
|
+
[
|
|
366
|
+
c
|
|
367
|
+
for c in result.get("children", [])
|
|
368
|
+
if c.get("type") == "file"
|
|
369
|
+
]
|
|
370
|
+
),
|
|
371
|
+
"directories": len(
|
|
372
|
+
[
|
|
373
|
+
c
|
|
374
|
+
for c in result.get("children", [])
|
|
375
|
+
if c.get("type") == "directory"
|
|
376
|
+
]
|
|
377
|
+
),
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
room=sid,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
self.logger.error(f"Error discovering top level: {e}")
|
|
385
|
+
await self.server.core.sio.emit(
|
|
386
|
+
"code:analysis:error",
|
|
387
|
+
{
|
|
388
|
+
"request_id": request_id,
|
|
389
|
+
"path": path,
|
|
390
|
+
"error": str(e),
|
|
391
|
+
},
|
|
392
|
+
room=sid,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
async def handle_discover_directory(self, sid: str, data: Dict[str, Any]):
|
|
396
|
+
"""Handle directory discovery request for lazy loading.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
sid: Socket ID of the requesting client
|
|
400
|
+
data: Request data containing directory path
|
|
401
|
+
"""
|
|
402
|
+
self.logger.info(f"Directory discovery requested from {sid}: {data}")
|
|
403
|
+
|
|
404
|
+
path = data.get("path")
|
|
405
|
+
ignore_patterns = data.get("ignore_patterns", [])
|
|
406
|
+
request_id = data.get("request_id")
|
|
407
|
+
show_hidden_files = data.get("show_hidden_files", False)
|
|
408
|
+
|
|
409
|
+
if not path:
|
|
410
|
+
await self.server.core.sio.emit(
|
|
411
|
+
"code:analysis:error",
|
|
412
|
+
{
|
|
413
|
+
"request_id": request_id,
|
|
414
|
+
"error": "Path is required",
|
|
415
|
+
},
|
|
416
|
+
room=sid,
|
|
417
|
+
)
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
# CRITICAL SECURITY FIX: Add path validation to prevent filesystem traversal
|
|
421
|
+
# The same validation logic as handle_discover_top_level
|
|
422
|
+
if path in (".", "..", "/") or not Path(path).is_absolute():
|
|
423
|
+
self.logger.warning(f"Invalid path for directory discovery: {path}")
|
|
424
|
+
await self.server.core.sio.emit(
|
|
425
|
+
"code:analysis:error",
|
|
426
|
+
{
|
|
427
|
+
"error": f"Invalid path for discovery: {path}. Must be an absolute path.",
|
|
428
|
+
"request_id": request_id,
|
|
429
|
+
"path": path,
|
|
430
|
+
},
|
|
431
|
+
room=sid,
|
|
432
|
+
)
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
# ADDITIONAL SECURITY: Ensure path is within working directory bounds
|
|
436
|
+
# This prevents access to system directories like /Users, /System, etc.
|
|
437
|
+
working_dir = Path.cwd().absolute()
|
|
438
|
+
try:
|
|
439
|
+
requested_path = Path(path).absolute()
|
|
440
|
+
# This will raise ValueError if path is not within working_dir
|
|
441
|
+
requested_path.relative_to(working_dir)
|
|
442
|
+
except ValueError:
|
|
443
|
+
self.logger.warning(
|
|
444
|
+
f"Access denied - path outside working directory: {path}"
|
|
445
|
+
)
|
|
446
|
+
await self.server.core.sio.emit(
|
|
447
|
+
"code:analysis:error",
|
|
448
|
+
{
|
|
449
|
+
"error": f"Access denied: Path outside working directory: {path}",
|
|
450
|
+
"request_id": request_id,
|
|
451
|
+
"path": path,
|
|
452
|
+
},
|
|
453
|
+
room=sid,
|
|
454
|
+
)
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
# Ensure analyzer exists or recreate if show_hidden_files changed
|
|
459
|
+
current_show_hidden = getattr(self.code_analyzer, 'show_hidden_files', None) if self.code_analyzer else None
|
|
460
|
+
need_recreate = (
|
|
461
|
+
not self.code_analyzer or
|
|
462
|
+
current_show_hidden != show_hidden_files
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
self.logger.info(f"[DEBUG] Analyzer recreation check:")
|
|
466
|
+
self.logger.info(f"[DEBUG] - Analyzer exists: {self.code_analyzer is not None}")
|
|
467
|
+
self.logger.info(f"[DEBUG] - Current show_hidden: {current_show_hidden}")
|
|
468
|
+
self.logger.info(f"[DEBUG] - Requested show_hidden: {show_hidden_files}")
|
|
469
|
+
self.logger.info(f"[DEBUG] - Need recreate: {need_recreate}")
|
|
470
|
+
|
|
471
|
+
if need_recreate:
|
|
472
|
+
emitter = CodeTreeEventEmitter(use_stdout=False)
|
|
473
|
+
# Override emit method to send to Socket.IO
|
|
474
|
+
original_emit = emitter.emit
|
|
475
|
+
|
|
476
|
+
def socket_emit(
|
|
477
|
+
event_type: str, event_data: Dict[str, Any], batch: bool = False
|
|
478
|
+
):
|
|
479
|
+
# Keep the original event format with colons - frontend expects this!
|
|
480
|
+
# The frontend listens for 'code:directory:discovered' not 'code.directory.discovered'
|
|
481
|
+
|
|
482
|
+
# Special handling for 'info' events - they should be passed through directly
|
|
483
|
+
if event_type == 'info':
|
|
484
|
+
# INFO events for granular tracking
|
|
485
|
+
loop = asyncio.get_event_loop()
|
|
486
|
+
loop.create_task(
|
|
487
|
+
self.server.core.sio.emit(
|
|
488
|
+
'info', {"request_id": request_id, **event_data}
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
# Regular code analysis events
|
|
493
|
+
loop = asyncio.get_event_loop()
|
|
494
|
+
loop.create_task(
|
|
495
|
+
self.server.core.sio.emit(
|
|
496
|
+
event_type, {"request_id": request_id, **event_data}
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
original_emit(event_type, event_data, batch)
|
|
500
|
+
|
|
501
|
+
emitter.emit = socket_emit
|
|
502
|
+
# Initialize CodeTreeAnalyzer with emitter keyword argument and show_hidden_files
|
|
503
|
+
self.logger.info(f"[DEBUG] Creating new CodeTreeAnalyzer with show_hidden_files={show_hidden_files}")
|
|
504
|
+
self.code_analyzer = CodeTreeAnalyzer(emitter=emitter, show_hidden_files=show_hidden_files)
|
|
505
|
+
self.logger.info(f"[DEBUG] CodeTreeAnalyzer created, analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
|
|
506
|
+
self.logger.info(f"[DEBUG] GitignoreManager.show_hidden_files={self.code_analyzer.gitignore_manager.show_hidden_files}")
|
|
507
|
+
else:
|
|
508
|
+
self.logger.info(f"[DEBUG] Reusing analyzer with show_hidden_files={self.code_analyzer.show_hidden_files}")
|
|
509
|
+
|
|
510
|
+
# Discover directory
|
|
511
|
+
result = self.code_analyzer.discover_directory(path, ignore_patterns)
|
|
512
|
+
|
|
513
|
+
# Send result with correct event name (using colons, not dots!)
|
|
514
|
+
await self.server.core.sio.emit(
|
|
515
|
+
"code:directory:discovered",
|
|
516
|
+
{
|
|
517
|
+
"request_id": request_id,
|
|
518
|
+
"path": path,
|
|
519
|
+
**result,
|
|
520
|
+
},
|
|
521
|
+
room=sid,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
except Exception as e:
|
|
525
|
+
self.logger.error(f"Error discovering directory {path}: {e}")
|
|
526
|
+
await self.server.core.sio.emit(
|
|
527
|
+
"code:analysis:error",
|
|
528
|
+
{
|
|
529
|
+
"request_id": request_id,
|
|
530
|
+
"path": path,
|
|
531
|
+
"error": str(e),
|
|
532
|
+
},
|
|
533
|
+
room=sid,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
async def handle_analyze_file(self, sid: str, data: Dict[str, Any]):
|
|
537
|
+
"""Handle file analysis request for lazy loading.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
sid: Socket ID of the requesting client
|
|
541
|
+
data: Request data containing file path
|
|
542
|
+
"""
|
|
543
|
+
self.logger.info(f"File analysis requested from {sid}: {data}")
|
|
544
|
+
|
|
545
|
+
path = data.get("path")
|
|
546
|
+
request_id = data.get("request_id")
|
|
547
|
+
show_hidden_files = data.get("show_hidden_files", False)
|
|
548
|
+
|
|
549
|
+
if not path:
|
|
550
|
+
await self.server.core.sio.emit(
|
|
551
|
+
"code:analysis:error",
|
|
552
|
+
{
|
|
553
|
+
"request_id": request_id,
|
|
554
|
+
"error": "Path is required",
|
|
555
|
+
},
|
|
556
|
+
room=sid,
|
|
557
|
+
)
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
# CRITICAL SECURITY FIX: Add path validation to prevent filesystem traversal
|
|
561
|
+
if path in (".", "..", "/") or not Path(path).is_absolute():
|
|
562
|
+
self.logger.warning(f"Invalid path for file analysis: {path}")
|
|
563
|
+
await self.server.core.sio.emit(
|
|
564
|
+
"code:analysis:error",
|
|
565
|
+
{
|
|
566
|
+
"error": f"Invalid path for analysis: {path}. Must be an absolute path.",
|
|
567
|
+
"request_id": request_id,
|
|
568
|
+
"path": path,
|
|
569
|
+
},
|
|
570
|
+
room=sid,
|
|
571
|
+
)
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
# ADDITIONAL SECURITY: Ensure file is within working directory bounds
|
|
575
|
+
working_dir = Path.cwd().absolute()
|
|
576
|
+
try:
|
|
577
|
+
requested_path = Path(path).absolute()
|
|
578
|
+
# This will raise ValueError if path is not within working_dir
|
|
579
|
+
requested_path.relative_to(working_dir)
|
|
580
|
+
except ValueError:
|
|
581
|
+
self.logger.warning(
|
|
582
|
+
f"Access denied - file outside working directory: {path}"
|
|
583
|
+
)
|
|
584
|
+
await self.server.core.sio.emit(
|
|
585
|
+
"code:analysis:error",
|
|
586
|
+
{
|
|
587
|
+
"error": f"Access denied: File outside working directory: {path}",
|
|
588
|
+
"request_id": request_id,
|
|
589
|
+
"path": path,
|
|
590
|
+
},
|
|
591
|
+
room=sid,
|
|
592
|
+
)
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
# Ensure analyzer exists or recreate if show_hidden_files changed
|
|
597
|
+
current_show_hidden = getattr(self.code_analyzer, 'show_hidden_files', None) if self.code_analyzer else None
|
|
598
|
+
need_recreate = (
|
|
599
|
+
not self.code_analyzer or
|
|
600
|
+
current_show_hidden != show_hidden_files
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
self.logger.info(f"[DEBUG] Analyzer recreation check:")
|
|
604
|
+
self.logger.info(f"[DEBUG] - Analyzer exists: {self.code_analyzer is not None}")
|
|
605
|
+
self.logger.info(f"[DEBUG] - Current show_hidden: {current_show_hidden}")
|
|
606
|
+
self.logger.info(f"[DEBUG] - Requested show_hidden: {show_hidden_files}")
|
|
607
|
+
self.logger.info(f"[DEBUG] - Need recreate: {need_recreate}")
|
|
608
|
+
|
|
609
|
+
if need_recreate:
|
|
610
|
+
emitter = CodeTreeEventEmitter(use_stdout=False)
|
|
611
|
+
# Override emit method to send to Socket.IO
|
|
612
|
+
original_emit = emitter.emit
|
|
613
|
+
|
|
614
|
+
def socket_emit(
|
|
615
|
+
event_type: str, event_data: Dict[str, Any], batch: bool = False
|
|
616
|
+
):
|
|
617
|
+
# Keep the original event format with colons - frontend expects this!
|
|
618
|
+
# The frontend listens for 'code:file:analyzed' not 'code.file.analyzed'
|
|
619
|
+
|
|
620
|
+
# Special handling for 'info' events - they should be passed through directly
|
|
621
|
+
if event_type == 'info':
|
|
622
|
+
# INFO events for granular tracking
|
|
623
|
+
loop = asyncio.get_event_loop()
|
|
624
|
+
loop.create_task(
|
|
625
|
+
self.server.core.sio.emit(
|
|
626
|
+
'info', {"request_id": request_id, **event_data}
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
else:
|
|
630
|
+
# Regular code analysis events
|
|
631
|
+
loop = asyncio.get_event_loop()
|
|
632
|
+
loop.create_task(
|
|
633
|
+
self.server.core.sio.emit(
|
|
634
|
+
event_type, {"request_id": request_id, **event_data}
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
original_emit(event_type, event_data, batch)
|
|
638
|
+
|
|
639
|
+
emitter.emit = socket_emit
|
|
640
|
+
# Initialize CodeTreeAnalyzer with emitter keyword argument and show_hidden_files
|
|
641
|
+
self.logger.info(f"[DEBUG] Creating new CodeTreeAnalyzer with show_hidden_files={show_hidden_files}")
|
|
642
|
+
self.code_analyzer = CodeTreeAnalyzer(emitter=emitter, show_hidden_files=show_hidden_files)
|
|
643
|
+
self.logger.info(f"[DEBUG] CodeTreeAnalyzer created, analyzer.show_hidden_files={self.code_analyzer.show_hidden_files}")
|
|
644
|
+
self.logger.info(f"[DEBUG] GitignoreManager.show_hidden_files={self.code_analyzer.gitignore_manager.show_hidden_files}")
|
|
645
|
+
else:
|
|
646
|
+
self.logger.info(f"[DEBUG] Reusing analyzer with show_hidden_files={self.code_analyzer.show_hidden_files}")
|
|
647
|
+
|
|
648
|
+
# Analyze file
|
|
649
|
+
result = self.code_analyzer.analyze_file(path)
|
|
650
|
+
|
|
651
|
+
# Send result with correct event name (using colons, not dots!)
|
|
652
|
+
await self.server.core.sio.emit(
|
|
653
|
+
"code:file:analyzed",
|
|
654
|
+
{
|
|
655
|
+
"request_id": request_id,
|
|
656
|
+
"path": path,
|
|
657
|
+
**result,
|
|
658
|
+
},
|
|
659
|
+
room=sid,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
except Exception as e:
|
|
663
|
+
self.logger.error(f"Error analyzing file {path}: {e}")
|
|
664
|
+
await self.server.core.sio.emit(
|
|
665
|
+
"code:analysis:error",
|
|
666
|
+
{
|
|
667
|
+
"request_id": request_id,
|
|
668
|
+
"path": path,
|
|
669
|
+
"error": str(e),
|
|
670
|
+
},
|
|
671
|
+
room=sid,
|
|
672
|
+
)
|
|
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
|
|
15
15
|
|
|
16
16
|
from ..server import SocketIOServer
|
|
17
17
|
|
|
18
|
+
from .code_analysis import CodeAnalysisEventHandler
|
|
18
19
|
from .connection import ConnectionEventHandler
|
|
19
20
|
from .file import FileEventHandler
|
|
20
21
|
from .git import GitEventHandler
|
|
@@ -37,6 +38,7 @@ class EventHandlerRegistry:
|
|
|
37
38
|
HookEventHandler, # Hook events for session tracking
|
|
38
39
|
GitEventHandler, # Git operations
|
|
39
40
|
FileEventHandler, # File operations
|
|
41
|
+
CodeAnalysisEventHandler, # Code analysis for dashboard
|
|
40
42
|
ProjectEventHandler, # Project management (future)
|
|
41
43
|
MemoryEventHandler, # Memory management (future)
|
|
42
44
|
]
|