claude-mpm 3.3.0__py3-none-any.whl → 3.4.0__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/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/pm.json +1 -1
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/test_integration.json +112 -0
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/cli/commands/memory.py +749 -26
- claude_mpm/cli/commands/run.py +115 -14
- claude_mpm/cli/parser.py +89 -1
- claude_mpm/constants.py +6 -0
- claude_mpm/core/claude_runner.py +74 -11
- claude_mpm/core/config.py +1 -1
- claude_mpm/core/session_manager.py +46 -0
- claude_mpm/core/simple_runner.py +74 -11
- claude_mpm/hooks/builtin/mpm_command_hook.py +5 -5
- claude_mpm/hooks/claude_hooks/hook_handler.py +213 -30
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -2
- claude_mpm/hooks/memory_integration_hook.py +51 -5
- claude_mpm/services/__init__.py +23 -5
- claude_mpm/services/agent_memory_manager.py +800 -71
- claude_mpm/services/memory_builder.py +823 -0
- claude_mpm/services/memory_optimizer.py +619 -0
- claude_mpm/services/memory_router.py +445 -0
- claude_mpm/services/project_analyzer.py +771 -0
- claude_mpm/services/socketio_server.py +649 -45
- claude_mpm/services/version_control/git_operations.py +26 -0
- claude_mpm-3.4.0.dist-info/METADATA +183 -0
- {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/RECORD +36 -52
- claude_mpm/agents/agent-template.yaml +0 -83
- claude_mpm/agents/templates/test-integration-agent.md +0 -34
- 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/core/websocket_handler.py +0 -233
- 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/services/websocket_server.py +0 -376
- claude_mpm-3.3.0.dist-info/METADATA +0 -432
- {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.3.0.dist-info → claude_mpm-3.4.0.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."""
|
|
@@ -254,10 +270,24 @@ class SocketIOServer:
|
|
|
254
270
|
self.app = web.Application()
|
|
255
271
|
self.sio.attach(self.app)
|
|
256
272
|
|
|
273
|
+
# Add CORS middleware
|
|
274
|
+
import aiohttp_cors
|
|
275
|
+
cors = aiohttp_cors.setup(self.app, defaults={
|
|
276
|
+
"*": aiohttp_cors.ResourceOptions(
|
|
277
|
+
allow_credentials=True,
|
|
278
|
+
expose_headers="*",
|
|
279
|
+
allow_headers="*",
|
|
280
|
+
allow_methods="*"
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
|
|
257
284
|
# Add HTTP routes
|
|
258
285
|
self.app.router.add_get('/health', self._handle_health)
|
|
259
286
|
self.app.router.add_get('/status', self._handle_health)
|
|
260
287
|
self.app.router.add_get('/api/git-diff', self._handle_git_diff)
|
|
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)
|
|
261
291
|
|
|
262
292
|
# Add dashboard routes
|
|
263
293
|
self.app.router.add_get('/', self._handle_dashboard)
|
|
@@ -310,6 +340,10 @@ class SocketIOServer:
|
|
|
310
340
|
"port": self.port,
|
|
311
341
|
"host": self.host,
|
|
312
342
|
"clients_connected": len(self.clients)
|
|
343
|
+
}, headers={
|
|
344
|
+
'Access-Control-Allow-Origin': '*',
|
|
345
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
346
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
313
347
|
})
|
|
314
348
|
|
|
315
349
|
async def _handle_dashboard(self, request):
|
|
@@ -321,6 +355,18 @@ class SocketIOServer:
|
|
|
321
355
|
return web.FileResponse(str(dashboard_path))
|
|
322
356
|
else:
|
|
323
357
|
return web.Response(text=f"Dashboard not found at: {dashboard_path}", status=404)
|
|
358
|
+
|
|
359
|
+
async def _handle_cors_preflight(self, request):
|
|
360
|
+
"""Handle CORS preflight requests."""
|
|
361
|
+
return web.Response(
|
|
362
|
+
status=200,
|
|
363
|
+
headers={
|
|
364
|
+
'Access-Control-Allow-Origin': '*',
|
|
365
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
366
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization',
|
|
367
|
+
'Access-Control-Max-Age': '86400'
|
|
368
|
+
}
|
|
369
|
+
)
|
|
324
370
|
|
|
325
371
|
async def _handle_git_diff(self, request):
|
|
326
372
|
"""Handle git diff requests for file operations.
|
|
@@ -336,23 +382,237 @@ class SocketIOServer:
|
|
|
336
382
|
timestamp = request.query.get('timestamp')
|
|
337
383
|
working_dir = request.query.get('working_dir', os.getcwd())
|
|
338
384
|
|
|
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}")
|
|
387
|
+
|
|
339
388
|
if not file_path:
|
|
389
|
+
self.logger.warning("Git diff request missing file parameter")
|
|
340
390
|
return web.json_response({
|
|
391
|
+
"success": False,
|
|
341
392
|
"error": "Missing required parameter: file"
|
|
342
|
-
}, status=400
|
|
393
|
+
}, status=400, headers={
|
|
394
|
+
'Access-Control-Allow-Origin': '*',
|
|
395
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
396
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
397
|
+
})
|
|
343
398
|
|
|
344
399
|
self.logger.debug(f"Git diff requested for file: {file_path}, timestamp: {timestamp}")
|
|
345
400
|
|
|
346
401
|
# Generate git diff using the _generate_git_diff helper
|
|
347
402
|
diff_result = await self._generate_git_diff(file_path, timestamp, working_dir)
|
|
348
403
|
|
|
349
|
-
|
|
404
|
+
self.logger.info(f"Git diff result: success={diff_result.get('success', False)}, method={diff_result.get('method', 'unknown')}")
|
|
405
|
+
|
|
406
|
+
return web.json_response(diff_result, headers={
|
|
407
|
+
'Access-Control-Allow-Origin': '*',
|
|
408
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
409
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
410
|
+
})
|
|
350
411
|
|
|
351
412
|
except Exception as e:
|
|
352
413
|
self.logger.error(f"Error generating git diff: {e}")
|
|
414
|
+
import traceback
|
|
415
|
+
self.logger.error(f"Git diff error traceback: {traceback.format_exc()}")
|
|
353
416
|
return web.json_response({
|
|
417
|
+
"success": False,
|
|
354
418
|
"error": f"Failed to generate git diff: {str(e)}"
|
|
355
|
-
}, status=500
|
|
419
|
+
}, status=500, headers={
|
|
420
|
+
'Access-Control-Allow-Origin': '*',
|
|
421
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
422
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept'
|
|
423
|
+
})
|
|
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
|
+
}
|
|
356
616
|
|
|
357
617
|
async def _generate_git_diff(self, file_path: str, timestamp: Optional[str] = None, working_dir: str = None):
|
|
358
618
|
"""Generate git diff for a specific file operation.
|
|
@@ -370,17 +630,36 @@ class SocketIOServer:
|
|
|
370
630
|
dict: Contains diff content, metadata, and status information
|
|
371
631
|
"""
|
|
372
632
|
try:
|
|
633
|
+
# If file_path is absolute, determine its git repository
|
|
634
|
+
if os.path.isabs(file_path):
|
|
635
|
+
# Find the directory containing the file
|
|
636
|
+
file_dir = os.path.dirname(file_path)
|
|
637
|
+
if os.path.exists(file_dir):
|
|
638
|
+
# Try to find the git root from the file's directory
|
|
639
|
+
current_dir = file_dir
|
|
640
|
+
while current_dir != "/" and current_dir:
|
|
641
|
+
if os.path.exists(os.path.join(current_dir, ".git")):
|
|
642
|
+
working_dir = current_dir
|
|
643
|
+
self.logger.info(f"Found git repository at: {working_dir}")
|
|
644
|
+
break
|
|
645
|
+
current_dir = os.path.dirname(current_dir)
|
|
646
|
+
else:
|
|
647
|
+
# If no git repo found, use the file's directory
|
|
648
|
+
working_dir = file_dir
|
|
649
|
+
self.logger.info(f"No git repo found, using file's directory: {working_dir}")
|
|
650
|
+
|
|
373
651
|
if working_dir is None:
|
|
374
652
|
working_dir = os.getcwd()
|
|
375
653
|
|
|
376
|
-
#
|
|
654
|
+
# For read-only git operations, we can work from any directory
|
|
655
|
+
# by passing the -C flag to git commands instead of changing directories
|
|
377
656
|
original_cwd = os.getcwd()
|
|
378
657
|
try:
|
|
379
|
-
|
|
658
|
+
# We'll use git -C <working_dir> for all commands instead of chdir
|
|
380
659
|
|
|
381
660
|
# Check if this is a git repository
|
|
382
661
|
git_check = await asyncio.create_subprocess_exec(
|
|
383
|
-
'git', 'rev-parse', '--git-dir',
|
|
662
|
+
'git', '-C', working_dir, 'rev-parse', '--git-dir',
|
|
384
663
|
stdout=asyncio.subprocess.PIPE,
|
|
385
664
|
stderr=asyncio.subprocess.PIPE
|
|
386
665
|
)
|
|
@@ -388,6 +667,7 @@ class SocketIOServer:
|
|
|
388
667
|
|
|
389
668
|
if git_check.returncode != 0:
|
|
390
669
|
return {
|
|
670
|
+
"success": False,
|
|
391
671
|
"error": "Not a git repository",
|
|
392
672
|
"file_path": file_path,
|
|
393
673
|
"working_dir": working_dir
|
|
@@ -395,14 +675,14 @@ class SocketIOServer:
|
|
|
395
675
|
|
|
396
676
|
# Get the absolute path of the file relative to git root
|
|
397
677
|
git_root_proc = await asyncio.create_subprocess_exec(
|
|
398
|
-
'git', 'rev-parse', '--show-toplevel',
|
|
678
|
+
'git', '-C', working_dir, 'rev-parse', '--show-toplevel',
|
|
399
679
|
stdout=asyncio.subprocess.PIPE,
|
|
400
680
|
stderr=asyncio.subprocess.PIPE
|
|
401
681
|
)
|
|
402
682
|
git_root_output, _ = await git_root_proc.communicate()
|
|
403
683
|
|
|
404
684
|
if git_root_proc.returncode != 0:
|
|
405
|
-
return {"error": "Failed to determine git root directory"}
|
|
685
|
+
return {"success": False, "error": "Failed to determine git root directory"}
|
|
406
686
|
|
|
407
687
|
git_root = git_root_output.decode().strip()
|
|
408
688
|
|
|
@@ -424,7 +704,7 @@ class SocketIOServer:
|
|
|
424
704
|
|
|
425
705
|
# Find commits that modified this file around the timestamp
|
|
426
706
|
log_proc = await asyncio.create_subprocess_exec(
|
|
427
|
-
'git', 'log', '--oneline', '--since', git_since,
|
|
707
|
+
'git', '-C', working_dir, 'log', '--oneline', '--since', git_since,
|
|
428
708
|
'--until', f'{git_since} +1 hour', '--', file_path,
|
|
429
709
|
stdout=asyncio.subprocess.PIPE,
|
|
430
710
|
stderr=asyncio.subprocess.PIPE
|
|
@@ -439,7 +719,7 @@ class SocketIOServer:
|
|
|
439
719
|
|
|
440
720
|
# Get the diff for this specific commit
|
|
441
721
|
diff_proc = await asyncio.create_subprocess_exec(
|
|
442
|
-
'git', 'show', '--format=fuller', commit_hash, '--', file_path,
|
|
722
|
+
'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
|
|
443
723
|
stdout=asyncio.subprocess.PIPE,
|
|
444
724
|
stderr=asyncio.subprocess.PIPE
|
|
445
725
|
)
|
|
@@ -459,7 +739,7 @@ class SocketIOServer:
|
|
|
459
739
|
|
|
460
740
|
# Fallback: Get the most recent change to the file
|
|
461
741
|
log_proc = await asyncio.create_subprocess_exec(
|
|
462
|
-
'git', 'log', '-1', '--oneline', '--', file_path,
|
|
742
|
+
'git', '-C', working_dir, 'log', '-1', '--oneline', '--', file_path,
|
|
463
743
|
stdout=asyncio.subprocess.PIPE,
|
|
464
744
|
stderr=asyncio.subprocess.PIPE
|
|
465
745
|
)
|
|
@@ -470,7 +750,7 @@ class SocketIOServer:
|
|
|
470
750
|
|
|
471
751
|
# Get the diff for the most recent commit
|
|
472
752
|
diff_proc = await asyncio.create_subprocess_exec(
|
|
473
|
-
'git', 'show', '--format=fuller', commit_hash, '--', file_path,
|
|
753
|
+
'git', '-C', working_dir, 'show', '--format=fuller', commit_hash, '--', file_path,
|
|
474
754
|
stdout=asyncio.subprocess.PIPE,
|
|
475
755
|
stderr=asyncio.subprocess.PIPE
|
|
476
756
|
)
|
|
@@ -486,9 +766,45 @@ class SocketIOServer:
|
|
|
486
766
|
"timestamp": timestamp
|
|
487
767
|
}
|
|
488
768
|
|
|
489
|
-
#
|
|
769
|
+
# Try to show unstaged changes first
|
|
490
770
|
diff_proc = await asyncio.create_subprocess_exec(
|
|
491
|
-
'git', '
|
|
771
|
+
'git', '-C', working_dir, 'diff', '--', file_path,
|
|
772
|
+
stdout=asyncio.subprocess.PIPE,
|
|
773
|
+
stderr=asyncio.subprocess.PIPE
|
|
774
|
+
)
|
|
775
|
+
diff_output, _ = await diff_proc.communicate()
|
|
776
|
+
|
|
777
|
+
if diff_proc.returncode == 0 and diff_output.decode().strip():
|
|
778
|
+
return {
|
|
779
|
+
"success": True,
|
|
780
|
+
"diff": diff_output.decode(),
|
|
781
|
+
"commit_hash": "unstaged_changes",
|
|
782
|
+
"file_path": file_path,
|
|
783
|
+
"method": "unstaged_changes",
|
|
784
|
+
"timestamp": timestamp
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
# Then try staged changes
|
|
788
|
+
diff_proc = await asyncio.create_subprocess_exec(
|
|
789
|
+
'git', '-C', working_dir, 'diff', '--cached', '--', file_path,
|
|
790
|
+
stdout=asyncio.subprocess.PIPE,
|
|
791
|
+
stderr=asyncio.subprocess.PIPE
|
|
792
|
+
)
|
|
793
|
+
diff_output, _ = await diff_proc.communicate()
|
|
794
|
+
|
|
795
|
+
if diff_proc.returncode == 0 and diff_output.decode().strip():
|
|
796
|
+
return {
|
|
797
|
+
"success": True,
|
|
798
|
+
"diff": diff_output.decode(),
|
|
799
|
+
"commit_hash": "staged_changes",
|
|
800
|
+
"file_path": file_path,
|
|
801
|
+
"method": "staged_changes",
|
|
802
|
+
"timestamp": timestamp
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
# Final fallback: Show changes against HEAD
|
|
806
|
+
diff_proc = await asyncio.create_subprocess_exec(
|
|
807
|
+
'git', '-C', working_dir, 'diff', 'HEAD', '--', file_path,
|
|
492
808
|
stdout=asyncio.subprocess.PIPE,
|
|
493
809
|
stderr=asyncio.subprocess.PIPE
|
|
494
810
|
)
|
|
@@ -506,14 +822,58 @@ class SocketIOServer:
|
|
|
506
822
|
"timestamp": timestamp
|
|
507
823
|
}
|
|
508
824
|
|
|
825
|
+
# Check if file is tracked by git
|
|
826
|
+
status_proc = await asyncio.create_subprocess_exec(
|
|
827
|
+
'git', '-C', working_dir, 'ls-files', '--', file_path,
|
|
828
|
+
stdout=asyncio.subprocess.PIPE,
|
|
829
|
+
stderr=asyncio.subprocess.PIPE
|
|
830
|
+
)
|
|
831
|
+
status_output, _ = await status_proc.communicate()
|
|
832
|
+
|
|
833
|
+
is_tracked = status_proc.returncode == 0 and status_output.decode().strip()
|
|
834
|
+
|
|
835
|
+
if not is_tracked:
|
|
836
|
+
# File is not tracked by git
|
|
837
|
+
return {
|
|
838
|
+
"success": False,
|
|
839
|
+
"error": "This file is not tracked by git",
|
|
840
|
+
"file_path": file_path,
|
|
841
|
+
"working_dir": working_dir,
|
|
842
|
+
"suggestions": [
|
|
843
|
+
"This file has not been added to git yet",
|
|
844
|
+
"Use 'git add' to track this file before viewing its diff",
|
|
845
|
+
"Git diff can only show changes for files that are tracked by git"
|
|
846
|
+
]
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
# File is tracked but has no changes to show
|
|
850
|
+
suggestions = [
|
|
851
|
+
"The file may not have any committed changes yet",
|
|
852
|
+
"The file may have been added but not committed",
|
|
853
|
+
"The timestamp may be outside the git history range"
|
|
854
|
+
]
|
|
855
|
+
|
|
856
|
+
if os.path.isabs(file_path) and not file_path.startswith(os.getcwd()):
|
|
857
|
+
current_repo = os.path.basename(os.getcwd())
|
|
858
|
+
file_repo = "unknown"
|
|
859
|
+
# Try to extract repository name from path
|
|
860
|
+
path_parts = file_path.split("/")
|
|
861
|
+
if "Projects" in path_parts:
|
|
862
|
+
idx = path_parts.index("Projects")
|
|
863
|
+
if idx + 1 < len(path_parts):
|
|
864
|
+
file_repo = path_parts[idx + 1]
|
|
865
|
+
|
|
866
|
+
suggestions.clear()
|
|
867
|
+
suggestions.append(f"This file is from the '{file_repo}' repository")
|
|
868
|
+
suggestions.append(f"The git diff viewer is running from the '{current_repo}' repository")
|
|
869
|
+
suggestions.append("Git diff can only show changes for files in the current repository")
|
|
870
|
+
suggestions.append("To view changes for this file, run the monitoring dashboard from its repository")
|
|
871
|
+
|
|
509
872
|
return {
|
|
873
|
+
"success": False,
|
|
510
874
|
"error": "No git history found for this file",
|
|
511
875
|
"file_path": file_path,
|
|
512
|
-
"suggestions":
|
|
513
|
-
"The file may not be tracked by git",
|
|
514
|
-
"The file may not have any committed changes",
|
|
515
|
-
"The timestamp may be outside the git history range"
|
|
516
|
-
]
|
|
876
|
+
"suggestions": suggestions
|
|
517
877
|
}
|
|
518
878
|
|
|
519
879
|
finally:
|
|
@@ -522,6 +882,7 @@ class SocketIOServer:
|
|
|
522
882
|
except Exception as e:
|
|
523
883
|
self.logger.error(f"Error in _generate_git_diff: {e}")
|
|
524
884
|
return {
|
|
885
|
+
"success": False,
|
|
525
886
|
"error": f"Git diff generation failed: {str(e)}",
|
|
526
887
|
"file_path": file_path
|
|
527
888
|
}
|
|
@@ -630,6 +991,245 @@ class SocketIOServer:
|
|
|
630
991
|
|
|
631
992
|
# Re-broadcast to all other clients
|
|
632
993
|
await self.sio.emit('claude_event', data, skip_sid=sid)
|
|
994
|
+
|
|
995
|
+
@self.sio.event
|
|
996
|
+
async def get_git_branch(sid, working_dir=None):
|
|
997
|
+
"""Get the current git branch for a directory"""
|
|
998
|
+
import subprocess
|
|
999
|
+
try:
|
|
1000
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] get_git_branch called with working_dir: {repr(working_dir)} (type: {type(working_dir)})")
|
|
1001
|
+
|
|
1002
|
+
# Handle case where working_dir is None, empty string, or 'Unknown'
|
|
1003
|
+
original_working_dir = working_dir
|
|
1004
|
+
if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '':
|
|
1005
|
+
working_dir = os.getcwd()
|
|
1006
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
|
|
1007
|
+
else:
|
|
1008
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Using provided working_dir: {working_dir}")
|
|
1009
|
+
|
|
1010
|
+
# Validate that the directory exists and is a valid path
|
|
1011
|
+
if not os.path.exists(working_dir):
|
|
1012
|
+
self.logger.warning(f"[GIT-BRANCH-DEBUG] Directory does not exist: {working_dir}")
|
|
1013
|
+
await self.sio.emit('git_branch_response', {
|
|
1014
|
+
'success': False,
|
|
1015
|
+
'error': f'Directory does not exist: {working_dir}',
|
|
1016
|
+
'working_dir': working_dir,
|
|
1017
|
+
'original_working_dir': original_working_dir
|
|
1018
|
+
}, room=sid)
|
|
1019
|
+
return
|
|
1020
|
+
|
|
1021
|
+
if not os.path.isdir(working_dir):
|
|
1022
|
+
self.logger.warning(f"[GIT-BRANCH-DEBUG] Path is not a directory: {working_dir}")
|
|
1023
|
+
await self.sio.emit('git_branch_response', {
|
|
1024
|
+
'success': False,
|
|
1025
|
+
'error': f'Path is not a directory: {working_dir}',
|
|
1026
|
+
'working_dir': working_dir,
|
|
1027
|
+
'original_working_dir': original_working_dir
|
|
1028
|
+
}, room=sid)
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Running git command in directory: {working_dir}")
|
|
1032
|
+
|
|
1033
|
+
# Run git command to get current branch
|
|
1034
|
+
result = subprocess.run(
|
|
1035
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
1036
|
+
cwd=working_dir,
|
|
1037
|
+
capture_output=True,
|
|
1038
|
+
text=True
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Git command result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
|
|
1042
|
+
|
|
1043
|
+
if result.returncode == 0:
|
|
1044
|
+
branch = result.stdout.strip()
|
|
1045
|
+
self.logger.info(f"[GIT-BRANCH-DEBUG] Successfully got git branch: {branch}")
|
|
1046
|
+
await self.sio.emit('git_branch_response', {
|
|
1047
|
+
'success': True,
|
|
1048
|
+
'branch': branch,
|
|
1049
|
+
'working_dir': working_dir,
|
|
1050
|
+
'original_working_dir': original_working_dir
|
|
1051
|
+
}, room=sid)
|
|
1052
|
+
else:
|
|
1053
|
+
self.logger.warning(f"[GIT-BRANCH-DEBUG] Git command failed: {result.stderr}")
|
|
1054
|
+
await self.sio.emit('git_branch_response', {
|
|
1055
|
+
'success': False,
|
|
1056
|
+
'error': 'Not a git repository',
|
|
1057
|
+
'working_dir': working_dir,
|
|
1058
|
+
'original_working_dir': original_working_dir,
|
|
1059
|
+
'git_error': result.stderr
|
|
1060
|
+
}, room=sid)
|
|
1061
|
+
|
|
1062
|
+
except Exception as e:
|
|
1063
|
+
self.logger.error(f"[GIT-BRANCH-DEBUG] Exception in get_git_branch: {e}")
|
|
1064
|
+
import traceback
|
|
1065
|
+
self.logger.error(f"[GIT-BRANCH-DEBUG] Stack trace: {traceback.format_exc()}")
|
|
1066
|
+
await self.sio.emit('git_branch_response', {
|
|
1067
|
+
'success': False,
|
|
1068
|
+
'error': str(e),
|
|
1069
|
+
'working_dir': working_dir,
|
|
1070
|
+
'original_working_dir': original_working_dir
|
|
1071
|
+
}, room=sid)
|
|
1072
|
+
|
|
1073
|
+
@self.sio.event
|
|
1074
|
+
async def check_file_tracked(sid, data):
|
|
1075
|
+
"""Check if a file is tracked by git"""
|
|
1076
|
+
import subprocess
|
|
1077
|
+
try:
|
|
1078
|
+
file_path = data.get('file_path')
|
|
1079
|
+
working_dir = data.get('working_dir', os.getcwd())
|
|
1080
|
+
|
|
1081
|
+
if not file_path:
|
|
1082
|
+
await self.sio.emit('file_tracked_response', {
|
|
1083
|
+
'success': False,
|
|
1084
|
+
'error': 'file_path is required',
|
|
1085
|
+
'file_path': file_path
|
|
1086
|
+
}, room=sid)
|
|
1087
|
+
return
|
|
1088
|
+
|
|
1089
|
+
# Use git ls-files to check if file is tracked
|
|
1090
|
+
result = subprocess.run(
|
|
1091
|
+
["git", "-C", working_dir, "ls-files", "--", file_path],
|
|
1092
|
+
capture_output=True,
|
|
1093
|
+
text=True
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
is_tracked = result.returncode == 0 and result.stdout.strip()
|
|
1097
|
+
|
|
1098
|
+
await self.sio.emit('file_tracked_response', {
|
|
1099
|
+
'success': True,
|
|
1100
|
+
'file_path': file_path,
|
|
1101
|
+
'working_dir': working_dir,
|
|
1102
|
+
'is_tracked': bool(is_tracked)
|
|
1103
|
+
}, room=sid)
|
|
1104
|
+
|
|
1105
|
+
except Exception as e:
|
|
1106
|
+
self.logger.error(f"Error checking file tracked status: {e}")
|
|
1107
|
+
await self.sio.emit('file_tracked_response', {
|
|
1108
|
+
'success': False,
|
|
1109
|
+
'error': str(e),
|
|
1110
|
+
'file_path': data.get('file_path', 'unknown')
|
|
1111
|
+
}, room=sid)
|
|
1112
|
+
|
|
1113
|
+
@self.sio.event
|
|
1114
|
+
async def read_file(sid, data):
|
|
1115
|
+
"""Read file contents safely"""
|
|
1116
|
+
try:
|
|
1117
|
+
file_path = data.get('file_path')
|
|
1118
|
+
working_dir = data.get('working_dir', os.getcwd())
|
|
1119
|
+
max_size = data.get('max_size', 1024 * 1024) # 1MB default limit
|
|
1120
|
+
|
|
1121
|
+
if not file_path:
|
|
1122
|
+
await self.sio.emit('file_content_response', {
|
|
1123
|
+
'success': False,
|
|
1124
|
+
'error': 'file_path is required',
|
|
1125
|
+
'file_path': file_path
|
|
1126
|
+
}, room=sid)
|
|
1127
|
+
return
|
|
1128
|
+
|
|
1129
|
+
# Use the shared file reading logic
|
|
1130
|
+
result = await self._read_file_safely(file_path, working_dir, max_size)
|
|
1131
|
+
|
|
1132
|
+
# Send the result back to the client
|
|
1133
|
+
await self.sio.emit('file_content_response', result, room=sid)
|
|
1134
|
+
|
|
1135
|
+
except Exception as e:
|
|
1136
|
+
self.logger.error(f"Error reading file: {e}")
|
|
1137
|
+
await self.sio.emit('file_content_response', {
|
|
1138
|
+
'success': False,
|
|
1139
|
+
'error': str(e),
|
|
1140
|
+
'file_path': data.get('file_path', 'unknown')
|
|
1141
|
+
}, room=sid)
|
|
1142
|
+
|
|
1143
|
+
@self.sio.event
|
|
1144
|
+
async def git_add_file(sid, data):
|
|
1145
|
+
"""Add file to git tracking"""
|
|
1146
|
+
import subprocess
|
|
1147
|
+
try:
|
|
1148
|
+
file_path = data.get('file_path')
|
|
1149
|
+
working_dir = data.get('working_dir', os.getcwd())
|
|
1150
|
+
|
|
1151
|
+
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)})")
|
|
1152
|
+
|
|
1153
|
+
if not file_path:
|
|
1154
|
+
await self.sio.emit('git_add_response', {
|
|
1155
|
+
'success': False,
|
|
1156
|
+
'error': 'file_path is required',
|
|
1157
|
+
'file_path': file_path
|
|
1158
|
+
}, room=sid)
|
|
1159
|
+
return
|
|
1160
|
+
|
|
1161
|
+
# Validate and sanitize working_dir
|
|
1162
|
+
original_working_dir = working_dir
|
|
1163
|
+
if not working_dir or working_dir == 'Unknown' or working_dir.strip() == '' or working_dir == '.':
|
|
1164
|
+
working_dir = os.getcwd()
|
|
1165
|
+
self.logger.info(f"[GIT-ADD-DEBUG] working_dir was invalid ({repr(original_working_dir)}), using cwd: {working_dir}")
|
|
1166
|
+
else:
|
|
1167
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Using provided working_dir: {working_dir}")
|
|
1168
|
+
|
|
1169
|
+
# Validate that the directory exists and is a valid path
|
|
1170
|
+
if not os.path.exists(working_dir):
|
|
1171
|
+
self.logger.warning(f"[GIT-ADD-DEBUG] Directory does not exist: {working_dir}")
|
|
1172
|
+
await self.sio.emit('git_add_response', {
|
|
1173
|
+
'success': False,
|
|
1174
|
+
'error': f'Directory does not exist: {working_dir}',
|
|
1175
|
+
'file_path': file_path,
|
|
1176
|
+
'working_dir': working_dir,
|
|
1177
|
+
'original_working_dir': original_working_dir
|
|
1178
|
+
}, room=sid)
|
|
1179
|
+
return
|
|
1180
|
+
|
|
1181
|
+
if not os.path.isdir(working_dir):
|
|
1182
|
+
self.logger.warning(f"[GIT-ADD-DEBUG] Path is not a directory: {working_dir}")
|
|
1183
|
+
await self.sio.emit('git_add_response', {
|
|
1184
|
+
'success': False,
|
|
1185
|
+
'error': f'Path is not a directory: {working_dir}',
|
|
1186
|
+
'file_path': file_path,
|
|
1187
|
+
'working_dir': working_dir,
|
|
1188
|
+
'original_working_dir': original_working_dir
|
|
1189
|
+
}, room=sid)
|
|
1190
|
+
return
|
|
1191
|
+
|
|
1192
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Running git add command in directory: {working_dir}")
|
|
1193
|
+
|
|
1194
|
+
# Use git add to track the file
|
|
1195
|
+
result = subprocess.run(
|
|
1196
|
+
["git", "-C", working_dir, "add", file_path],
|
|
1197
|
+
capture_output=True,
|
|
1198
|
+
text=True
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Git add result: returncode={result.returncode}, stdout={repr(result.stdout)}, stderr={repr(result.stderr)}")
|
|
1202
|
+
|
|
1203
|
+
if result.returncode == 0:
|
|
1204
|
+
self.logger.info(f"[GIT-ADD-DEBUG] Successfully added {file_path} to git in {working_dir}")
|
|
1205
|
+
await self.sio.emit('git_add_response', {
|
|
1206
|
+
'success': True,
|
|
1207
|
+
'file_path': file_path,
|
|
1208
|
+
'working_dir': working_dir,
|
|
1209
|
+
'original_working_dir': original_working_dir,
|
|
1210
|
+
'message': 'File successfully added to git tracking'
|
|
1211
|
+
}, room=sid)
|
|
1212
|
+
else:
|
|
1213
|
+
error_message = result.stderr.strip() or 'Unknown git error'
|
|
1214
|
+
self.logger.warning(f"[GIT-ADD-DEBUG] Git add failed: {error_message}")
|
|
1215
|
+
await self.sio.emit('git_add_response', {
|
|
1216
|
+
'success': False,
|
|
1217
|
+
'error': f'Git add failed: {error_message}',
|
|
1218
|
+
'file_path': file_path,
|
|
1219
|
+
'working_dir': working_dir,
|
|
1220
|
+
'original_working_dir': original_working_dir
|
|
1221
|
+
}, room=sid)
|
|
1222
|
+
|
|
1223
|
+
except Exception as e:
|
|
1224
|
+
self.logger.error(f"[GIT-ADD-DEBUG] Exception in git_add_file: {e}")
|
|
1225
|
+
import traceback
|
|
1226
|
+
self.logger.error(f"[GIT-ADD-DEBUG] Stack trace: {traceback.format_exc()}")
|
|
1227
|
+
await self.sio.emit('git_add_response', {
|
|
1228
|
+
'success': False,
|
|
1229
|
+
'error': str(e),
|
|
1230
|
+
'file_path': data.get('file_path', 'unknown'),
|
|
1231
|
+
'working_dir': data.get('working_dir', 'unknown')
|
|
1232
|
+
}, room=sid)
|
|
633
1233
|
|
|
634
1234
|
async def _send_current_status(self, sid: str):
|
|
635
1235
|
"""Send current system status to a client."""
|
|
@@ -737,18 +1337,22 @@ class SocketIOServer:
|
|
|
737
1337
|
|
|
738
1338
|
# Broadcast to clients with timeout and error handling
|
|
739
1339
|
try:
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1340
|
+
# Check if the event loop is still running and not closed
|
|
1341
|
+
if self.loop and not self.loop.is_closed() and self.loop.is_running():
|
|
1342
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
1343
|
+
self.sio.emit('claude_event', event),
|
|
1344
|
+
self.loop
|
|
1345
|
+
)
|
|
1346
|
+
# Wait for completion with timeout to detect issues
|
|
1347
|
+
try:
|
|
1348
|
+
future.result(timeout=2.0) # 2 second timeout
|
|
1349
|
+
self.logger.debug(f"📨 Successfully broadcasted {event_type} to {len(self.clients)} clients")
|
|
1350
|
+
except asyncio.TimeoutError:
|
|
1351
|
+
self.logger.warning(f"⏰ Broadcast timeout for event {event_type} - continuing anyway")
|
|
1352
|
+
except Exception as emit_error:
|
|
1353
|
+
self.logger.error(f"❌ Broadcast emit error for {event_type}: {emit_error}")
|
|
1354
|
+
else:
|
|
1355
|
+
self.logger.warning(f"⚠️ Event loop not available for broadcast of {event_type} - event loop closed or not running")
|
|
752
1356
|
except Exception as e:
|
|
753
1357
|
self.logger.error(f"❌ Failed to submit broadcast to event loop: {e}")
|
|
754
1358
|
import traceback
|
|
@@ -919,4 +1523,4 @@ def stop_socketio_server():
|
|
|
919
1523
|
global _socketio_server
|
|
920
1524
|
if _socketio_server:
|
|
921
1525
|
_socketio_server.stop()
|
|
922
|
-
_socketio_server = None
|
|
1526
|
+
_socketio_server = None
|