claude-mpm 4.1.10__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 (48) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/__init__.py +11 -0
  3. claude_mpm/cli/commands/analyze.py +2 -1
  4. claude_mpm/cli/commands/configure.py +9 -8
  5. claude_mpm/cli/commands/configure_tui.py +3 -1
  6. claude_mpm/cli/commands/dashboard.py +288 -0
  7. claude_mpm/cli/commands/debug.py +0 -1
  8. claude_mpm/cli/commands/mpm_init.py +427 -0
  9. claude_mpm/cli/commands/mpm_init_handler.py +83 -0
  10. claude_mpm/cli/parsers/base_parser.py +15 -0
  11. claude_mpm/cli/parsers/dashboard_parser.py +113 -0
  12. claude_mpm/cli/parsers/mpm_init_parser.py +122 -0
  13. claude_mpm/constants.py +10 -0
  14. claude_mpm/dashboard/analysis_runner.py +52 -25
  15. claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
  16. claude_mpm/dashboard/static/built/components/code-tree.js +2 -0
  17. claude_mpm/dashboard/static/built/components/code-viewer.js +2 -0
  18. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  19. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  20. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  21. claude_mpm/dashboard/static/css/code-tree.css +330 -1
  22. claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
  23. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  24. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  25. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  26. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  27. claude_mpm/dashboard/static/js/components/activity-tree.js +212 -13
  28. claude_mpm/dashboard/static/js/components/code-tree.js +1999 -821
  29. claude_mpm/dashboard/static/js/components/event-viewer.js +58 -19
  30. claude_mpm/dashboard/static/js/dashboard.js +15 -3
  31. claude_mpm/dashboard/static/js/socket-client.js +74 -32
  32. claude_mpm/dashboard/templates/index.html +9 -11
  33. claude_mpm/services/agents/memory/memory_format_service.py +3 -1
  34. claude_mpm/services/cli/agent_cleanup_service.py +1 -4
  35. claude_mpm/services/cli/startup_checker.py +0 -1
  36. claude_mpm/services/core/cache_manager.py +0 -1
  37. claude_mpm/services/socketio/event_normalizer.py +64 -0
  38. claude_mpm/services/socketio/handlers/code_analysis.py +502 -0
  39. claude_mpm/services/socketio/server/connection_manager.py +3 -1
  40. claude_mpm/tools/code_tree_analyzer.py +843 -25
  41. claude_mpm/tools/code_tree_builder.py +0 -1
  42. claude_mpm/tools/code_tree_events.py +113 -15
  43. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.11.dist-info}/METADATA +2 -1
  44. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.11.dist-info}/RECORD +48 -41
  45. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.11.dist-info}/WHEEL +0 -0
  46. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.11.dist-info}/entry_points.txt +0 -0
  47. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.11.dist-info}/licenses/LICENSE +0 -0
  48. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.11.dist-info}/top_level.txt +0 -0
@@ -12,11 +12,15 @@ DESIGN DECISIONS:
12
12
  - Stream events in real-time to all connected clients
13
13
  """
14
14
 
15
+ import asyncio
15
16
  import uuid
17
+ from pathlib import Path
16
18
  from typing import Any, Dict
17
19
 
18
20
  from ....core.logging_config import get_logger
19
21
  from ....dashboard.analysis_runner import CodeAnalysisRunner
22
+ from ....tools.code_tree_analyzer import CodeTreeAnalyzer
23
+ from ....tools.code_tree_events import CodeTreeEventEmitter
20
24
  from .base import BaseEventHandler
21
25
 
22
26
 
@@ -36,6 +40,7 @@ class CodeAnalysisEventHandler(BaseEventHandler):
36
40
  super().__init__(server)
37
41
  self.logger = get_logger(__name__)
38
42
  self.analysis_runner = None
43
+ self.code_analyzer = None # For lazy loading operations
39
44
 
40
45
  def initialize(self):
41
46
  """Initialize the analysis runner."""
@@ -58,9 +63,14 @@ class CodeAnalysisEventHandler(BaseEventHandler):
58
63
  Dictionary mapping event names to handler methods
59
64
  """
60
65
  return {
66
+ # Legacy full analysis
61
67
  "code:analyze:request": self.handle_analyze_request,
62
68
  "code:analyze:cancel": self.handle_cancel_request,
63
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,
64
74
  }
65
75
 
66
76
  def register_events(self) -> None:
@@ -168,3 +178,495 @@ class CodeAnalysisEventHandler(BaseEventHandler):
168
178
 
169
179
  # Send status to requesting client
170
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
+ )
@@ -149,7 +149,9 @@ class ConnectionManager:
149
149
  - Automatic event replay on reconnection
150
150
  """
151
151
 
152
- def __init__(self, max_buffer_size: Optional[int] = None, event_ttl: Optional[int] = None):
152
+ def __init__(
153
+ self, max_buffer_size: Optional[int] = None, event_ttl: Optional[int] = None
154
+ ):
153
155
  """
154
156
  Initialize connection manager with centralized configuration.
155
157