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.
Files changed (111) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +26 -1
  3. claude_mpm/agents/agents_metadata.py +57 -0
  4. claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
  5. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
  6. claude_mpm/agents/templates/agent-manager.json +263 -17
  7. claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
  8. claude_mpm/agents/templates/code_analyzer.json +18 -8
  9. claude_mpm/agents/templates/engineer.json +1 -1
  10. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
  11. claude_mpm/agents/templates/qa.json +1 -1
  12. claude_mpm/agents/templates/research.json +1 -1
  13. claude_mpm/cli/__init__.py +15 -0
  14. claude_mpm/cli/commands/__init__.py +6 -0
  15. claude_mpm/cli/commands/analyze.py +548 -0
  16. claude_mpm/cli/commands/analyze_code.py +524 -0
  17. claude_mpm/cli/commands/configure.py +78 -28
  18. claude_mpm/cli/commands/configure_tui.py +62 -60
  19. claude_mpm/cli/commands/dashboard.py +288 -0
  20. claude_mpm/cli/commands/debug.py +1386 -0
  21. claude_mpm/cli/commands/mpm_init.py +427 -0
  22. claude_mpm/cli/commands/mpm_init_handler.py +83 -0
  23. claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
  24. claude_mpm/cli/parsers/analyze_parser.py +135 -0
  25. claude_mpm/cli/parsers/base_parser.py +44 -0
  26. claude_mpm/cli/parsers/dashboard_parser.py +113 -0
  27. claude_mpm/cli/parsers/debug_parser.py +319 -0
  28. claude_mpm/cli/parsers/mpm_init_parser.py +122 -0
  29. claude_mpm/constants.py +13 -1
  30. claude_mpm/core/framework_loader.py +148 -6
  31. claude_mpm/core/log_manager.py +16 -13
  32. claude_mpm/core/logger.py +1 -1
  33. claude_mpm/core/unified_agent_registry.py +1 -1
  34. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
  35. claude_mpm/dashboard/analysis_runner.py +455 -0
  36. claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
  37. claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
  38. claude_mpm/dashboard/static/built/components/code-tree.js +2 -0
  39. claude_mpm/dashboard/static/built/components/code-viewer.js +2 -0
  40. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  41. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
  42. claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
  43. claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
  44. claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
  45. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  46. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  47. claude_mpm/dashboard/static/css/activity.css +549 -0
  48. claude_mpm/dashboard/static/css/code-tree.css +1175 -0
  49. claude_mpm/dashboard/static/css/dashboard.css +245 -0
  50. claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
  51. claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
  52. claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
  53. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  54. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  55. claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
  56. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  57. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  58. claude_mpm/dashboard/static/js/components/activity-tree.js +1338 -0
  59. claude_mpm/dashboard/static/js/components/code-tree.js +2535 -0
  60. claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
  61. claude_mpm/dashboard/static/js/components/event-viewer.js +59 -9
  62. claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
  63. claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
  64. claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
  65. claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
  66. claude_mpm/dashboard/static/js/dashboard.js +51 -0
  67. claude_mpm/dashboard/static/js/socket-client.js +465 -29
  68. claude_mpm/dashboard/templates/index.html +182 -4
  69. claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
  70. claude_mpm/hooks/claude_hooks/installer.py +386 -113
  71. claude_mpm/scripts/claude-hook-handler.sh +161 -0
  72. claude_mpm/scripts/socketio_daemon.py +121 -8
  73. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
  74. claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
  75. claude_mpm/services/agents/memory/memory_format_service.py +1 -3
  76. claude_mpm/services/cli/agent_cleanup_service.py +1 -5
  77. claude_mpm/services/cli/agent_dependency_service.py +1 -1
  78. claude_mpm/services/cli/agent_validation_service.py +3 -4
  79. claude_mpm/services/cli/dashboard_launcher.py +2 -3
  80. claude_mpm/services/cli/startup_checker.py +0 -11
  81. claude_mpm/services/core/cache_manager.py +1 -3
  82. claude_mpm/services/core/path_resolver.py +1 -4
  83. claude_mpm/services/core/service_container.py +2 -2
  84. claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
  85. claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
  86. claude_mpm/services/infrastructure/monitoring.py +11 -11
  87. claude_mpm/services/project/architecture_analyzer.py +1 -1
  88. claude_mpm/services/project/dependency_analyzer.py +4 -4
  89. claude_mpm/services/project/language_analyzer.py +3 -3
  90. claude_mpm/services/project/metrics_collector.py +3 -6
  91. claude_mpm/services/socketio/event_normalizer.py +64 -0
  92. claude_mpm/services/socketio/handlers/__init__.py +2 -0
  93. claude_mpm/services/socketio/handlers/code_analysis.py +672 -0
  94. claude_mpm/services/socketio/handlers/registry.py +2 -0
  95. claude_mpm/services/socketio/server/connection_manager.py +6 -4
  96. claude_mpm/services/socketio/server/core.py +100 -11
  97. claude_mpm/services/socketio/server/main.py +8 -2
  98. claude_mpm/services/visualization/__init__.py +19 -0
  99. claude_mpm/services/visualization/mermaid_generator.py +938 -0
  100. claude_mpm/tools/__main__.py +208 -0
  101. claude_mpm/tools/code_tree_analyzer.py +1596 -0
  102. claude_mpm/tools/code_tree_builder.py +631 -0
  103. claude_mpm/tools/code_tree_events.py +416 -0
  104. claude_mpm/tools/socketio_debug.py +671 -0
  105. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/METADATA +2 -1
  106. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/RECORD +110 -74
  107. claude_mpm/agents/schema/agent_schema.json +0 -314
  108. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/WHEEL +0 -0
  109. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/entry_points.txt +0 -0
  110. {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/licenses/LICENSE +0 -0
  111. {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
  ]