shell-mirror 1.5.39 → 1.5.41
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.
- package/mac-agent/agent-debug.log +9 -94
- package/mac-agent/agent.js +372 -64
- package/package.json +1 -1
- package/public/app/dashboard.css +172 -0
- package/public/app/dashboard.js +150 -6
- package/public/app/terminal.html +146 -3
- package/public/app/terminal.js +209 -5
package/public/app/terminal.js
CHANGED
|
@@ -50,18 +50,45 @@ let AGENT_ID;
|
|
|
50
50
|
let CLIENT_ID;
|
|
51
51
|
let SELECTED_AGENT; // Store full agent data including WebSocket URL
|
|
52
52
|
|
|
53
|
+
// Session management
|
|
54
|
+
let currentSession = null;
|
|
55
|
+
let availableSessions = [];
|
|
56
|
+
let requestedSessionId = null; // For connecting to specific session from URL
|
|
57
|
+
|
|
58
|
+
// Connection status management
|
|
59
|
+
function updateConnectionStatus(status) {
|
|
60
|
+
const statusElement = document.getElementById('connection-status');
|
|
61
|
+
if (!statusElement) return;
|
|
62
|
+
|
|
63
|
+
statusElement.className = 'connection-status';
|
|
64
|
+
switch(status) {
|
|
65
|
+
case 'connecting':
|
|
66
|
+
statusElement.classList.add('connecting');
|
|
67
|
+
break;
|
|
68
|
+
case 'connected':
|
|
69
|
+
statusElement.classList.add('connected');
|
|
70
|
+
break;
|
|
71
|
+
case 'disconnected':
|
|
72
|
+
default:
|
|
73
|
+
// Default red styling already applied
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
// Check for agent parameter and connect directly
|
|
54
79
|
window.addEventListener('load', () => {
|
|
55
80
|
loadVersionInfo();
|
|
56
81
|
|
|
57
|
-
// Get agent ID from URL
|
|
82
|
+
// Get agent ID and session ID from URL parameters
|
|
58
83
|
const urlParams = new URLSearchParams(window.location.search);
|
|
59
84
|
const agentId = urlParams.get('agent');
|
|
85
|
+
const sessionId = urlParams.get('session');
|
|
60
86
|
|
|
61
87
|
if (agentId) {
|
|
62
88
|
AGENT_ID = agentId;
|
|
63
89
|
SELECTED_AGENT = { id: agentId, agentId: agentId };
|
|
64
|
-
|
|
90
|
+
requestedSessionId = sessionId; // Store for session request
|
|
91
|
+
console.log('[CLIENT] 🔗 Connecting to agent:', agentId, sessionId ? `session: ${sessionId}` : '(new session)');
|
|
65
92
|
startConnection();
|
|
66
93
|
} else {
|
|
67
94
|
// No agent specified, redirect to dashboard
|
|
@@ -97,8 +124,9 @@ async function loadVersionInfo() {
|
|
|
97
124
|
|
|
98
125
|
|
|
99
126
|
function startConnection() {
|
|
127
|
+
updateConnectionStatus('connecting');
|
|
100
128
|
connectContainer.style.display = 'none';
|
|
101
|
-
terminalContainer.
|
|
129
|
+
terminalContainer.classList.add('show');
|
|
102
130
|
term.open(document.getElementById('terminal'));
|
|
103
131
|
// Delay fit to ensure proper dimensions after CSS transitions
|
|
104
132
|
setTimeout(() => {
|
|
@@ -143,7 +171,23 @@ async function initialize() {
|
|
|
143
171
|
// Start polling to connect to the agent
|
|
144
172
|
const intervalId = setInterval(() => {
|
|
145
173
|
console.log(`[CLIENT] 📞 Sending client-hello to Agent: ${AGENT_ID}`);
|
|
146
|
-
|
|
174
|
+
|
|
175
|
+
// Build session request
|
|
176
|
+
let sessionRequest = null;
|
|
177
|
+
if (requestedSessionId) {
|
|
178
|
+
sessionRequest = { sessionId: requestedSessionId };
|
|
179
|
+
console.log(`[CLIENT] 🎯 Requesting existing session: ${requestedSessionId}`);
|
|
180
|
+
} else {
|
|
181
|
+
sessionRequest = { newSession: true };
|
|
182
|
+
console.log(`[CLIENT] 🆕 Requesting new session`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sent = sendMessage({
|
|
186
|
+
type: 'client-hello',
|
|
187
|
+
from: CLIENT_ID,
|
|
188
|
+
to: AGENT_ID,
|
|
189
|
+
sessionRequest: sessionRequest
|
|
190
|
+
});
|
|
147
191
|
if (!sent) {
|
|
148
192
|
console.error(`[CLIENT] ❌ Failed to send client-hello - stopping attempts`);
|
|
149
193
|
clearInterval(intervalId);
|
|
@@ -169,6 +213,24 @@ async function initialize() {
|
|
|
169
213
|
console.log('[CLIENT] Received offer from agent. Stopping client-hello retries.');
|
|
170
214
|
clearInterval(intervalId);
|
|
171
215
|
|
|
216
|
+
// Handle session assignment from agent
|
|
217
|
+
if (nextData.sessionId) {
|
|
218
|
+
currentSession = {
|
|
219
|
+
id: nextData.sessionId,
|
|
220
|
+
name: nextData.sessionName || 'Terminal Session',
|
|
221
|
+
isNewSession: nextData.isNewSession || false
|
|
222
|
+
};
|
|
223
|
+
console.log('[CLIENT] 📋 Session assigned:', currentSession);
|
|
224
|
+
|
|
225
|
+
// Update UI to show session info
|
|
226
|
+
updateSessionDisplay();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (nextData.availableSessions) {
|
|
230
|
+
availableSessions = nextData.availableSessions;
|
|
231
|
+
console.log('[CLIENT] 📚 Available sessions:', availableSessions);
|
|
232
|
+
}
|
|
233
|
+
|
|
172
234
|
console.log('[CLIENT] Received WebRTC offer from agent.');
|
|
173
235
|
await createPeerConnection();
|
|
174
236
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(nextData));
|
|
@@ -346,25 +408,31 @@ async function createPeerConnection() {
|
|
|
346
408
|
break;
|
|
347
409
|
case 'connected':
|
|
348
410
|
console.log('[CLIENT] ✅ WebRTC connection established!');
|
|
411
|
+
updateConnectionStatus('connected');
|
|
349
412
|
break;
|
|
350
413
|
case 'completed':
|
|
351
414
|
console.log('[CLIENT] ✅ ICE connection completed successfully!');
|
|
415
|
+
updateConnectionStatus('connected');
|
|
352
416
|
break;
|
|
353
417
|
case 'failed':
|
|
354
418
|
console.log('[CLIENT] ❌ ICE connection failed - no viable candidates');
|
|
355
419
|
console.log('[CLIENT] 💡 Troubleshooting: This may be due to firewall/NAT issues or blocked STUN servers');
|
|
420
|
+
updateConnectionStatus('disconnected');
|
|
356
421
|
term.write('\r\n\r\n❌ Connection failed: Network connectivity issues\r\n');
|
|
357
422
|
term.write('💡 This may be due to:\r\n');
|
|
358
423
|
term.write(' • Firewall blocking WebRTC traffic\r\n');
|
|
359
424
|
term.write(' • Corporate network restrictions\r\n');
|
|
360
425
|
term.write(' • STUN/TURN servers unreachable\r\n');
|
|
361
|
-
term.write('
|
|
426
|
+
term.write(' • Agent may have crashed or disconnected\r\n');
|
|
427
|
+
term.write('\r\n🔄 Click Dashboard to return and try another agent\r\n');
|
|
362
428
|
break;
|
|
363
429
|
case 'disconnected':
|
|
364
430
|
console.log('[CLIENT] ⚠️ ICE connection disconnected');
|
|
431
|
+
updateConnectionStatus('disconnected');
|
|
365
432
|
break;
|
|
366
433
|
case 'closed':
|
|
367
434
|
console.log('[CLIENT] 🔐 ICE connection closed');
|
|
435
|
+
updateConnectionStatus('disconnected');
|
|
368
436
|
break;
|
|
369
437
|
}
|
|
370
438
|
};
|
|
@@ -432,6 +500,9 @@ function setupDataChannel() {
|
|
|
432
500
|
const message = JSON.parse(event.data);
|
|
433
501
|
if (message.type === 'output') {
|
|
434
502
|
term.write(message.data);
|
|
503
|
+
} else {
|
|
504
|
+
// Handle session-related messages
|
|
505
|
+
handleSessionMessage(message);
|
|
435
506
|
}
|
|
436
507
|
} catch (err) {
|
|
437
508
|
console.error('[CLIENT] Error parsing data channel message:', err);
|
|
@@ -440,12 +511,16 @@ function setupDataChannel() {
|
|
|
440
511
|
|
|
441
512
|
dataChannel.onclose = () => {
|
|
442
513
|
console.log('[CLIENT] Data channel closed.');
|
|
514
|
+
updateConnectionStatus('disconnected');
|
|
443
515
|
term.write('\r\n\r\n\x1b[31m❌ Terminal session ended.\x1b[0m\r\n');
|
|
516
|
+
term.write('🔄 Click Dashboard to return and start a new session\r\n');
|
|
444
517
|
};
|
|
445
518
|
|
|
446
519
|
dataChannel.onerror = (error) => {
|
|
447
520
|
console.error('[CLIENT] Data channel error:', error);
|
|
521
|
+
updateConnectionStatus('disconnected');
|
|
448
522
|
term.write('\r\n\r\n\x1b[31m❌ Data channel error occurred.\x1b[0m\r\n');
|
|
523
|
+
term.write('🔄 Click Dashboard to return and try again\r\n');
|
|
449
524
|
};
|
|
450
525
|
|
|
451
526
|
term.onData((data) => {
|
|
@@ -490,4 +565,133 @@ function sendMessage(message) {
|
|
|
490
565
|
console.error(`[CLIENT] ❌ Error sending message:`, error);
|
|
491
566
|
return false;
|
|
492
567
|
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Session Management Functions
|
|
571
|
+
function updateSessionDisplay() {
|
|
572
|
+
const sessionHeader = document.getElementById('session-header');
|
|
573
|
+
const sessionName = document.getElementById('session-name');
|
|
574
|
+
const sessionId = document.getElementById('session-id');
|
|
575
|
+
|
|
576
|
+
if (currentSession) {
|
|
577
|
+
sessionHeader.style.display = 'flex';
|
|
578
|
+
sessionName.textContent = currentSession.name;
|
|
579
|
+
sessionId.textContent = `(${currentSession.id.substring(0, 8)}...)`;
|
|
580
|
+
|
|
581
|
+
// Update available sessions dropdown
|
|
582
|
+
updateSessionDropdown();
|
|
583
|
+
|
|
584
|
+
console.log('[CLIENT] 📋 Session display updated:', currentSession);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function updateSessionDropdown() {
|
|
589
|
+
const dropdownContent = document.getElementById('session-dropdown-content');
|
|
590
|
+
|
|
591
|
+
// Clear existing items except "New Session"
|
|
592
|
+
dropdownContent.innerHTML = `
|
|
593
|
+
<div class="session-dropdown-item" onclick="createNewSession()">
|
|
594
|
+
<span>+ New Session</span>
|
|
595
|
+
</div>
|
|
596
|
+
`;
|
|
597
|
+
|
|
598
|
+
// Add available sessions
|
|
599
|
+
if (availableSessions && availableSessions.length > 0) {
|
|
600
|
+
availableSessions.forEach(session => {
|
|
601
|
+
const item = document.createElement('div');
|
|
602
|
+
item.className = 'session-dropdown-item';
|
|
603
|
+
if (currentSession && session.id === currentSession.id) {
|
|
604
|
+
item.classList.add('current');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
item.innerHTML = `
|
|
608
|
+
<span>${session.name}</span>
|
|
609
|
+
<small>${formatLastActivity(session.lastActivity)}</small>
|
|
610
|
+
`;
|
|
611
|
+
item.onclick = () => switchToSession(session.id);
|
|
612
|
+
dropdownContent.appendChild(item);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function formatLastActivity(timestamp) {
|
|
618
|
+
const now = Date.now();
|
|
619
|
+
const diff = now - timestamp;
|
|
620
|
+
const minutes = Math.floor(diff / 60000);
|
|
621
|
+
const hours = Math.floor(diff / 3600000);
|
|
622
|
+
const days = Math.floor(diff / 86400000);
|
|
623
|
+
|
|
624
|
+
if (minutes < 1) return 'now';
|
|
625
|
+
if (minutes < 60) return `${minutes}m`;
|
|
626
|
+
if (hours < 24) return `${hours}h`;
|
|
627
|
+
return `${days}d`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function switchToSession(sessionId) {
|
|
631
|
+
if (!dataChannel || dataChannel.readyState !== 'open') {
|
|
632
|
+
console.error('[CLIENT] ❌ Cannot switch session - data channel not open');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
console.log('[CLIENT] 🔄 Switching to session:', sessionId);
|
|
637
|
+
dataChannel.send(JSON.stringify({
|
|
638
|
+
type: 'session-switch',
|
|
639
|
+
sessionId: sessionId
|
|
640
|
+
}));
|
|
641
|
+
|
|
642
|
+
// Hide dropdown
|
|
643
|
+
document.getElementById('session-dropdown-content').classList.remove('show');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function createNewSession() {
|
|
647
|
+
// Navigate to terminal with new session request
|
|
648
|
+
const url = new URL(window.location.href);
|
|
649
|
+
url.searchParams.delete('session'); // Remove session param to create new one
|
|
650
|
+
window.location.href = url.toString();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Setup dropdown toggle
|
|
654
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
655
|
+
const dropdownBtn = document.getElementById('session-dropdown-btn');
|
|
656
|
+
const dropdownContent = document.getElementById('session-dropdown-content');
|
|
657
|
+
|
|
658
|
+
if (dropdownBtn && dropdownContent) {
|
|
659
|
+
dropdownBtn.onclick = (e) => {
|
|
660
|
+
e.stopPropagation();
|
|
661
|
+
dropdownContent.classList.toggle('show');
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// Close dropdown when clicking outside
|
|
665
|
+
document.addEventListener('click', () => {
|
|
666
|
+
dropdownContent.classList.remove('show');
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Handle session-related data channel messages
|
|
672
|
+
function handleSessionMessage(message) {
|
|
673
|
+
switch (message.type) {
|
|
674
|
+
case 'session-switched':
|
|
675
|
+
currentSession = {
|
|
676
|
+
id: message.sessionId,
|
|
677
|
+
name: message.sessionName || 'Terminal Session'
|
|
678
|
+
};
|
|
679
|
+
updateSessionDisplay();
|
|
680
|
+
term.clear(); // Clear terminal for new session
|
|
681
|
+
console.log('[CLIENT] ✅ Switched to session:', currentSession);
|
|
682
|
+
break;
|
|
683
|
+
case 'session-ended':
|
|
684
|
+
term.write(`\r\n\x1b[31m❌ Session ended: ${message.reason}\x1b[0m\r\n`);
|
|
685
|
+
if (message.code) {
|
|
686
|
+
term.write(`Exit code: ${message.code}\r\n`);
|
|
687
|
+
}
|
|
688
|
+
break;
|
|
689
|
+
case 'session-terminated':
|
|
690
|
+
term.write(`\r\n\x1b[31m❌ Session terminated\x1b[0m\r\n`);
|
|
691
|
+
term.write('🔄 Click Dashboard to start a new session\r\n');
|
|
692
|
+
break;
|
|
693
|
+
case 'error':
|
|
694
|
+
term.write(`\r\n\x1b[31m❌ Error: ${message.message}\x1b[0m\r\n`);
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
493
697
|
}
|