shell-mirror 1.5.124 → 1.5.126

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.
@@ -807,15 +807,79 @@ async function createPeerConnection(clientId) {
807
807
  };
808
808
  }
809
809
 
810
- function cleanup(clientId = null) {
810
+ async function cleanup(clientId = null) {
811
811
  // Disconnect client from session manager
812
812
  if (clientId) {
813
813
  sessionManager.disconnectClient(clientId);
814
814
  } else {
815
- // Full agent shutdown - stop heartbeat system
815
+ // Full agent shutdown - send final heartbeat with empty sessions BEFORE stopping heartbeat
816
+ logToFile('[AGENT] Sending final heartbeat with empty sessions before shutdown...');
817
+
818
+ if (heartbeatInterval) {
819
+ const finalHeartbeatData = JSON.stringify({
820
+ agentId: AGENT_ID,
821
+ timestamp: Date.now(),
822
+ activeSessions: 0,
823
+ sessions: [], // Empty session list to clear dashboard
824
+ localPort: process.env.LOCAL_PORT || 8080,
825
+ capabilities: ['webrtc', 'direct_websocket'],
826
+ status: 'shutting_down'
827
+ });
828
+
829
+ const options = {
830
+ hostname: 'shellmirror.app',
831
+ port: 443,
832
+ path: '/php-backend/api/agent-heartbeat.php',
833
+ method: 'POST',
834
+ headers: {
835
+ 'Content-Type': 'application/json',
836
+ 'Content-Length': Buffer.byteLength(finalHeartbeatData),
837
+ 'X-Agent-Secret': 'mac-agent-secret-2024',
838
+ 'X-Agent-ID': AGENT_ID
839
+ }
840
+ };
841
+
842
+ try {
843
+ await new Promise((resolve, reject) => {
844
+ const req = https.request(options, (res) => {
845
+ let responseData = '';
846
+ res.on('data', (chunk) => {
847
+ responseData += chunk;
848
+ });
849
+ res.on('end', () => {
850
+ if (res.statusCode === 200) {
851
+ logToFile('[AGENT] ✅ Sent final heartbeat with empty sessions');
852
+ resolve();
853
+ } else {
854
+ logToFile(`[AGENT] ⚠️ Final heartbeat HTTP error: ${res.statusCode}`);
855
+ resolve(); // Continue shutdown even if heartbeat fails
856
+ }
857
+ });
858
+ });
859
+
860
+ req.on('error', (error) => {
861
+ logToFile(`[AGENT] ❌ Failed to send final heartbeat: ${error.message}`);
862
+ resolve(); // Continue shutdown even if heartbeat fails
863
+ });
864
+
865
+ req.setTimeout(5000, () => {
866
+ req.destroy();
867
+ logToFile('[AGENT] ⚠️ Final heartbeat timed out after 5s');
868
+ resolve(); // Continue shutdown even if heartbeat times out
869
+ });
870
+
871
+ req.write(finalHeartbeatData);
872
+ req.end();
873
+ });
874
+ } catch (error) {
875
+ logToFile(`[AGENT] ❌ Error sending final heartbeat: ${error.message}`);
876
+ }
877
+ }
878
+
879
+ // Now stop heartbeat system
816
880
  stopHeartbeatSystem();
817
881
  }
818
-
882
+
819
883
  if (dataChannel) {
820
884
  dataChannel.close();
821
885
  dataChannel = null;
@@ -1058,17 +1122,17 @@ function sendMessage(message) {
1058
1122
  }
1059
1123
 
1060
1124
  // Graceful shutdown handling
1061
- process.on('SIGINT', () => {
1125
+ process.on('SIGINT', async () => {
1062
1126
  console.log('\n[AGENT] Shutting down gracefully...');
1063
- cleanup();
1127
+ await cleanup();
1064
1128
  if (ws) ws.close();
1065
1129
  if (localServer) localServer.close();
1066
1130
  process.exit(0);
1067
1131
  });
1068
1132
 
1069
- process.on('SIGTERM', () => {
1133
+ process.on('SIGTERM', async () => {
1070
1134
  console.log('\n[AGENT] Received SIGTERM, shutting down...');
1071
- cleanup();
1135
+ await cleanup();
1072
1136
  if (ws) ws.close();
1073
1137
  if (localServer) localServer.close();
1074
1138
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.124",
3
+ "version": "1.5.126",
4
4
  "description": "Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -56,6 +56,10 @@ let currentSession = null;
56
56
  let availableSessions = [];
57
57
  let requestedSessionId = null; // For connecting to specific session from URL
58
58
 
59
+ // Connection status messaging
60
+ let connectionStatusMessage = 'Connecting to agent...';
61
+ let connectionTimeoutWarning = null;
62
+
59
63
  // Chunk reassembly for large messages
60
64
  const chunkAssembler = {
61
65
  activeChunks: new Map(),
@@ -166,6 +170,31 @@ function updateConnectionStatus(status) {
166
170
  // else: disconnected (default red)
167
171
  }
168
172
 
173
+ // Set connection status message (shown in tab bar when no sessions)
174
+ function setConnectionMessage(message, writeToTerminal = true) {
175
+ connectionStatusMessage = message;
176
+ console.log('[CLIENT] 📢 Connection message:', message);
177
+
178
+ // Update the tab bar display
179
+ updateSessionDisplay();
180
+
181
+ // Optionally write to terminal for visibility
182
+ if (writeToTerminal && term) {
183
+ term.write(`\r\n\x1b[36m${message}\x1b[0m\r\n`); // Cyan color
184
+ }
185
+ }
186
+
187
+ // Clear connection timeout warnings (called when connection succeeds)
188
+ function clearConnectionTimeouts() {
189
+ if (connectionTimeoutWarning) {
190
+ clearTimeout(connectionTimeoutWarning.timeout10s);
191
+ clearTimeout(connectionTimeoutWarning.timeout30s);
192
+ clearTimeout(connectionTimeoutWarning.timeout60s);
193
+ connectionTimeoutWarning = null;
194
+ console.log('[CLIENT] ✅ Connection timeout warnings cleared');
195
+ }
196
+ }
197
+
169
198
  // Cleanup timer for chunk assembler
170
199
  setInterval(() => {
171
200
  chunkAssembler.cleanup();
@@ -244,6 +273,9 @@ function startConnection() {
244
273
  terminalContainer.classList.add('show');
245
274
  term.open(document.getElementById('terminal'));
246
275
 
276
+ // Show initial connection message
277
+ setConnectionMessage('🔗 Connecting to agent...', true);
278
+
247
279
  // Initialize session display (shows header with connection status even before session exists)
248
280
  updateSessionDisplay();
249
281
 
@@ -262,6 +294,40 @@ function startConnection() {
262
294
  fitAddon.fit();
263
295
  term.focus(); // Ensure cursor is visible even before connection
264
296
  }, 100);
297
+
298
+ // Set up connection timeout warnings
299
+ const timeout10s = setTimeout(() => {
300
+ if (!currentSession) {
301
+ setConnectionMessage('⏱️ Taking longer than usual... Please wait', true);
302
+ term.write('\r\n\x1b[33m⏱️ Connection is taking longer than expected...\x1b[0m\r\n');
303
+ }
304
+ }, 10000);
305
+
306
+ const timeout30s = setTimeout(() => {
307
+ if (!currentSession) {
308
+ setConnectionMessage('⚠️ Connection very slow - Agent may be offline', true);
309
+ term.write('\x1b[33m⚠️ Still trying to connect. The agent may be offline or unreachable.\x1b[0m\r\n');
310
+ term.write('\x1b[36m💡 Tip: Check the agent status on the Dashboard\x1b[0m\r\n');
311
+ }
312
+ }, 30000);
313
+
314
+ const timeout60s = setTimeout(() => {
315
+ if (!currentSession) {
316
+ updateConnectionStatus('disconnected');
317
+ setConnectionMessage('❌ Failed to connect - Agent or session unavailable', false);
318
+ term.write('\r\n\r\n\x1b[31m❌ Connection Failed\x1b[0m\r\n');
319
+ term.write('\x1b[33mThe agent or session you requested is not available.\x1b[0m\r\n');
320
+ term.write('\r\n\x1b[36mPossible reasons:\x1b[0m\r\n');
321
+ term.write(' • Agent is offline or shut down\r\n');
322
+ term.write(' • Session was terminated\r\n');
323
+ term.write(' • Network connectivity issues\r\n');
324
+ term.write('\r\n\x1b[36m💡 Click Dashboard to return and try another session\x1b[0m\r\n');
325
+ }
326
+ }, 60000);
327
+
328
+ // Store timeout IDs so they can be cleared on successful connection
329
+ connectionTimeoutWarning = { timeout10s, timeout30s, timeout60s };
330
+
265
331
  initialize();
266
332
  }
267
333
 
@@ -275,17 +341,20 @@ async function initialize() {
275
341
 
276
342
  if (directConnectionSuccess) {
277
343
  console.log('[CLIENT] ✅ Direct connection established - no server needed!');
344
+ setConnectionMessage('✅ Connected via local network!', true);
278
345
  return;
279
346
  }
280
-
347
+
281
348
  console.log('[CLIENT] ⚠️ Direct connection failed, falling back to WebRTC signaling...');
349
+ setConnectionMessage('🌐 Attempting WebRTC connection...', true);
282
350
  await initializeWebRTCSignaling();
283
351
  }
284
352
 
285
353
  async function tryDirectConnection() {
286
354
  console.log('[CLIENT] 🔗 Attempting direct connection to agent...');
287
355
  updateConnectionStatus('connecting');
288
-
356
+ setConnectionMessage('🔍 Trying direct connection to local network...', true);
357
+
289
358
  // Get agent data from API to find local connection details
290
359
  try {
291
360
  const response = await fetch('/php-backend/api/agents-list.php', {
@@ -394,6 +463,10 @@ function setupDirectConnection(directWs) {
394
463
  case 'session_created':
395
464
  console.log('[CLIENT] ✅ Direct session created:', data.sessionId);
396
465
 
466
+ // Clear connection timeout warnings
467
+ clearConnectionTimeouts();
468
+ setConnectionMessage('✅ Session created successfully!', false);
469
+
397
470
  // Update current session
398
471
  currentSession = {
399
472
  id: data.sessionId,
@@ -570,6 +643,10 @@ async function initializeWebRTCSignaling() {
570
643
 
571
644
  // Handle session assignment from agent
572
645
  if (nextData.sessionId) {
646
+ // Clear connection timeout warnings
647
+ clearConnectionTimeouts();
648
+ setConnectionMessage('✅ Session connected!', false);
649
+
573
650
  currentSession = {
574
651
  id: nextData.sessionId,
575
652
  name: nextData.sessionName || 'Terminal Session',
@@ -577,7 +654,7 @@ async function initializeWebRTCSignaling() {
577
654
  };
578
655
  console.log('[CLIENT] 📋 Session assigned:', currentSession);
579
656
  console.log('[CLIENT] 🔍 Agent ID for storage:', AGENT_ID);
580
-
657
+
581
658
  // Update UI to show session info
582
659
  updateSessionDisplay();
583
660
 
@@ -793,6 +870,7 @@ async function createPeerConnection() {
793
870
  console.log('[CLIENT] ❌ ICE connection failed - no viable candidates');
794
871
  console.log('[CLIENT] 💡 Troubleshooting: This may be due to firewall/NAT issues or blocked STUN servers');
795
872
  updateConnectionStatus('disconnected');
873
+ setConnectionMessage('❌ Unable to connect - Agent may be offline', false);
796
874
  term.write('\r\n\r\n❌ Connection failed: Network connectivity issues\r\n');
797
875
  term.write('💡 This may be due to:\r\n');
798
876
  term.write(' • Firewall blocking WebRTC traffic\r\n');
@@ -1043,8 +1121,8 @@ function renderTabs() {
1043
1121
  // Add new session button only when we have sessions
1044
1122
  tabsHTML += '<button class="session-tab-new" onclick="createNewSession()" title="New Session">+</button>';
1045
1123
  } else {
1046
- // No sessions available - show status message
1047
- tabsHTML = '<div style="color: #888; font-size: 0.85rem; padding: 6px 12px;">No sessions available</div>';
1124
+ // No sessions - show connection status message
1125
+ tabsHTML = `<div style="color: #888; font-size: 0.85rem; padding: 6px 12px;">${connectionStatusMessage}</div>`;
1048
1126
  }
1049
1127
 
1050
1128
  tabBar.innerHTML = tabsHTML;