claude-mpm 3.3.2__py3-none-any.whl → 3.4.2__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/cli/commands/memory.py +192 -14
- claude_mpm/cli/parser.py +13 -1
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +61 -0
- claude_mpm/core/config.py +161 -1
- claude_mpm/core/simple_runner.py +61 -0
- claude_mpm/hooks/builtin/mpm_command_hook.py +5 -5
- claude_mpm/hooks/claude_hooks/hook_handler.py +211 -4
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +10 -3
- claude_mpm/hooks/memory_integration_hook.py +51 -5
- claude_mpm/scripts/socketio_daemon.py +49 -9
- claude_mpm/scripts/socketio_server_manager.py +370 -45
- claude_mpm/services/__init__.py +41 -5
- claude_mpm/services/agent_memory_manager.py +541 -51
- claude_mpm/services/exceptions.py +677 -0
- claude_mpm/services/health_monitor.py +892 -0
- claude_mpm/services/memory_builder.py +341 -7
- claude_mpm/services/memory_optimizer.py +6 -2
- claude_mpm/services/project_analyzer.py +771 -0
- claude_mpm/services/recovery_manager.py +670 -0
- claude_mpm/services/socketio_server.py +653 -36
- claude_mpm/services/standalone_socketio_server.py +703 -34
- claude_mpm/services/version_control/git_operations.py +26 -0
- {claude_mpm-3.3.2.dist-info → claude_mpm-3.4.2.dist-info}/METADATA +34 -10
- {claude_mpm-3.3.2.dist-info → claude_mpm-3.4.2.dist-info}/RECORD +30 -44
- claude_mpm/agents/agent-template.yaml +0 -83
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +0 -6
- claude_mpm/cli/README.md +0 -109
- claude_mpm/cli_module/refactoring_guide.md +0 -253
- claude_mpm/core/agent_registry.py.bak +0 -312
- claude_mpm/core/base_service.py.bak +0 -406
- claude_mpm/hooks/README.md +0 -97
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +0 -66
- claude_mpm/schemas/README_SECURITY.md +0 -92
- claude_mpm/schemas/agent_schema.json +0 -395
- claude_mpm/schemas/agent_schema_documentation.md +0 -181
- claude_mpm/schemas/agent_schema_security_notes.md +0 -165
- claude_mpm/schemas/examples/standard_workflow.json +0 -505
- claude_mpm/schemas/ticket_workflow_documentation.md +0 -482
- claude_mpm/schemas/ticket_workflow_schema.json +0 -590
- claude_mpm/services/framework_claude_md_generator/README.md +0 -92
- claude_mpm/services/parent_directory_manager/README.md +0 -83
- claude_mpm/services/version_control/VERSION +0 -1
- /claude_mpm/{web → dashboard}/open_dashboard.py +0 -0
- {claude_mpm-3.3.2.dist-info → claude_mpm-3.4.2.dist-info}/WHEEL +0 -0
- {claude_mpm-3.3.2.dist-info → claude_mpm-3.4.2.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.3.2.dist-info → claude_mpm-3.4.2.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.3.2.dist-info → claude_mpm-3.4.2.dist-info}/top_level.txt +0 -0
|
@@ -21,17 +21,14 @@ try:
|
|
|
21
21
|
import aiohttp
|
|
22
22
|
from aiohttp import web
|
|
23
23
|
SOCKETIO_AVAILABLE = True
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
print(f"Socket.IO server using python-socketio v{version}")
|
|
27
|
-
except:
|
|
28
|
-
print("Socket.IO server using python-socketio (version unavailable)")
|
|
24
|
+
# Don't print at module level - this causes output during imports
|
|
25
|
+
# Version will be logged when server is actually started
|
|
29
26
|
except ImportError:
|
|
30
27
|
SOCKETIO_AVAILABLE = False
|
|
31
28
|
socketio = None
|
|
32
29
|
aiohttp = None
|
|
33
30
|
web = None
|
|
34
|
-
|
|
31
|
+
# Don't print warnings at module level
|
|
35
32
|
|
|
36
33
|
from ..core.logger import get_logger
|
|
37
34
|
from ..deployment_paths import get_project_root, get_scripts_dir
|
|
@@ -53,6 +50,7 @@ class SocketIOClientProxy:
|
|
|
53
50
|
self.running = True # Always "running" for compatibility
|
|
54
51
|
self._sio_client = None
|
|
55
52
|
self._client_thread = None
|
|
53
|
+
self._client_loop = None
|
|
56
54
|
|
|
57
55
|
def start(self):
|
|
58
56
|
"""Start the Socket.IO client connection to the persistent server."""
|
|
@@ -69,12 +67,14 @@ class SocketIOClientProxy:
|
|
|
69
67
|
def _start_client(self):
|
|
70
68
|
"""Start Socket.IO client in a background thread."""
|
|
71
69
|
def run_client():
|
|
72
|
-
|
|
73
|
-
asyncio.set_event_loop(
|
|
70
|
+
self._client_loop = asyncio.new_event_loop()
|
|
71
|
+
asyncio.set_event_loop(self._client_loop)
|
|
74
72
|
try:
|
|
75
|
-
|
|
73
|
+
self._client_loop.run_until_complete(self._connect_and_run())
|
|
76
74
|
except Exception as e:
|
|
77
75
|
self.logger.error(f"SocketIOClientProxy client thread error: {e}")
|
|
76
|
+
finally:
|
|
77
|
+
self._client_loop.close()
|
|
78
78
|
|
|
79
79
|
self._client_thread = threading.Thread(target=run_client, daemon=True)
|
|
80
80
|
self._client_thread.start()
|
|
@@ -123,10 +123,19 @@ class SocketIOClientProxy:
|
|
|
123
123
|
"data": data
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
# Send
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
# Send event safely using run_coroutine_threadsafe
|
|
127
|
+
if hasattr(self, '_client_loop') and self._client_loop and not self._client_loop.is_closed():
|
|
128
|
+
try:
|
|
129
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
130
|
+
self._sio_client.emit('claude_event', event),
|
|
131
|
+
self._client_loop
|
|
132
|
+
)
|
|
133
|
+
# Don't wait for the result to avoid blocking
|
|
134
|
+
self.logger.debug(f"SocketIOClientProxy: Scheduled emit for {event_type}")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self.logger.error(f"SocketIOClientProxy: Failed to schedule emit for {event_type}: {e}")
|
|
137
|
+
else:
|
|
138
|
+
self.logger.warning(f"SocketIOClientProxy: Client event loop not available for {event_type}")
|
|
130
139
|
|
|
131
140
|
self.logger.debug(f"SocketIOClientProxy: Sent event {event_type}")
|
|
132
141
|
except Exception as e:
|
|
@@ -181,6 +190,13 @@ class SocketIOServer:
|
|
|
181
190
|
|
|
182
191
|
if not SOCKETIO_AVAILABLE:
|
|
183
192
|
self.logger.warning("Socket.IO support not available. Install 'python-socketio' and 'aiohttp' packages to enable.")
|
|
193
|
+
else:
|
|
194
|
+
# Log version info when server is actually created
|
|
195
|
+
try:
|
|
196
|
+
version = getattr(socketio, '__version__', 'unknown')
|
|
197
|
+
self.logger.info(f"Socket.IO server using python-socketio v{version}")
|
|
198
|
+
except:
|
|
199
|
+
self.logger.info("Socket.IO server using python-socketio (version unavailable)")
|
|
184
200
|
|
|
185
201
|
def start(self):
|
|
186
202
|
"""Start the Socket.IO server in a background thread."""
|
|
@@ -270,13 +286,15 @@ class SocketIOServer:
|
|
|
270
286
|
self.app.router.add_get('/status', self._handle_health)
|
|
271
287
|
self.app.router.add_get('/api/git-diff', self._handle_git_diff)
|
|
272
288
|
self.app.router.add_options('/api/git-diff', self._handle_cors_preflight)
|
|
289
|
+
self.app.router.add_get('/api/file-content', self._handle_file_content)
|
|
290
|
+
self.app.router.add_options('/api/file-content', self._handle_cors_preflight)
|
|
273
291
|
|
|
274
292
|
# Add dashboard routes
|
|
275
293
|
self.app.router.add_get('/', self._handle_dashboard)
|
|
276
294
|
self.app.router.add_get('/dashboard', self._handle_dashboard)
|
|
277
295
|
|
|
278
296
|
# Add static file serving for web assets
|
|
279
|
-
static_path = get_project_root() / 'src' / 'claude_mpm' / '
|
|
297
|
+
static_path = get_project_root() / 'src' / 'claude_mpm' / 'dashboard' / 'static'
|
|
280
298
|
if static_path.exists():
|
|
281
299
|
self.app.router.add_static('/static/', path=str(static_path), name='static')
|
|
282
300
|
|
|
@@ -330,7 +348,7 @@ class SocketIOServer:
|
|
|
330
348
|
|
|
331
349
|
async def _handle_dashboard(self, request):
|
|
332
350
|
"""Serve the dashboard HTML file."""
|
|
333
|
-
dashboard_path = get_project_root() / 'src' / 'claude_mpm' / '
|
|
351
|
+
dashboard_path = get_project_root() / 'src' / 'claude_mpm' / 'dashboard' / 'templates' / 'index.html'
|
|
334
352
|
self.logger.info(f"Dashboard requested, looking for: {dashboard_path}")
|
|
335
353
|
self.logger.info(f"Path exists: {dashboard_path.exists()}")
|
|
336
354
|
if dashboard_path.exists():
|
|
@@ -365,6 +383,7 @@ class SocketIOServer:
|
|
|
365
383
|
working_dir = request.query.get('working_dir', os.getcwd())
|
|
366
384
|
|
|
367
385
|
self.logger.info(f"Git diff API request: file={file_path}, timestamp={timestamp}, working_dir={working_dir}")
|
|
386
|
+
self.logger.info(f"Git diff request details: query_params={dict(request.query)}, file_exists={os.path.exists(file_path) if file_path else False}")
|
|
368
387
|
|
|
369
388
|
if not file_path:
|
|
370
389
|
self.logger.warning("Git diff request missing file parameter")
|
|
@@ -403,6 +422,198 @@ class SocketIOServer:
|
|
|
403
422
|
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
404
423
|
})
|
|
405
424
|
|
|
425
|
+
async def _handle_file_content(self, request):
|
|
426
|
+
"""Handle file content requests via HTTP API.
|
|
427
|
+
|
|
428
|
+
Expected query parameters:
|
|
429
|
+
- file_path: The file path to read
|
|
430
|
+
- working_dir: Working directory for file operations (optional)
|
|
431
|
+
- max_size: Maximum file size in bytes (optional, default 1MB)
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
# Extract query parameters
|
|
435
|
+
file_path = request.query.get('file_path')
|
|
436
|
+
working_dir = request.query.get('working_dir', os.getcwd())
|
|
437
|
+
max_size = int(request.query.get('max_size', 1024 * 1024)) # 1MB default
|
|
438
|
+
|
|
439
|
+
self.logger.info(f"File content API request: file_path={file_path}, working_dir={working_dir}")
|
|
440
|
+
|
|
441
|
+
if not file_path:
|
|
442
|
+
self.logger.warning("File content request missing file_path parameter")
|
|
443
|
+
return web.json_response({
|
|
444
|
+
"success": False,
|
|
445
|
+
"error": "Missing required parameter: file_path"
|
|
446
|
+
}, status=400, headers={
|
|
447
|
+
'Access-Control-Allow-Origin': '*',
|
|
448
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
449
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
# Use the same file reading logic as the Socket.IO handler
|
|
453
|
+
result = await self._read_file_safely(file_path, working_dir, max_size)
|
|
454
|
+
|
|
455
|
+
status_code = 200 if result.get('success') else 400
|
|
456
|
+
return web.json_response(result, status=status_code, headers={
|
|
457
|
+
'Access-Control-Allow-Origin': '*',
|
|
458
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
459
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
self.logger.error(f"Error reading file content: {e}")
|
|
464
|
+
import traceback
|
|
465
|
+
self.logger.error(f"File content error traceback: {traceback.format_exc()}")
|
|
466
|
+
return web.json_response({
|
|
467
|
+
"success": False,
|
|
468
|
+
"error": f"Failed to read file: {str(e)}"
|
|
469
|
+
}, status=500, headers={
|
|
470
|
+
'Access-Control-Allow-Origin': '*',
|
|
471
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
472
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
async def _read_file_safely(self, file_path: str, working_dir: str = None, max_size: int = 1024 * 1024):
|
|
476
|
+
"""Safely read file content with security checks.
|
|
477
|
+
|
|
478
|
+
This method contains the core file reading logic that can be used by both
|
|
479
|
+
HTTP API endpoints and Socket.IO event handlers.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
file_path: Path to the file to read
|
|
483
|
+
working_dir: Working directory (defaults to current directory)
|
|
484
|
+
max_size: Maximum file size in bytes
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
dict: Response with success status, content, and metadata
|
|
488
|
+
"""
|
|
489
|
+
try:
|
|
490
|
+
if working_dir is None:
|
|
491
|
+
working_dir = os.getcwd()
|
|
492
|
+
|
|
493
|
+
# Resolve absolute path based on working directory
|
|
494
|
+
if not os.path.isabs(file_path):
|
|
495
|
+
full_path = os.path.join(working_dir, file_path)
|
|
496
|
+
else:
|
|
497
|
+
full_path = file_path
|
|
498
|
+
|
|
499
|
+
# Security check: ensure file is within working directory or project
|
|
500
|
+
try:
|
|
501
|
+
real_path = os.path.realpath(full_path)
|
|
502
|
+
real_working_dir = os.path.realpath(working_dir)
|
|
503
|
+
|
|
504
|
+
# Allow access to files within working directory or the project root
|
|
505
|
+
project_root = os.path.realpath(get_project_root())
|
|
506
|
+
allowed_paths = [real_working_dir, project_root]
|
|
507
|
+
|
|
508
|
+
is_allowed = any(real_path.startswith(allowed_path) for allowed_path in allowed_paths)
|
|
509
|
+
|
|
510
|
+
if not is_allowed:
|
|
511
|
+
return {
|
|
512
|
+
'success': False,
|
|
513
|
+
'error': 'Access denied: file is outside allowed directories',
|
|
514
|
+
'file_path': file_path
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
except Exception as path_error:
|
|
518
|
+
self.logger.error(f"Path validation error: {path_error}")
|
|
519
|
+
return {
|
|
520
|
+
'success': False,
|
|
521
|
+
'error': 'Invalid file path',
|
|
522
|
+
'file_path': file_path
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# Check if file exists
|
|
526
|
+
if not os.path.exists(real_path):
|
|
527
|
+
return {
|
|
528
|
+
'success': False,
|
|
529
|
+
'error': 'File does not exist',
|
|
530
|
+
'file_path': file_path
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
# Check if it's a file (not directory)
|
|
534
|
+
if not os.path.isfile(real_path):
|
|
535
|
+
return {
|
|
536
|
+
'success': False,
|
|
537
|
+
'error': 'Path is not a file',
|
|
538
|
+
'file_path': file_path
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
# Check file size
|
|
542
|
+
file_size = os.path.getsize(real_path)
|
|
543
|
+
if file_size > max_size:
|
|
544
|
+
return {
|
|
545
|
+
'success': False,
|
|
546
|
+
'error': f'File too large ({file_size} bytes). Maximum allowed: {max_size} bytes',
|
|
547
|
+
'file_path': file_path,
|
|
548
|
+
'file_size': file_size
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
# Read file content
|
|
552
|
+
try:
|
|
553
|
+
with open(real_path, 'r', encoding='utf-8') as f:
|
|
554
|
+
content = f.read()
|
|
555
|
+
|
|
556
|
+
# Get file extension for syntax highlighting hint
|
|
557
|
+
_, ext = os.path.splitext(real_path)
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
'success': True,
|
|
561
|
+
'file_path': file_path,
|
|
562
|
+
'content': content,
|
|
563
|
+
'file_size': file_size,
|
|
564
|
+
'extension': ext.lower(),
|
|
565
|
+
'encoding': 'utf-8'
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
except UnicodeDecodeError:
|
|
569
|
+
# Try reading as binary if UTF-8 fails
|
|
570
|
+
try:
|
|
571
|
+
with open(real_path, 'rb') as f:
|
|
572
|
+
binary_content = f.read()
|
|
573
|
+
|
|
574
|
+
# Check if it's a text file by looking for common text patterns
|
|
575
|
+
try:
|
|
576
|
+
text_content = binary_content.decode('latin-1')
|
|
577
|
+
if '\x00' in text_content:
|
|
578
|
+
# Binary file
|
|
579
|
+
return {
|
|
580
|
+
'success': False,
|
|
581
|
+
'error': 'File appears to be binary and cannot be displayed as text',
|
|
582
|
+
'file_path': file_path,
|
|
583
|
+
'file_size': file_size
|
|
584
|
+
}
|
|
585
|
+
else:
|
|
586
|
+
# Text file with different encoding
|
|
587
|
+
_, ext = os.path.splitext(real_path)
|
|
588
|
+
return {
|
|
589
|
+
'success': True,
|
|
590
|
+
'file_path': file_path,
|
|
591
|
+
'content': text_content,
|
|
592
|
+
'file_size': file_size,
|
|
593
|
+
'extension': ext.lower(),
|
|
594
|
+
'encoding': 'latin-1'
|
|
595
|
+
}
|
|
596
|
+
except Exception:
|
|
597
|
+
return {
|
|
598
|
+
'success': False,
|
|
599
|
+
'error': 'File encoding not supported',
|
|
600
|
+
'file_path': file_path
|
|
601
|
+
}
|
|
602
|
+
except Exception as read_error:
|
|
603
|
+
return {
|
|
604
|
+
'success': False,
|
|
605
|
+
'error': f'Failed to read file: {str(read_error)}',
|
|
606
|
+
'file_path': file_path
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
except Exception as e:
|
|
610
|
+
self.logger.error(f"Error in _read_file_safely: {e}")
|
|
611
|
+
return {
|
|
612
|
+
'success': False,
|
|
613
|
+
'error': str(e),
|
|
614
|
+
'file_path': file_path
|
|
615
|
+
}
|
|
616
|
+
|
|
406
617
|
async def _generate_git_diff(self, file_path: str, timestamp: Optional[str] = None, working_dir: str = None):
|
|
407
618
|
"""Generate git diff for a specific file operation.
|
|
408
619
|
|
|
@@ -437,8 +648,13 @@ class SocketIOServer:
|
|
|
437
648
|
working_dir = file_dir
|
|
438
649
|
self.logger.info(f"No git repo found, using file's directory: {working_dir}")
|
|
439
650
|
|
|
440
|
-
|
|
651
|
+
# Handle case where working_dir is None, empty string, or 'Unknown'
|
|
652
|
+
original_working_dir = working_dir
|
|
653
|
+
if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '':
|
|
441
654
|
working_dir = os.getcwd()
|
|
655
|
+
self.logger.info(f"[GIT-DIFF-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
|
|
656
|
+
else:
|
|
657
|
+
self.logger.info(f"[GIT-DIFF-DEBUG] Using provided working_dir: {working_dir}")
|
|
442
658
|
|
|
443
659
|
# For read-only git operations, we can work from any directory
|
|
444
660
|
# by passing the -C flag to git commands instead of changing directories
|
|
@@ -611,10 +827,34 @@ class SocketIOServer:
|
|
|
611
827
|
"timestamp": timestamp
|
|
612
828
|
}
|
|
613
829
|
|
|
614
|
-
# Check if file is
|
|
830
|
+
# Check if file is tracked by git
|
|
831
|
+
status_proc = await asyncio.create_subprocess_exec(
|
|
832
|
+
'git', '-C', working_dir, 'ls-files', '--', file_path,
|
|
833
|
+
stdout=asyncio.subprocess.PIPE,
|
|
834
|
+
stderr=asyncio.subprocess.PIPE
|
|
835
|
+
)
|
|
836
|
+
status_output, _ = await status_proc.communicate()
|
|
837
|
+
|
|
838
|
+
is_tracked = status_proc.returncode == 0 and status_output.decode().strip()
|
|
839
|
+
|
|
840
|
+
if not is_tracked:
|
|
841
|
+
# File is not tracked by git
|
|
842
|
+
return {
|
|
843
|
+
"success": False,
|
|
844
|
+
"error": "This file is not tracked by git",
|
|
845
|
+
"file_path": file_path,
|
|
846
|
+
"working_dir": working_dir,
|
|
847
|
+
"suggestions": [
|
|
848
|
+
"This file has not been added to git yet",
|
|
849
|
+
"Use 'git add' to track this file before viewing its diff",
|
|
850
|
+
"Git diff can only show changes for files that are tracked by git"
|
|
851
|
+
]
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
# File is tracked but has no changes to show
|
|
615
855
|
suggestions = [
|
|
616
|
-
"The file may not
|
|
617
|
-
"The file may
|
|
856
|
+
"The file may not have any committed changes yet",
|
|
857
|
+
"The file may have been added but not committed",
|
|
618
858
|
"The timestamp may be outside the git history range"
|
|
619
859
|
]
|
|
620
860
|
|
|
@@ -762,8 +1002,53 @@ class SocketIOServer:
|
|
|
762
1002
|
"""Get the current git branch for a directory"""
|
|
763
1003
|
import subprocess
|
|
764
1004
|
try:
|
|
765
|
-
|
|
1005
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] get_git_branch called with working_dir: {repr(working_dir)} (type: {type(working_dir)})")
|
|
1006
|
+
|
|
1007
|
+
# Handle case where working_dir is None, empty string, or common invalid states
|
|
1008
|
+
original_working_dir = working_dir
|
|
1009
|
+
invalid_states = [
|
|
1010
|
+
None, '', 'Unknown', 'Loading...', 'Loading', 'undefined', 'null',
|
|
1011
|
+
'Not Connected', 'Invalid Directory', 'No Directory'
|
|
1012
|
+
]
|
|
1013
|
+
|
|
1014
|
+
if working_dir in invalid_states or (isinstance(working_dir, str) and working_dir.strip() == ''):
|
|
766
1015
|
working_dir = os.getcwd()
|
|
1016
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
|
|
1017
|
+
else:
|
|
1018
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Using provided working_dir: {working_dir}")
|
|
1019
|
+
|
|
1020
|
+
# Additional validation for obviously invalid paths
|
|
1021
|
+
if isinstance(working_dir, str):
|
|
1022
|
+
working_dir = working_dir.strip()
|
|
1023
|
+
# Check for null bytes or other invalid characters
|
|
1024
|
+
if '\x00' in working_dir:
|
|
1025
|
+
self.logger.warning(f"[GIT-BRANCH-DEBUG] working_dir contains null bytes, using cwd instead")
|
|
1026
|
+
working_dir = os.getcwd()
|
|
1027
|
+
|
|
1028
|
+
# Validate that the directory exists and is a valid path
|
|
1029
|
+
if not os.path.exists(working_dir):
|
|
1030
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Directory does not exist: {working_dir} - responding gracefully")
|
|
1031
|
+
await self.sio.emit('git_branch_response', {
|
|
1032
|
+
'success': False,
|
|
1033
|
+
'error': f'Directory not found',
|
|
1034
|
+
'working_dir': working_dir,
|
|
1035
|
+
'original_working_dir': original_working_dir,
|
|
1036
|
+
'detail': f'Path does not exist: {working_dir}'
|
|
1037
|
+
}, room=sid)
|
|
1038
|
+
return
|
|
1039
|
+
|
|
1040
|
+
if not os.path.isdir(working_dir):
|
|
1041
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Path is not a directory: {working_dir} - responding gracefully")
|
|
1042
|
+
await self.sio.emit('git_branch_response', {
|
|
1043
|
+
'success': False,
|
|
1044
|
+
'error': f'Not a directory',
|
|
1045
|
+
'working_dir': working_dir,
|
|
1046
|
+
'original_working_dir': original_working_dir,
|
|
1047
|
+
'detail': f'Path is not a directory: {working_dir}'
|
|
1048
|
+
}, room=sid)
|
|
1049
|
+
return
|
|
1050
|
+
|
|
1051
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Running git command in directory: {working_dir}")
|
|
767
1052
|
|
|
768
1053
|
# Run git command to get current branch
|
|
769
1054
|
result = subprocess.run(
|
|
@@ -773,26 +1058,354 @@ class SocketIOServer:
|
|
|
773
1058
|
text=True
|
|
774
1059
|
)
|
|
775
1060
|
|
|
1061
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Git command result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
|
|
1062
|
+
|
|
776
1063
|
if result.returncode == 0:
|
|
777
1064
|
branch = result.stdout.strip()
|
|
1065
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Successfully got git branch: {branch}")
|
|
778
1066
|
await self.sio.emit('git_branch_response', {
|
|
779
1067
|
'success': True,
|
|
780
1068
|
'branch': branch,
|
|
781
|
-
'working_dir': working_dir
|
|
1069
|
+
'working_dir': working_dir,
|
|
1070
|
+
'original_working_dir': original_working_dir
|
|
782
1071
|
}, room=sid)
|
|
783
1072
|
else:
|
|
1073
|
+
self.logger.warning(f"[GIT-BRANCH-DEBUG] Git command failed: {result.stderr}")
|
|
784
1074
|
await self.sio.emit('git_branch_response', {
|
|
785
1075
|
'success': False,
|
|
786
1076
|
'error': 'Not a git repository',
|
|
787
|
-
'working_dir': working_dir
|
|
1077
|
+
'working_dir': working_dir,
|
|
1078
|
+
'original_working_dir': original_working_dir,
|
|
1079
|
+
'git_error': result.stderr
|
|
788
1080
|
}, room=sid)
|
|
789
1081
|
|
|
790
1082
|
except Exception as e:
|
|
791
|
-
self.logger.error(f"
|
|
1083
|
+
self.logger.error(f"[GIT-BRANCH-DEBUG] Exception in get_git_branch: {e}")
|
|
1084
|
+
import traceback
|
|
1085
|
+
self.logger.error(f"[GIT-BRANCH-DEBUG] Stack trace: {traceback.format_exc()}")
|
|
792
1086
|
await self.sio.emit('git_branch_response', {
|
|
793
1087
|
'success': False,
|
|
794
1088
|
'error': str(e),
|
|
795
|
-
'working_dir': working_dir
|
|
1089
|
+
'working_dir': working_dir,
|
|
1090
|
+
'original_working_dir': original_working_dir
|
|
1091
|
+
}, room=sid)
|
|
1092
|
+
|
|
1093
|
+
@self.sio.event
|
|
1094
|
+
async def check_file_tracked(sid, data):
|
|
1095
|
+
"""Check if a file is tracked by git"""
|
|
1096
|
+
import subprocess
|
|
1097
|
+
try:
|
|
1098
|
+
file_path = data.get('file_path')
|
|
1099
|
+
working_dir = data.get('working_dir', os.getcwd())
|
|
1100
|
+
|
|
1101
|
+
if not file_path:
|
|
1102
|
+
await self.sio.emit('file_tracked_response', {
|
|
1103
|
+
'success': False,
|
|
1104
|
+
'error': 'file_path is required',
|
|
1105
|
+
'file_path': file_path
|
|
1106
|
+
}, room=sid)
|
|
1107
|
+
return
|
|
1108
|
+
|
|
1109
|
+
# Use git ls-files to check if file is tracked
|
|
1110
|
+
result = subprocess.run(
|
|
1111
|
+
["git", "-C", working_dir, "ls-files", "--", file_path],
|
|
1112
|
+
capture_output=True,
|
|
1113
|
+
text=True
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
is_tracked = result.returncode == 0 and result.stdout.strip()
|
|
1117
|
+
|
|
1118
|
+
await self.sio.emit('file_tracked_response', {
|
|
1119
|
+
'success': True,
|
|
1120
|
+
'file_path': file_path,
|
|
1121
|
+
'working_dir': working_dir,
|
|
1122
|
+
'is_tracked': bool(is_tracked)
|
|
1123
|
+
}, room=sid)
|
|
1124
|
+
|
|
1125
|
+
except Exception as e:
|
|
1126
|
+
self.logger.error(f"Error checking file tracked status: {e}")
|
|
1127
|
+
await self.sio.emit('file_tracked_response', {
|
|
1128
|
+
'success': False,
|
|
1129
|
+
'error': str(e),
|
|
1130
|
+
'file_path': data.get('file_path', 'unknown')
|
|
1131
|
+
}, room=sid)
|
|
1132
|
+
|
|
1133
|
+
@self.sio.event
|
|
1134
|
+
async def read_file(sid, data):
|
|
1135
|
+
"""Read file contents safely"""
|
|
1136
|
+
try:
|
|
1137
|
+
file_path = data.get('file_path')
|
|
1138
|
+
working_dir = data.get('working_dir', os.getcwd())
|
|
1139
|
+
max_size = data.get('max_size', 1024 * 1024) # 1MB default limit
|
|
1140
|
+
|
|
1141
|
+
if not file_path:
|
|
1142
|
+
await self.sio.emit('file_content_response', {
|
|
1143
|
+
'success': False,
|
|
1144
|
+
'error': 'file_path is required',
|
|
1145
|
+
'file_path': file_path
|
|
1146
|
+
}, room=sid)
|
|
1147
|
+
return
|
|
1148
|
+
|
|
1149
|
+
# Use the shared file reading logic
|
|
1150
|
+
result = await self._read_file_safely(file_path, working_dir, max_size)
|
|
1151
|
+
|
|
1152
|
+
# Send the result back to the client
|
|
1153
|
+
await self.sio.emit('file_content_response', result, room=sid)
|
|
1154
|
+
|
|
1155
|
+
except Exception as e:
|
|
1156
|
+
self.logger.error(f"Error reading file: {e}")
|
|
1157
|
+
await self.sio.emit('file_content_response', {
|
|
1158
|
+
'success': False,
|
|
1159
|
+
'error': str(e),
|
|
1160
|
+
'file_path': data.get('file_path', 'unknown')
|
|
1161
|
+
}, room=sid)
|
|
1162
|
+
|
|
1163
|
+
@self.sio.event
|
|
1164
|
+
async def check_git_status(sid, data):
|
|
1165
|
+
"""Check git status for a file to determine if git diff icons should be shown"""
|
|
1166
|
+
import subprocess
|
|
1167
|
+
try:
|
|
1168
|
+
file_path = data.get('file_path')
|
|
1169
|
+
working_dir = data.get('working_dir', os.getcwd())
|
|
1170
|
+
|
|
1171
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] check_git_status called with file_path: {repr(file_path)}, working_dir: {repr(working_dir)}")
|
|
1172
|
+
|
|
1173
|
+
if not file_path:
|
|
1174
|
+
await self.sio.emit('git_status_response', {
|
|
1175
|
+
'success': False,
|
|
1176
|
+
'error': 'file_path is required',
|
|
1177
|
+
'file_path': file_path
|
|
1178
|
+
}, room=sid)
|
|
1179
|
+
return
|
|
1180
|
+
|
|
1181
|
+
# Validate and sanitize working_dir
|
|
1182
|
+
original_working_dir = working_dir
|
|
1183
|
+
if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '' or working_dir == '.':
|
|
1184
|
+
working_dir = os.getcwd()
|
|
1185
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
|
|
1186
|
+
else:
|
|
1187
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] Using provided working_dir: {working_dir}")
|
|
1188
|
+
|
|
1189
|
+
# Check if the working directory exists and is a directory
|
|
1190
|
+
if not os.path.exists(working_dir):
|
|
1191
|
+
self.logger.warning(f"[GIT-STATUS-DEBUG] Directory does not exist: {working_dir}")
|
|
1192
|
+
await self.sio.emit('git_status_response', {
|
|
1193
|
+
'success': False,
|
|
1194
|
+
'error': f'Directory does not exist: {working_dir}',
|
|
1195
|
+
'file_path': file_path,
|
|
1196
|
+
'working_dir': working_dir,
|
|
1197
|
+
'original_working_dir': original_working_dir
|
|
1198
|
+
}, room=sid)
|
|
1199
|
+
return
|
|
1200
|
+
|
|
1201
|
+
if not os.path.isdir(working_dir):
|
|
1202
|
+
self.logger.warning(f"[GIT-STATUS-DEBUG] Path is not a directory: {working_dir}")
|
|
1203
|
+
await self.sio.emit('git_status_response', {
|
|
1204
|
+
'success': False,
|
|
1205
|
+
'error': f'Path is not a directory: {working_dir}',
|
|
1206
|
+
'file_path': file_path,
|
|
1207
|
+
'working_dir': working_dir,
|
|
1208
|
+
'original_working_dir': original_working_dir
|
|
1209
|
+
}, room=sid)
|
|
1210
|
+
return
|
|
1211
|
+
|
|
1212
|
+
# Check if this is a git repository
|
|
1213
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] Checking if {working_dir} is a git repository")
|
|
1214
|
+
git_check = subprocess.run(
|
|
1215
|
+
["git", "-C", working_dir, "rev-parse", "--git-dir"],
|
|
1216
|
+
capture_output=True,
|
|
1217
|
+
text=True
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
if git_check.returncode != 0:
|
|
1221
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] Not a git repository: {working_dir}")
|
|
1222
|
+
await self.sio.emit('git_status_response', {
|
|
1223
|
+
'success': False,
|
|
1224
|
+
'error': 'Not a git repository',
|
|
1225
|
+
'file_path': file_path,
|
|
1226
|
+
'working_dir': working_dir,
|
|
1227
|
+
'original_working_dir': original_working_dir
|
|
1228
|
+
}, room=sid)
|
|
1229
|
+
return
|
|
1230
|
+
|
|
1231
|
+
# Determine if the file path should be made relative to git root
|
|
1232
|
+
file_path_for_git = file_path
|
|
1233
|
+
if os.path.isabs(file_path):
|
|
1234
|
+
# Get git root to make path relative if needed
|
|
1235
|
+
git_root_result = subprocess.run(
|
|
1236
|
+
["git", "-C", working_dir, "rev-parse", "--show-toplevel"],
|
|
1237
|
+
capture_output=True,
|
|
1238
|
+
text=True
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
if git_root_result.returncode == 0:
|
|
1242
|
+
git_root = git_root_result.stdout.strip()
|
|
1243
|
+
try:
|
|
1244
|
+
file_path_for_git = os.path.relpath(file_path, git_root)
|
|
1245
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] Made file path relative to git root: {file_path_for_git}")
|
|
1246
|
+
except ValueError:
|
|
1247
|
+
# File is not under git root - keep original path
|
|
1248
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] File not under git root, keeping original path: {file_path}")
|
|
1249
|
+
pass
|
|
1250
|
+
|
|
1251
|
+
# Check if the file exists
|
|
1252
|
+
full_path = file_path if os.path.isabs(file_path) else os.path.join(working_dir, file_path)
|
|
1253
|
+
if not os.path.exists(full_path):
|
|
1254
|
+
self.logger.warning(f"[GIT-STATUS-DEBUG] File does not exist: {full_path}")
|
|
1255
|
+
await self.sio.emit('git_status_response', {
|
|
1256
|
+
'success': False,
|
|
1257
|
+
'error': f'File does not exist: {file_path}',
|
|
1258
|
+
'file_path': file_path,
|
|
1259
|
+
'working_dir': working_dir,
|
|
1260
|
+
'original_working_dir': original_working_dir
|
|
1261
|
+
}, room=sid)
|
|
1262
|
+
return
|
|
1263
|
+
|
|
1264
|
+
# Check git status for the file - this succeeds if git knows about the file
|
|
1265
|
+
# (either tracked, modified, staged, etc.)
|
|
1266
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] Checking git status for file: {file_path_for_git}")
|
|
1267
|
+
git_status_result = subprocess.run(
|
|
1268
|
+
["git", "-C", working_dir, "status", "--porcelain", file_path_for_git],
|
|
1269
|
+
capture_output=True,
|
|
1270
|
+
text=True
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] Git status result: returncode={git_status_result.returncode}, stdout={repr(git_status_result.stdout)}, stderr={repr(git_status_result.stderr)}")
|
|
1274
|
+
|
|
1275
|
+
# Also check if file is tracked by git (alternative approach)
|
|
1276
|
+
ls_files_result = subprocess.run(
|
|
1277
|
+
["git", "-C", working_dir, "ls-files", file_path_for_git],
|
|
1278
|
+
capture_output=True,
|
|
1279
|
+
text=True
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
is_tracked = ls_files_result.returncode == 0 and ls_files_result.stdout.strip()
|
|
1283
|
+
has_status = git_status_result.returncode == 0
|
|
1284
|
+
|
|
1285
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] File tracking status: is_tracked={is_tracked}, has_status={has_status}")
|
|
1286
|
+
|
|
1287
|
+
# Success if git knows about the file (either tracked or has status changes)
|
|
1288
|
+
if is_tracked or has_status:
|
|
1289
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] Git status check successful for {file_path}")
|
|
1290
|
+
await self.sio.emit('git_status_response', {
|
|
1291
|
+
'success': True,
|
|
1292
|
+
'file_path': file_path,
|
|
1293
|
+
'working_dir': working_dir,
|
|
1294
|
+
'original_working_dir': original_working_dir,
|
|
1295
|
+
'is_tracked': is_tracked,
|
|
1296
|
+
'has_changes': bool(git_status_result.stdout.strip()) if has_status else False
|
|
1297
|
+
}, room=sid)
|
|
1298
|
+
else:
|
|
1299
|
+
self.logger.info(f"[GIT-STATUS-DEBUG] File {file_path} is not tracked by git")
|
|
1300
|
+
await self.sio.emit('git_status_response', {
|
|
1301
|
+
'success': False,
|
|
1302
|
+
'error': 'File is not tracked by git',
|
|
1303
|
+
'file_path': file_path,
|
|
1304
|
+
'working_dir': working_dir,
|
|
1305
|
+
'original_working_dir': original_working_dir,
|
|
1306
|
+
'is_tracked': False
|
|
1307
|
+
}, room=sid)
|
|
1308
|
+
|
|
1309
|
+
except Exception as e:
|
|
1310
|
+
self.logger.error(f"[GIT-STATUS-DEBUG] Exception in check_git_status: {e}")
|
|
1311
|
+
import traceback
|
|
1312
|
+
self.logger.error(f"[GIT-STATUS-DEBUG] Stack trace: {traceback.format_exc()}")
|
|
1313
|
+
await self.sio.emit('git_status_response', {
|
|
1314
|
+
'success': False,
|
|
1315
|
+
'error': str(e),
|
|
1316
|
+
'file_path': data.get('file_path', 'unknown'),
|
|
1317
|
+
'working_dir': data.get('working_dir', 'unknown')
|
|
1318
|
+
}, room=sid)
|
|
1319
|
+
|
|
1320
|
+
@self.sio.event
|
|
1321
|
+
async def git_add_file(sid, data):
|
|
1322
|
+
"""Add file to git tracking"""
|
|
1323
|
+
import subprocess
|
|
1324
|
+
try:
|
|
1325
|
+
file_path = data.get('file_path')
|
|
1326
|
+
working_dir = data.get('working_dir', os.getcwd())
|
|
1327
|
+
|
|
1328
|
+
self.logger.info(f"[GIT-ADD-DEBUG] git_add_file called with file_path: {repr(file_path)}, working_dir: {repr(working_dir)} (type: {type(working_dir)})")
|
|
1329
|
+
|
|
1330
|
+
if not file_path:
|
|
1331
|
+
await self.sio.emit('git_add_response', {
|
|
1332
|
+
'success': False,
|
|
1333
|
+
'error': 'file_path is required',
|
|
1334
|
+
'file_path': file_path
|
|
1335
|
+
}, room=sid)
|
|
1336
|
+
return
|
|
1337
|
+
|
|
1338
|
+
# Validate and sanitize working_dir
|
|
1339
|
+
original_working_dir = working_dir
|
|
1340
|
+
if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '' or working_dir == '.':
|
|
1341
|
+
working_dir = os.getcwd()
|
|
1342
|
+
self.logger.info(f"[GIT-ADD-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
|
|
1343
|
+
else:
|
|
1344
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Using provided working_dir: {working_dir}")
|
|
1345
|
+
|
|
1346
|
+
# Validate that the directory exists and is a valid path
|
|
1347
|
+
if not os.path.exists(working_dir):
|
|
1348
|
+
self.logger.warning(f"[GIT-ADD-DEBUG] Directory does not exist: {working_dir}")
|
|
1349
|
+
await self.sio.emit('git_add_response', {
|
|
1350
|
+
'success': False,
|
|
1351
|
+
'error': f'Directory does not exist: {working_dir}',
|
|
1352
|
+
'file_path': file_path,
|
|
1353
|
+
'working_dir': working_dir,
|
|
1354
|
+
'original_working_dir': original_working_dir
|
|
1355
|
+
}, room=sid)
|
|
1356
|
+
return
|
|
1357
|
+
|
|
1358
|
+
if not os.path.isdir(working_dir):
|
|
1359
|
+
self.logger.warning(f"[GIT-ADD-DEBUG] Path is not a directory: {working_dir}")
|
|
1360
|
+
await self.sio.emit('git_add_response', {
|
|
1361
|
+
'success': False,
|
|
1362
|
+
'error': f'Path is not a directory: {working_dir}',
|
|
1363
|
+
'file_path': file_path,
|
|
1364
|
+
'working_dir': working_dir,
|
|
1365
|
+
'original_working_dir': original_working_dir
|
|
1366
|
+
}, room=sid)
|
|
1367
|
+
return
|
|
1368
|
+
|
|
1369
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Running git add command in directory: {working_dir}")
|
|
1370
|
+
|
|
1371
|
+
# Use git add to track the file
|
|
1372
|
+
result = subprocess.run(
|
|
1373
|
+
["git", "-C", working_dir, "add", file_path],
|
|
1374
|
+
capture_output=True,
|
|
1375
|
+
text=True
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Git add result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
|
|
1379
|
+
|
|
1380
|
+
if result.returncode == 0:
|
|
1381
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Successfully added {file_path} to git in {working_dir}")
|
|
1382
|
+
await self.sio.emit('git_add_response', {
|
|
1383
|
+
'success': True,
|
|
1384
|
+
'file_path': file_path,
|
|
1385
|
+
'working_dir': working_dir,
|
|
1386
|
+
'original_working_dir': original_working_dir,
|
|
1387
|
+
'message': 'File successfully added to git tracking'
|
|
1388
|
+
}, room=sid)
|
|
1389
|
+
else:
|
|
1390
|
+
error_message = result.stderr.strip() or 'Unknown git error'
|
|
1391
|
+
self.logger.warning(f"[GIT-ADD-DEBUG] Git add failed: {error_message}")
|
|
1392
|
+
await self.sio.emit('git_add_response', {
|
|
1393
|
+
'success': False,
|
|
1394
|
+
'error': f'Git add failed: {error_message}',
|
|
1395
|
+
'file_path': file_path,
|
|
1396
|
+
'working_dir': working_dir,
|
|
1397
|
+
'original_working_dir': original_working_dir
|
|
1398
|
+
}, room=sid)
|
|
1399
|
+
|
|
1400
|
+
except Exception as e:
|
|
1401
|
+
self.logger.error(f"[GIT-ADD-DEBUG] Exception in git_add_file: {e}")
|
|
1402
|
+
import traceback
|
|
1403
|
+
self.logger.error(f"[GIT-ADD-DEBUG] Stack trace: {traceback.format_exc()}")
|
|
1404
|
+
await self.sio.emit('git_add_response', {
|
|
1405
|
+
'success': False,
|
|
1406
|
+
'error': str(e),
|
|
1407
|
+
'file_path': data.get('file_path', 'unknown'),
|
|
1408
|
+
'working_dir': data.get('working_dir', 'unknown')
|
|
796
1409
|
}, room=sid)
|
|
797
1410
|
|
|
798
1411
|
async def _send_current_status(self, sid: str):
|
|
@@ -901,18 +1514,22 @@ class SocketIOServer:
|
|
|
901
1514
|
|
|
902
1515
|
# Broadcast to clients with timeout and error handling
|
|
903
1516
|
try:
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1517
|
+
# Check if the event loop is still running and not closed
|
|
1518
|
+
if self.loop and not self.loop.is_closed() and self.loop.is_running():
|
|
1519
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
1520
|
+
self.sio.emit('claude_event', event),
|
|
1521
|
+
self.loop
|
|
1522
|
+
)
|
|
1523
|
+
# Wait for completion with timeout to detect issues
|
|
1524
|
+
try:
|
|
1525
|
+
future.result(timeout=2.0) # 2 second timeout
|
|
1526
|
+
self.logger.debug(f"📨 Successfully broadcasted {event_type} to {len(self.clients)} clients")
|
|
1527
|
+
except asyncio.TimeoutError:
|
|
1528
|
+
self.logger.warning(f"⏰ Broadcast timeout for event {event_type} - continuing anyway")
|
|
1529
|
+
except Exception as emit_error:
|
|
1530
|
+
self.logger.error(f"❌ Broadcast emit error for {event_type}: {emit_error}")
|
|
1531
|
+
else:
|
|
1532
|
+
self.logger.warning(f"⚠️ Event loop not available for broadcast of {event_type} - event loop closed or not running")
|
|
916
1533
|
except Exception as e:
|
|
917
1534
|
self.logger.error(f"❌ Failed to submit broadcast to event loop: {e}")
|
|
918
1535
|
import traceback
|