shell-mirror 1.5.65 → 1.5.67

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.
@@ -419,7 +419,9 @@ async function sendHeartbeat() {
419
419
  const heartbeatData = JSON.stringify({
420
420
  agentId: AGENT_ID,
421
421
  timestamp: Date.now(),
422
- activeSessions: Object.keys(sessions).length
422
+ activeSessions: Object.keys(sessionManager.sessions).length,
423
+ localPort: process.env.LOCAL_PORT || 8080,
424
+ capabilities: ['webrtc', 'direct_websocket']
423
425
  });
424
426
 
425
427
  const options = {
@@ -945,6 +947,7 @@ process.on('SIGINT', () => {
945
947
  console.log('\n[AGENT] Shutting down gracefully...');
946
948
  cleanup();
947
949
  if (ws) ws.close();
950
+ if (localServer) localServer.close();
948
951
  process.exit(0);
949
952
  });
950
953
 
@@ -952,10 +955,122 @@ process.on('SIGTERM', () => {
952
955
  console.log('\n[AGENT] Received SIGTERM, shutting down...');
953
956
  cleanup();
954
957
  if (ws) ws.close();
958
+ if (localServer) localServer.close();
955
959
  process.exit(0);
956
960
  });
957
961
 
962
+ // --- Local WebSocket Server for Direct Connections ---
963
+ function startLocalServer() {
964
+ const localPort = process.env.LOCAL_PORT || 8080;
965
+ const localServer = require('ws').Server;
966
+ const wss = new localServer({ port: localPort });
967
+
968
+ logToFile(`🏠 Starting local WebSocket server on port ${localPort}`);
969
+
970
+ wss.on('connection', (localWs, request) => {
971
+ const clientIp = request.socket.remoteAddress;
972
+ logToFile(`🔗 Direct connection from ${clientIp}`);
973
+
974
+ // Handle direct browser connections
975
+ localWs.on('message', (data) => {
976
+ try {
977
+ const message = JSON.parse(data);
978
+ logToFile(`[LOCAL] Received direct message: ${message.type}`);
979
+
980
+ switch (message.type) {
981
+ case 'ping':
982
+ localWs.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
983
+ break;
984
+
985
+ case 'authenticate':
986
+ // For direct connections, we can implement simpler auth
987
+ localWs.send(JSON.stringify({
988
+ type: 'authenticated',
989
+ agentId: AGENT_ID,
990
+ timestamp: Date.now()
991
+ }));
992
+ break;
993
+
994
+ case 'create_session':
995
+ // Create new terminal session for direct connection
996
+ const sessionId = uuidv4();
997
+ const ptyProcess = pty.spawn(shell, [], {
998
+ name: 'xterm-color',
999
+ cols: message.cols || 120,
1000
+ rows: message.rows || 30,
1001
+ cwd: process.env.HOME,
1002
+ env: process.env
1003
+ });
1004
+
1005
+ // Store session
1006
+ sessions[sessionId] = {
1007
+ pty: ptyProcess,
1008
+ buffer: new CircularBuffer(),
1009
+ lastActivity: Date.now()
1010
+ };
1011
+
1012
+ // Send session output to direct connection
1013
+ ptyProcess.onData((data) => {
1014
+ if (localWs.readyState === WebSocket.OPEN) {
1015
+ localWs.send(JSON.stringify({
1016
+ type: 'output',
1017
+ sessionId,
1018
+ data
1019
+ }));
1020
+ }
1021
+ });
1022
+
1023
+ localWs.send(JSON.stringify({
1024
+ type: 'session_created',
1025
+ sessionId,
1026
+ cols: message.cols || 120,
1027
+ rows: message.rows || 30
1028
+ }));
1029
+
1030
+ logToFile(`[LOCAL] Created direct session: ${sessionId}`);
1031
+ break;
1032
+
1033
+ case 'input':
1034
+ // Handle terminal input for direct connection
1035
+ if (sessions[message.sessionId]) {
1036
+ sessions[message.sessionId].pty.write(message.data);
1037
+ sessions[message.sessionId].lastActivity = Date.now();
1038
+ }
1039
+ break;
1040
+
1041
+ case 'resize':
1042
+ // Handle terminal resize for direct connection
1043
+ if (sessions[message.sessionId]) {
1044
+ sessions[message.sessionId].pty.resize(message.cols, message.rows);
1045
+ }
1046
+ break;
1047
+
1048
+ default:
1049
+ logToFile(`[LOCAL] Unknown message type: ${message.type}`);
1050
+ }
1051
+ } catch (err) {
1052
+ logToFile(`[LOCAL] Error parsing message: ${err.message}`);
1053
+ }
1054
+ });
1055
+
1056
+ localWs.on('close', () => {
1057
+ logToFile(`[LOCAL] Direct connection from ${clientIp} closed`);
1058
+ });
1059
+
1060
+ localWs.on('error', (error) => {
1061
+ logToFile(`[LOCAL] Direct connection error: ${error.message}`);
1062
+ });
1063
+ });
1064
+
1065
+ logToFile(`✅ Local WebSocket server started on port ${localPort}`);
1066
+ return wss;
1067
+ }
1068
+
958
1069
  // --- Start the agent ---
959
1070
  console.log(`[AGENT] Starting Mac Agent with ID: ${AGENT_ID}`);
1071
+
1072
+ // Start local server for direct connections
1073
+ const localServer = startLocalServer();
1074
+
960
1075
  console.log(`[AGENT] Connecting to signaling server at: ${SIGNALING_SERVER_URL}`);
961
1076
  connectToSignalingServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.65",
3
+ "version": "1.5.67",
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": {
@@ -46,7 +46,7 @@ class ShellMirrorDashboard {
46
46
  this.user = authStatus.user;
47
47
  await this.loadDashboardData();
48
48
  this.renderAuthenticatedDashboard();
49
- this.setupWebSocket(); // Setup real-time connection
49
+ this.enableHttpOnlyMode(); // Use HTTP-only mode (no persistent WebSocket)
50
50
  this.startAutoRefresh(); // Start auto-refresh for authenticated users
51
51
  } else {
52
52
  this.renderUnauthenticatedDashboard();
@@ -65,12 +65,12 @@ class ShellMirrorDashboard {
65
65
  clearInterval(this.refreshInterval);
66
66
  }
67
67
 
68
- // Refresh agent data every 10 seconds (reduced from 30s)
68
+ // Refresh agent data every 30 seconds (HTTP polling only)
69
69
  this.refreshInterval = setInterval(async () => {
70
70
  if (this.isAuthenticated && !this.isRefreshing) {
71
71
  await this.refreshDashboardData();
72
72
  }
73
- }, 10000);
73
+ }, 30000);
74
74
  }
75
75
 
76
76
  async refreshDashboardData() {
@@ -170,7 +170,10 @@ class ShellMirrorDashboard {
170
170
  console.log(`[DASHBOARD] 🔌 WebSocket closed: ${event.code} (${reason})`, event.reason);
171
171
 
172
172
  if (event.code === 1008) {
173
- console.error('[DASHBOARD] ❌ Authentication required - WebSocket rejected connection');
173
+ console.error('[DASHBOARD] ❌ Authentication/Policy violation - switching to HTTP-only mode');
174
+ this.updateConnectionStatus('failed');
175
+ this.enableHttpOnlyMode();
176
+ return;
174
177
  } else if (event.code === 1006) {
175
178
  console.error('[DASHBOARD] ❌ Abnormal closure - WebSocket endpoint may not exist');
176
179
  }
@@ -268,23 +268,215 @@ function startConnection() {
268
268
 
269
269
 
270
270
  async function initialize() {
271
- console.log('[CLIENT] 🚀 Initializing WebRTC connection to agent:', AGENT_ID);
271
+ console.log('[CLIENT] 🚀 Initializing connection to agent:', AGENT_ID);
272
272
  console.log('[CLIENT] 📋 Selected agent data:', SELECTED_AGENT);
273
273
 
274
- // Use Heroku WebSocket server for all connections
274
+ // First try direct connection to agent
275
+ const directConnectionSuccess = await tryDirectConnection();
276
+
277
+ if (directConnectionSuccess) {
278
+ console.log('[CLIENT] ✅ Direct connection established - no server needed!');
279
+ return;
280
+ }
281
+
282
+ console.log('[CLIENT] ⚠️ Direct connection failed, falling back to WebRTC signaling...');
283
+ await initializeWebRTCSignaling();
284
+ }
285
+
286
+ async function tryDirectConnection() {
287
+ console.log('[CLIENT] 🔗 Attempting direct connection to agent...');
288
+ updateConnectionStatus('connecting');
289
+
290
+ // Get agent data from API to find local connection details
291
+ try {
292
+ const response = await fetch('/php-backend/api/agents-list.php', {
293
+ credentials: 'include'
294
+ });
295
+
296
+ const data = await response.json();
297
+ if (!data.success || !data.data.agents) {
298
+ console.log('[CLIENT] ❌ Could not get agent list for direct connection');
299
+ return false;
300
+ }
301
+
302
+ const agent = data.data.agents.find(a => a.agentId === AGENT_ID);
303
+ if (!agent || !agent.localPort) {
304
+ console.log('[CLIENT] ❌ Agent not found or no local port information');
305
+ return false;
306
+ }
307
+
308
+ // Try common local IPs for the agent
309
+ // Start with localhost/loopback, then try common private network ranges
310
+ const possibleIPs = [
311
+ 'localhost',
312
+ '127.0.0.1',
313
+ // Common private network ranges
314
+ ...generatePrivateIPCandidates()
315
+ ];
316
+
317
+ for (const ip of possibleIPs) {
318
+ const success = await tryDirectConnectionToIP(ip, agent.localPort);
319
+ if (success) {
320
+ return true;
321
+ }
322
+ }
323
+
324
+ console.log('[CLIENT] ❌ Direct connection failed to all IP candidates');
325
+ updateConnectionStatus('disconnected');
326
+ return false;
327
+
328
+ } catch (error) {
329
+ console.log('[CLIENT] ❌ Error during direct connection attempt:', error);
330
+ return false;
331
+ }
332
+ }
333
+
334
+ async function tryDirectConnectionToIP(ip, port) {
335
+ return new Promise((resolve) => {
336
+ console.log(`[CLIENT] 🔍 Trying direct connection to ${ip}:${port}`);
337
+
338
+ const directWs = new WebSocket(`ws://${ip}:${port}`);
339
+ const timeout = setTimeout(() => {
340
+ console.log(`[CLIENT] ⏰ Connection timeout to ${ip}:${port}`);
341
+ directWs.close();
342
+ resolve(false);
343
+ }, 3000); // 3 second timeout
344
+
345
+ directWs.onopen = () => {
346
+ clearTimeout(timeout);
347
+ console.log(`[CLIENT] ✅ Direct connection established to ${ip}:${port}`);
348
+
349
+ // Set up the direct connection handlers
350
+ setupDirectConnection(directWs);
351
+ resolve(true);
352
+ };
353
+
354
+ directWs.onerror = () => {
355
+ clearTimeout(timeout);
356
+ console.log(`[CLIENT] ❌ Connection failed to ${ip}:${port}`);
357
+ resolve(false);
358
+ };
359
+
360
+ directWs.onclose = () => {
361
+ clearTimeout(timeout);
362
+ resolve(false);
363
+ };
364
+ });
365
+ }
366
+
367
+ function setupDirectConnection(directWs) {
368
+ console.log('[CLIENT] 🔧 Setting up direct connection handlers');
369
+
370
+ // Store the WebSocket for global access
371
+ ws = directWs;
372
+
373
+ // Set up message handlers
374
+ directWs.onmessage = (event) => {
375
+ const data = JSON.parse(event.data);
376
+ console.log(`[CLIENT] 📨 Direct message: ${data.type}`);
377
+
378
+ switch (data.type) {
379
+ case 'pong':
380
+ console.log('[CLIENT] 🏓 Received pong from direct connection');
381
+ break;
382
+
383
+ case 'authenticated':
384
+ console.log('[CLIENT] ✅ Direct authentication successful');
385
+ // Request session creation
386
+ directWs.send(JSON.stringify({
387
+ type: 'create_session',
388
+ sessionId: requestedSessionId,
389
+ cols: term.cols,
390
+ rows: term.rows
391
+ }));
392
+ break;
393
+
394
+ case 'session_created':
395
+ console.log('[CLIENT] ✅ Direct session created:', data.sessionId);
396
+ currentSession = { id: data.sessionId };
397
+ updateSessionDisplay();
398
+ break;
399
+
400
+ case 'output':
401
+ // Handle terminal output
402
+ if (data.sessionId === currentSession?.id) {
403
+ term.write(data.data);
404
+ }
405
+ break;
406
+
407
+ default:
408
+ console.log('[CLIENT] ❓ Unknown direct message type:', data.type);
409
+ }
410
+ };
411
+
412
+ directWs.onclose = () => {
413
+ console.log('[CLIENT] ❌ Direct connection closed');
414
+ updateConnectionStatus('disconnected');
415
+ };
416
+
417
+ directWs.onerror = (error) => {
418
+ console.error('[CLIENT] ❌ Direct connection error:', error);
419
+ };
420
+
421
+ // Set up terminal input handler for direct connection
422
+ term.onData((data) => {
423
+ if (currentSession && directWs.readyState === WebSocket.OPEN) {
424
+ directWs.send(JSON.stringify({
425
+ type: 'input',
426
+ sessionId: currentSession.id,
427
+ data: data
428
+ }));
429
+ }
430
+ });
431
+
432
+ // Send authentication
433
+ directWs.send(JSON.stringify({
434
+ type: 'authenticate',
435
+ agentId: AGENT_ID
436
+ }));
437
+
438
+ updateConnectionStatus('connected');
439
+ }
440
+
441
+ function generatePrivateIPCandidates() {
442
+ // Generate most common private network IP candidates
443
+ const candidates = [];
444
+
445
+ // Most common home router ranges (limit to most popular subnets)
446
+ const commonSubnets = [0, 1, 2, 10, 100];
447
+ for (const subnet of commonSubnets) {
448
+ // Common host IPs: router (1), common DHCP assignments
449
+ const hosts = [1, 2, 10, 100, 101, 150];
450
+ for (const host of hosts) {
451
+ candidates.push(`192.168.${subnet}.${host}`);
452
+ }
453
+ }
454
+
455
+ // Common corporate/enterprise ranges (just the most common ones)
456
+ candidates.push(
457
+ '10.0.0.1', '10.0.0.2', '10.0.0.100',
458
+ '10.0.1.1', '10.0.1.100',
459
+ '172.16.0.1', '172.16.0.100'
460
+ );
461
+
462
+ return candidates;
463
+ }
464
+
465
+ async function initializeWebRTCSignaling() {
466
+ console.log('[CLIENT] 🚀 Initializing WebRTC signaling connection to agent:', AGENT_ID);
467
+
468
+ // Use Heroku WebSocket server for WebRTC signaling only
275
469
  const signalingUrl = 'wss://shell-mirror-30aa5479ceaf.herokuapp.com';
276
- console.log('[CLIENT] 🌐 Using Heroku WebSocket server:', signalingUrl);
470
+ console.log('[CLIENT] 🌐 Using Heroku WebSocket server for signaling:', signalingUrl);
277
471
 
278
472
  ws = new WebSocket(`${signalingUrl}?role=client`);
279
-
473
+
280
474
  ws.onopen = () => {
281
475
  console.log('[CLIENT] ✅ WebSocket connection to signaling server opened.');
282
476
  };
283
-
284
477
  ws.onmessage = async (message) => {
285
478
  const data = JSON.parse(message.data);
286
479
  console.log(`[CLIENT] Received message of type: ${data.type}`);
287
-
288
480
  switch (data.type) {
289
481
  case 'server-hello':
290
482
  CLIENT_ID = data.id;