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.
@@ -1,94 +1,9 @@
1
- === Mac Agent Debug Log Started 2025-08-06T23:17:12.104Z ===
2
- [2025-08-06T23:17:12.119Z] ✅ @koush/wrtc package loaded successfully
3
- [2025-08-06T23:17:12.122Z] 🆔 Agent ID: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
4
- [2025-08-06T23:17:12.122Z] [AGENT] Host is 0.0.0.0, connecting to localhost instead.
5
- [2025-08-06T23:17:12.122Z] 🌐 Signaling server URL: ws://localhost:3000
6
- [2025-08-06T23:17:12.122Z] 🐚 Shell: bash
7
- [2025-08-06T23:17:12.122Z] 🔌 Connecting to signaling server at ws://localhost:3000?role=agent&agentId=agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
8
- [2025-08-06T23:17:12.132Z] Connected to signaling server.
9
- [2025-08-06T23:17:17.684Z] 🔌 Connecting to signaling server at ws://localhost:3000?role=agent&agentId=agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
10
- [2025-08-06T23:17:17.690Z] ✅ Connected to signaling server.
11
- [2025-08-06T23:17:18.729Z] 📨 Received message of type: ping from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
12
- [2025-08-06T23:17:18.730Z] [AGENT] ❓ Unknown message type: ping
13
- [2025-08-06T23:17:19.733Z] 📨 Received message of type: client-hello from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
14
- [2025-08-06T23:17:19.733Z] 🔄 Received client-hello from local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731. Initiating WebRTC connection.
15
- [2025-08-06T23:17:19.734Z] Creating new PeerConnection
16
- [2025-08-06T23:17:19.734Z] 🌐 Configuring ICE servers: stun:stun.l.google.com:19302, stun:stun1.l.google.com:19302, stun:stun.cloudflare.com:3478, stun:stun.services.mozilla.com:3478, turn:openrelay.metered.ca:80, turn:openrelay.metered.ca:443
17
- [2025-08-06T23:17:19.734Z] ⚙️ WebRTC config: {"iceServers":[{"urls":"stun:stun.l.google.com:19302"},{"urls":"stun:stun1.l.google.com:19302"},{"urls":"stun:stun.cloudflare.com:3478"},{"urls":"stun:stun.services.mozilla.com:3478"},{"urls":"turn:openrelay.metered.ca:80","username":"openrelayproject","credential":"openrelayproject"},{"urls":"turn:openrelay.metered.ca:443","username":"openrelayproject","credential":"openrelayproject"}],"iceCandidatePoolSize":10,"iceTransportPolicy":"all","bundlePolicy":"balanced"}
18
- [2025-08-06T23:17:19.748Z] [AGENT] 🔧 Attaching ICE candidate event handler...
19
- [2025-08-06T23:17:19.748Z] [AGENT] Creating data channel...
20
- [2025-08-06T23:17:19.750Z] [AGENT] Spawning new terminal
21
- [2025-08-06T23:17:19.753Z] [AGENT] ✅ Terminal spawned (PID: 70587)
22
- [2025-08-06T23:17:19.753Z] 📡 PeerConnection created, generating offer...
23
- [2025-08-06T23:17:19.755Z] 📋 Offer created: offer
24
- [2025-08-06T23:17:19.756Z] 📤 Sending WebRTC offer to client.
25
- [2025-08-06T23:17:19.756Z] [AGENT] Sending message: offer
26
- [2025-08-06T23:17:19.757Z] ✅ WebRTC offer sent successfully
27
- [2025-08-06T23:17:19.757Z] [AGENT] 🔧 Setting up ICE gathering fallback timer...
28
- [2025-08-06T23:17:19.757Z] [AGENT] 🔍 ICE gathering state changed: gathering
29
- [2025-08-06T23:17:19.758Z] [AGENT] 🔍 ICE gathering in progress...
30
- [2025-08-06T23:17:19.758Z] [AGENT] 🧊 ICE candidate event fired: candidate found
31
- [2025-08-06T23:17:19.758Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:1431643032 1 udp 2122260224 192.168.31.244 61993 typ host generation 0 ufrag fZih network-id 1 network-cost 50","sdpMid":"0","sdpMLineIndex":0}
32
- [2025-08-06T23:17:19.758Z] [AGENT] 📤 Sending ICE candidate to client...
33
- [2025-08-06T23:17:19.758Z] [AGENT] Sending message: candidate
34
- [2025-08-06T23:17:19.759Z] [AGENT] ✅ ICE candidate sent successfully
35
- [2025-08-06T23:17:19.759Z] [AGENT] 🧊 ICE candidate event fired: candidate found
36
- [2025-08-06T23:17:19.759Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:559267639 1 udp 2122202368 ::1 59459 typ host generation 0 ufrag fZih network-id 3","sdpMid":"0","sdpMLineIndex":0}
37
- [2025-08-06T23:17:19.759Z] [AGENT] 📤 Sending ICE candidate to client...
38
- [2025-08-06T23:17:19.759Z] [AGENT] Sending message: candidate
39
- [2025-08-06T23:17:19.759Z] [AGENT] ✅ ICE candidate sent successfully
40
- [2025-08-06T23:17:19.760Z] [AGENT] 🧊 ICE candidate event fired: candidate found
41
- [2025-08-06T23:17:19.760Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:1510613869 1 udp 2122129152 127.0.0.1 52296 typ host generation 0 ufrag fZih network-id 2","sdpMid":"0","sdpMLineIndex":0}
42
- [2025-08-06T23:17:19.760Z] [AGENT] 📤 Sending ICE candidate to client...
43
- [2025-08-06T23:17:19.760Z] [AGENT] Sending message: candidate
44
- [2025-08-06T23:17:19.760Z] [AGENT] ✅ ICE candidate sent successfully
45
- [2025-08-06T23:17:19.785Z] 📨 Received message of type: answer from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
46
- [2025-08-06T23:17:19.785Z] [AGENT] 📥 Received WebRTC answer from client.
47
- [2025-08-06T23:17:19.786Z] 📨 Received message of type: candidate from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
48
- [2025-08-06T23:17:19.786Z] [AGENT] 🧊 Received ICE candidate from client.
49
- [2025-08-06T23:17:19.787Z] [AGENT] 📊 ICE connection state changed: new
50
- [2025-08-06T23:17:19.787Z] [AGENT] 📊 ICE gathering state: gathering
51
- [2025-08-06T23:17:19.787Z] [AGENT] 🆕 ICE connection starting...
52
- [2025-08-06T23:17:19.787Z] [AGENT] 📡 Connection state changed: new
53
- [2025-08-06T23:17:19.787Z] [AGENT] 🆕 Connection starting...
54
- [2025-08-06T23:17:19.787Z] [AGENT] ✅ WebRTC answer processed successfully
55
- [2025-08-06T23:17:19.787Z] [AGENT] ✅ ICE candidate added successfully
56
- [2025-08-06T23:17:19.791Z] [AGENT] 🧊 ICE candidate event fired: candidate found
57
- [2025-08-06T23:17:19.792Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:2581077003 1 udp 1686052607 46.34.249.124 1753 typ srflx raddr 192.168.31.244 rport 61993 generation 0 ufrag fZih network-id 1 network-cost 50","sdpMid":"0","sdpMLineIndex":0}
58
- [2025-08-06T23:17:19.792Z] [AGENT] 📤 Sending ICE candidate to client...
59
- [2025-08-06T23:17:19.792Z] [AGENT] Sending message: candidate
60
- [2025-08-06T23:17:19.792Z] [AGENT] ✅ ICE candidate sent successfully
61
- [2025-08-06T23:17:19.797Z] 📨 Received message of type: candidate from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
62
- [2025-08-06T23:17:19.797Z] [AGENT] 🧊 Received ICE candidate from client.
63
- [2025-08-06T23:17:19.797Z] [AGENT] ✅ ICE candidate added successfully
64
- [2025-08-06T23:17:19.847Z] [AGENT] 🧊 ICE candidate event fired: candidate found
65
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:467066728 1 tcp 1518280447 192.168.31.244 57774 typ host tcptype passive generation 0 ufrag fZih network-id 1 network-cost 50","sdpMid":"0","sdpMLineIndex":0}
66
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 Sending ICE candidate to client...
67
- [2025-08-06T23:17:19.847Z] [AGENT] Sending message: candidate
68
- [2025-08-06T23:17:19.847Z] [AGENT] ✅ ICE candidate sent successfully
69
- [2025-08-06T23:17:19.847Z] [AGENT] 🧊 ICE candidate event fired: candidate found
70
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:1876313031 1 tcp 1518222591 ::1 57775 typ host tcptype passive generation 0 ufrag fZih network-id 3","sdpMid":"0","sdpMLineIndex":0}
71
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 Sending ICE candidate to client...
72
- [2025-08-06T23:17:19.847Z] [AGENT] Sending message: candidate
73
- [2025-08-06T23:17:19.847Z] [AGENT] ✅ ICE candidate sent successfully
74
- [2025-08-06T23:17:19.847Z] [AGENT] 🧊 ICE candidate event fired: candidate found
75
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:344579997 1 tcp 1518149375 127.0.0.1 57776 typ host tcptype passive generation 0 ufrag fZih network-id 2","sdpMid":"0","sdpMLineIndex":0}
76
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 Sending ICE candidate to client...
77
- [2025-08-06T23:17:19.847Z] [AGENT] Sending message: candidate
78
- [2025-08-06T23:17:19.848Z] [AGENT] ✅ ICE candidate sent successfully
79
- [2025-08-06T23:17:19.953Z] [AGENT] 🔍 ICE gathering state changed: complete
80
- [2025-08-06T23:17:19.953Z] [AGENT] ✅ ICE gathering completed
81
- [2025-08-06T23:17:19.953Z] [AGENT] 🧊 ICE candidate event fired: gathering complete
82
- [2025-08-06T23:17:19.953Z] [AGENT] 🏁 ICE candidate gathering complete.
83
- [2025-08-06T23:17:19.955Z] [AGENT] 📊 ICE connection state changed: connected
84
- [2025-08-06T23:17:19.955Z] [AGENT] 📊 ICE gathering state: complete
85
- [2025-08-06T23:17:19.955Z] [AGENT] ✅ WebRTC connection established!
86
- [2025-08-06T23:17:19.956Z] [AGENT] 📡 Connection state changed: connected
87
- [2025-08-06T23:17:19.956Z] [AGENT] ✅ Peer connection fully established!
88
- [2025-08-06T23:17:19.957Z] [AGENT] ✅ Data channel is open!
89
- [2025-08-06T23:17:21.758Z] [AGENT] ✅ ICE gathering is active: complete
90
- [2025-08-06T23:17:35.042Z] [AGENT] 📊 ICE connection state changed: completed
91
- [2025-08-06T23:17:35.043Z] [AGENT] 📊 ICE gathering state: complete
92
- [2025-08-06T23:17:35.044Z] [AGENT] ✅ ICE connection completed successfully!
93
- [2025-08-06T23:17:35.044Z] [AGENT] 📡 Connection state changed: connected
94
- [2025-08-06T23:17:35.044Z] [AGENT] ✅ Peer connection fully established!
1
+ === Mac Agent Debug Log Started 2025-08-17T10:31:27.217Z ===
2
+ [2025-08-17T10:31:27.469Z] ✅ @koush/wrtc package loaded successfully
3
+ [2025-08-17T10:31:27.471Z] 🆔 Agent ID: agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
4
+ [2025-08-17T10:31:27.471Z] [AGENT] Host is 0.0.0.0, connecting to localhost instead.
5
+ [2025-08-17T10:31:27.471Z] 🌐 Using local WebSocket URL: ws://localhost:8080
6
+ [2025-08-17T10:31:27.471Z] 🐚 Shell: bash
7
+ [2025-08-17T10:31:27.471Z] 🔌 Connecting to signaling server at ws://localhost:8080?role=agent&agentId=agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
8
+ [2025-08-17T10:31:32.485Z] 🔌 Connecting to signaling server at ws://localhost:8080?role=agent&agentId=agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
9
+ [2025-08-17T10:31:37.492Z] 🔌 Connecting to signaling server at ws://localhost:8080?role=agent&agentId=agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
@@ -53,10 +53,268 @@ if (process.env.WEBSOCKET_URL) {
53
53
  const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
54
54
  logToFile(`🐚 Shell: ${shell}`);
55
55
 
56
+ // Circular buffer for session output persistence
57
+ class CircularBuffer {
58
+ constructor(size = 10000) {
59
+ this.size = size;
60
+ this.buffer = [];
61
+ this.index = 0;
62
+ this.full = false;
63
+ }
64
+
65
+ add(data) {
66
+ this.buffer[this.index] = data;
67
+ this.index = (this.index + 1) % this.size;
68
+ if (this.index === 0) this.full = true;
69
+ }
70
+
71
+ getAll() {
72
+ if (!this.full) {
73
+ return this.buffer.slice(0, this.index).join('');
74
+ }
75
+ return this.buffer.slice(this.index).concat(this.buffer.slice(0, this.index)).join('');
76
+ }
77
+
78
+ clear() {
79
+ this.buffer = [];
80
+ this.index = 0;
81
+ this.full = false;
82
+ }
83
+ }
84
+
85
+ // Session Manager for multiple persistent terminal sessions
86
+ class SessionManager {
87
+ constructor() {
88
+ this.sessions = {};
89
+ this.maxSessions = 10;
90
+ this.defaultSessionTimeout = 24 * 60 * 60 * 1000; // 24 hours
91
+ this.clientSessions = {}; // Maps clientId to sessionId
92
+ }
93
+
94
+ createSession(sessionName = null, clientId = null) {
95
+ const sessionId = `ses_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
96
+ const name = sessionName || `Session ${Object.keys(this.sessions).length + 1}`;
97
+
98
+ logToFile(`[SESSION] Creating new session: ${sessionId} (${name})`);
99
+
100
+ // Check session limit
101
+ if (Object.keys(this.sessions).length >= this.maxSessions) {
102
+ logToFile(`[SESSION] ❌ Maximum sessions (${this.maxSessions}) reached`);
103
+ return null;
104
+ }
105
+
106
+ const macShell = os.platform() === 'darwin' ? '/bin/zsh' : shell;
107
+ const terminalEnv = {
108
+ ...process.env,
109
+ TERM: 'xterm-256color',
110
+ COLORTERM: 'truecolor',
111
+ LANG: 'en_US.UTF-8',
112
+ LC_ALL: 'en_US.UTF-8',
113
+ SHELL: macShell,
114
+ TERM_PROGRAM: 'Terminal',
115
+ TERM_PROGRAM_VERSION: '2.12.7'
116
+ };
117
+
118
+ const terminal = pty.spawn(macShell, ['--login'], {
119
+ name: 'xterm-256color',
120
+ cols: 120,
121
+ rows: 30,
122
+ cwd: process.env.HOME,
123
+ env: terminalEnv,
124
+ encoding: 'utf8'
125
+ });
126
+
127
+ const session = {
128
+ id: sessionId,
129
+ name: name,
130
+ terminal: terminal,
131
+ buffer: new CircularBuffer(10000),
132
+ connectedClients: [],
133
+ createdAt: Date.now(),
134
+ lastActivity: Date.now(),
135
+ status: 'active'
136
+ };
137
+
138
+ // Set up terminal event handlers
139
+ terminal.on('data', (data) => {
140
+ session.buffer.add(data);
141
+ session.lastActivity = Date.now();
142
+
143
+ // Send to all connected clients for this session
144
+ session.connectedClients.forEach(clientId => {
145
+ this.sendToClient(clientId, { type: 'output', data: data });
146
+ });
147
+ });
148
+
149
+ terminal.on('exit', (code) => {
150
+ logToFile(`[SESSION] Terminal process exited for session ${sessionId} with code ${code}`);
151
+ session.status = 'crashed';
152
+ // Notify connected clients
153
+ session.connectedClients.forEach(clientId => {
154
+ this.sendToClient(clientId, {
155
+ type: 'session-ended',
156
+ sessionId: sessionId,
157
+ reason: 'terminal-exit',
158
+ code: code
159
+ });
160
+ });
161
+ });
162
+
163
+ this.sessions[sessionId] = session;
164
+
165
+ // Associate with client if provided
166
+ if (clientId) {
167
+ this.clientSessions[clientId] = sessionId;
168
+ session.connectedClients.push(clientId);
169
+ }
170
+
171
+ logToFile(`[SESSION] ✅ Session created: ${sessionId} (PID: ${terminal.pid})`);
172
+ return sessionId;
173
+ }
174
+
175
+ getSession(sessionId) {
176
+ return this.sessions[sessionId] || null;
177
+ }
178
+
179
+ connectClientToSession(clientId, sessionId) {
180
+ const session = this.sessions[sessionId];
181
+ if (!session) {
182
+ logToFile(`[SESSION] ❌ Cannot connect client ${clientId} - session ${sessionId} not found`);
183
+ return false;
184
+ }
185
+
186
+ // Disconnect client from any existing session
187
+ this.disconnectClient(clientId);
188
+
189
+ // Connect to new session
190
+ this.clientSessions[clientId] = sessionId;
191
+ if (!session.connectedClients.includes(clientId)) {
192
+ session.connectedClients.push(clientId);
193
+ }
194
+ session.lastActivity = Date.now();
195
+
196
+ logToFile(`[SESSION] ✅ Client ${clientId} connected to session ${sessionId}`);
197
+
198
+ // Send buffered output to newly connected client
199
+ const bufferedOutput = session.buffer.getAll();
200
+ if (bufferedOutput) {
201
+ this.sendToClient(clientId, { type: 'output', data: bufferedOutput });
202
+ }
203
+
204
+ return true;
205
+ }
206
+
207
+ disconnectClient(clientId) {
208
+ const sessionId = this.clientSessions[clientId];
209
+ if (sessionId && this.sessions[sessionId]) {
210
+ const session = this.sessions[sessionId];
211
+ session.connectedClients = session.connectedClients.filter(id => id !== clientId);
212
+ logToFile(`[SESSION] Client ${clientId} disconnected from session ${sessionId}`);
213
+ }
214
+ delete this.clientSessions[clientId];
215
+ }
216
+
217
+ getClientSession(clientId) {
218
+ const sessionId = this.clientSessions[clientId];
219
+ return sessionId ? this.sessions[sessionId] : null;
220
+ }
221
+
222
+ getAllSessions() {
223
+ return Object.values(this.sessions).map(session => ({
224
+ id: session.id,
225
+ name: session.name,
226
+ lastActivity: session.lastActivity,
227
+ createdAt: session.createdAt,
228
+ status: session.status,
229
+ connectedClients: session.connectedClients.length
230
+ }));
231
+ }
232
+
233
+ terminateSession(sessionId) {
234
+ const session = this.sessions[sessionId];
235
+ if (!session) return false;
236
+
237
+ logToFile(`[SESSION] Terminating session: ${sessionId}`);
238
+
239
+ // Notify connected clients
240
+ session.connectedClients.forEach(clientId => {
241
+ this.sendToClient(clientId, {
242
+ type: 'session-terminated',
243
+ sessionId: sessionId
244
+ });
245
+ delete this.clientSessions[clientId];
246
+ });
247
+
248
+ // Kill terminal process
249
+ if (session.terminal) {
250
+ session.terminal.kill();
251
+ }
252
+
253
+ delete this.sessions[sessionId];
254
+ logToFile(`[SESSION] ✅ Session terminated: ${sessionId}`);
255
+ return true;
256
+ }
257
+
258
+ sendToClient(clientId, message) {
259
+ // This will be connected to the WebRTC data channel sending logic
260
+ // For now, we'll use a global dataChannel reference
261
+ // In a full implementation, this would use a clientId-to-dataChannel mapping
262
+ if (typeof dataChannel !== 'undefined' && dataChannel && dataChannel.readyState === 'open') {
263
+ try {
264
+ dataChannel.send(JSON.stringify(message));
265
+ } catch (err) {
266
+ logToFile(`[SESSION] Error sending to client ${clientId}: ${err.message}`);
267
+ }
268
+ } else {
269
+ logToFile(`[SESSION] ⚠️ Cannot send to client ${clientId} - data channel not available`);
270
+ }
271
+ }
272
+
273
+ writeToSession(sessionId, data) {
274
+ const session = this.sessions[sessionId];
275
+ if (session && session.terminal) {
276
+ session.terminal.write(data);
277
+ session.lastActivity = Date.now();
278
+ return true;
279
+ }
280
+ return false;
281
+ }
282
+
283
+ resizeSession(sessionId, cols, rows) {
284
+ const session = this.sessions[sessionId];
285
+ if (session && session.terminal) {
286
+ session.terminal.resize(cols, rows);
287
+ session.lastActivity = Date.now();
288
+ return true;
289
+ }
290
+ return false;
291
+ }
292
+
293
+ cleanupIdleSessions() {
294
+ const now = Date.now();
295
+ Object.keys(this.sessions).forEach(sessionId => {
296
+ const session = this.sessions[sessionId];
297
+ const idleTime = now - session.lastActivity;
298
+
299
+ if (idleTime > this.defaultSessionTimeout && session.connectedClients.length === 0) {
300
+ logToFile(`[SESSION] Auto-cleanup idle session: ${sessionId} (idle for ${Math.floor(idleTime / 60000)} minutes)`);
301
+ this.terminateSession(sessionId);
302
+ }
303
+ });
304
+ }
305
+ }
306
+
307
+ // Initialize session manager
308
+ const sessionManager = new SessionManager();
309
+
310
+ // Cleanup idle sessions every 30 minutes
311
+ setInterval(() => {
312
+ sessionManager.cleanupIdleSessions();
313
+ }, 30 * 60 * 1000);
314
+
56
315
  let ws;
57
316
  let peerConnection;
58
317
  let dataChannel;
59
- let term;
60
318
 
61
319
  const iceServers = [
62
320
  // Google STUN servers (primary)
@@ -95,16 +353,71 @@ function connectToSignalingServer() {
95
353
 
96
354
  switch (data.type) {
97
355
  case 'client-hello':
98
- logToFile(`🔄 Received client-hello from ${data.from}. Initiating WebRTC connection.`);
356
+ logToFile(`🔄 Received client-hello from ${data.from}. Processing session request.`);
99
357
  try {
358
+ let sessionId;
359
+ let isNewSession = false;
360
+ let availableSessions = sessionManager.getAllSessions();
361
+
362
+ // Handle session request from client
363
+ if (data.sessionRequest) {
364
+ if (data.sessionRequest.sessionId) {
365
+ // Connect to existing session
366
+ sessionId = data.sessionRequest.sessionId;
367
+ logToFile(`[SESSION] Client requesting existing session: ${sessionId}`);
368
+ if (!sessionManager.getSession(sessionId)) {
369
+ logToFile(`[SESSION] ⚠️ Requested session ${sessionId} not found, creating new session`);
370
+ sessionId = sessionManager.createSession(data.sessionRequest.sessionName, data.from);
371
+ isNewSession = true;
372
+ }
373
+ } else if (data.sessionRequest.newSession) {
374
+ // Create new session
375
+ sessionId = sessionManager.createSession(data.sessionRequest.sessionName, data.from);
376
+ isNewSession = true;
377
+ logToFile(`[SESSION] Client requesting new session: ${sessionId}`);
378
+ } else {
379
+ // Default: create new session if no specific request
380
+ sessionId = sessionManager.createSession(null, data.from);
381
+ isNewSession = true;
382
+ }
383
+ } else {
384
+ // Backward compatibility: no session request means create default session
385
+ sessionId = sessionManager.createSession(null, data.from);
386
+ isNewSession = true;
387
+ }
388
+
389
+ if (!sessionId) {
390
+ logToFile(`[SESSION] ❌ Failed to create/connect to session`);
391
+ sendMessage({
392
+ type: 'error',
393
+ message: 'Failed to create session - maximum sessions reached',
394
+ to: data.from,
395
+ from: AGENT_ID
396
+ });
397
+ break;
398
+ }
399
+
400
+ // Connect client to session
401
+ sessionManager.connectClientToSession(data.from, sessionId);
402
+
100
403
  await createPeerConnection(data.from);
101
404
  logToFile('📡 PeerConnection created, generating offer...');
102
405
  const offer = await peerConnection.createOffer();
103
406
  logToFile(`📋 Offer created: ${offer.type}`);
104
407
  await peerConnection.setLocalDescription(offer);
105
- logToFile('📤 Sending WebRTC offer to client.');
106
- sendMessage({ type: 'offer', sdp: offer.sdp, to: data.from, from: AGENT_ID });
107
- logToFile('✅ WebRTC offer sent successfully');
408
+
409
+ // Send WebRTC offer with session assignment
410
+ sendMessage({
411
+ type: 'offer',
412
+ sdp: offer.sdp,
413
+ to: data.from,
414
+ from: AGENT_ID,
415
+ sessionId: sessionId,
416
+ sessionName: sessionManager.getSession(sessionId).name,
417
+ isNewSession: isNewSession,
418
+ availableSessions: availableSessions
419
+ });
420
+ logToFile('✅ WebRTC offer sent with session assignment');
108
421
 
109
422
  // Force ICE gathering if it hasn't started within 2 seconds
110
423
  logToFile('[AGENT] 🔧 Setting up ICE gathering fallback timer...');
@@ -204,14 +517,18 @@ async function createPeerConnection(clientId) {
204
517
  dataChannel = peerConnection.createDataChannel('terminal', {
205
518
  ordered: true
206
519
  });
207
- setupDataChannel();
208
- setupTerminal();
520
+ setupDataChannel(clientId);
209
521
 
210
522
  peerConnection.ondatachannel = (event) => {
211
523
  logToFile('[AGENT] Additional data channel received (this should not happen)');
212
524
  };
213
525
 
214
526
  peerConnection.oniceconnectionstatechange = () => {
527
+ if (!peerConnection) {
528
+ logToFile('[AGENT] ⚠️ ICE connection state change after peerConnection was closed');
529
+ return;
530
+ }
531
+
215
532
  logToFile(`[AGENT] 📊 ICE connection state changed: ${peerConnection.iceConnectionState}`);
216
533
  logToFile(`[AGENT] 📊 ICE gathering state: ${peerConnection.iceGatheringState}`);
217
534
 
@@ -230,11 +547,11 @@ async function createPeerConnection(clientId) {
230
547
  break;
231
548
  case 'failed':
232
549
  logToFile('[AGENT] ❌ ICE connection failed - no viable candidates');
233
- cleanup();
550
+ cleanup(clientId);
234
551
  break;
235
552
  case 'disconnected':
236
553
  logToFile('[AGENT] ⚠️ ICE connection disconnected');
237
- cleanup();
554
+ cleanup(clientId);
238
555
  break;
239
556
  case 'closed':
240
557
  logToFile('[AGENT] 🔐 ICE connection closed');
@@ -243,6 +560,11 @@ async function createPeerConnection(clientId) {
243
560
  };
244
561
 
245
562
  peerConnection.onconnectionstatechange = () => {
563
+ if (!peerConnection) {
564
+ logToFile('[AGENT] ⚠️ Connection state change after peerConnection was closed');
565
+ return;
566
+ }
567
+
246
568
  logToFile(`[AGENT] 📡 Connection state changed: ${peerConnection.connectionState}`);
247
569
 
248
570
  switch (peerConnection.connectionState) {
@@ -268,6 +590,11 @@ async function createPeerConnection(clientId) {
268
590
  };
269
591
 
270
592
  peerConnection.onicegatheringstatechange = () => {
593
+ if (!peerConnection) {
594
+ logToFile('[AGENT] ⚠️ ICE gathering state change after peerConnection was closed');
595
+ return;
596
+ }
597
+
271
598
  logToFile(`[AGENT] 🔍 ICE gathering state changed: ${peerConnection.iceGatheringState}`);
272
599
 
273
600
  switch (peerConnection.iceGatheringState) {
@@ -284,11 +611,12 @@ async function createPeerConnection(clientId) {
284
611
  };
285
612
  }
286
613
 
287
- function cleanup() {
288
- if (term) {
289
- term.kill();
290
- term = null;
614
+ function cleanup(clientId = null) {
615
+ // Disconnect client from session manager
616
+ if (clientId) {
617
+ sessionManager.disconnectClient(clientId);
291
618
  }
619
+
292
620
  if (dataChannel) {
293
621
  dataChannel.close();
294
622
  dataChannel = null;
@@ -299,7 +627,7 @@ function cleanup() {
299
627
  }
300
628
  }
301
629
 
302
- function setupDataChannel() {
630
+ function setupDataChannel(clientId) {
303
631
  dataChannel.onopen = () => {
304
632
  logToFile('[AGENT] ✅ Data channel is open!');
305
633
  };
@@ -307,11 +635,35 @@ function setupDataChannel() {
307
635
  dataChannel.onmessage = (event) => {
308
636
  try {
309
637
  const message = JSON.parse(event.data);
310
- if (term && message.type === 'input') {
311
- term.write(message.data);
312
- } else if (term && message.type === 'resize') {
313
- logToFile(`[AGENT] Resizing terminal to ${message.cols}x${message.rows}`);
314
- term.resize(message.cols, message.rows);
638
+ const session = sessionManager.getClientSession(clientId);
639
+
640
+ if (!session) {
641
+ logToFile(`[AGENT] ⚠️ No session found for client ${clientId}`);
642
+ return;
643
+ }
644
+
645
+ if (message.type === 'input') {
646
+ sessionManager.writeToSession(session.id, message.data);
647
+ } else if (message.type === 'resize') {
648
+ logToFile(`[AGENT] Resizing session ${session.id} to ${message.cols}x${message.rows}`);
649
+ sessionManager.resizeSession(session.id, message.cols, message.rows);
650
+ } else if (message.type === 'session-switch') {
651
+ // Handle session switching
652
+ logToFile(`[AGENT] Client ${clientId} switching to session ${message.sessionId}`);
653
+ if (sessionManager.connectClientToSession(clientId, message.sessionId)) {
654
+ // Send confirmation and buffered output
655
+ const newSession = sessionManager.getSession(message.sessionId);
656
+ dataChannel.send(JSON.stringify({
657
+ type: 'session-switched',
658
+ sessionId: message.sessionId,
659
+ sessionName: newSession.name
660
+ }));
661
+ } else {
662
+ dataChannel.send(JSON.stringify({
663
+ type: 'error',
664
+ message: `Session ${message.sessionId} not found`
665
+ }));
666
+ }
315
667
  }
316
668
  } catch (err) {
317
669
  logToFile(`[AGENT] Error parsing data channel message: ${err.message}`);
@@ -320,7 +672,7 @@ function setupDataChannel() {
320
672
 
321
673
  dataChannel.onclose = () => {
322
674
  logToFile('[AGENT] Data channel closed.');
323
- cleanup();
675
+ cleanup(clientId);
324
676
  };
325
677
 
326
678
  dataChannel.onerror = (error) => {
@@ -328,50 +680,6 @@ function setupDataChannel() {
328
680
  };
329
681
  }
330
682
 
331
- function setupTerminal() {
332
- logToFile('[AGENT] Spawning new terminal');
333
-
334
- // Use zsh (default on modern macOS) instead of bash for Mac-like experience
335
- const macShell = os.platform() === 'darwin' ? '/bin/zsh' : shell;
336
-
337
- // Create enhanced environment for Mac terminal appearance
338
- const terminalEnv = {
339
- ...process.env,
340
- TERM: 'xterm-256color', // Enable full 256 color support
341
- COLORTERM: 'truecolor', // Enable true color support
342
- LANG: 'en_US.UTF-8', // Proper locale for Mac
343
- LC_ALL: 'en_US.UTF-8', // Full UTF-8 support
344
- SHELL: macShell, // Set proper shell
345
- TERM_PROGRAM: 'Terminal', // Mimic macOS Terminal.app
346
- TERM_PROGRAM_VERSION: '2.12.7' // Recent Terminal.app version
347
- };
348
-
349
- term = pty.spawn(macShell, ['--login'], {
350
- name: 'xterm-256color', // Full color terminal emulation
351
- cols: 120, // Wider default like Mac terminal
352
- rows: 30,
353
- cwd: process.env.HOME,
354
- env: terminalEnv,
355
- encoding: 'utf8' // Ensure UTF-8 encoding
356
- });
357
-
358
- term.on('data', (data) => {
359
- if (dataChannel && dataChannel.readyState === 'open') {
360
- try {
361
- dataChannel.send(JSON.stringify({ type: 'output', data: data }));
362
- } catch (err) {
363
- logToFile(`[AGENT] Error sending terminal output: ${err.message}`);
364
- }
365
- }
366
- });
367
-
368
- term.on('exit', (code) => {
369
- logToFile(`[AGENT] Terminal process exited with code ${code}`);
370
- cleanup();
371
- });
372
-
373
- logToFile(`[AGENT] ✅ Terminal spawned (PID: ${term.pid})`);
374
- }
375
683
 
376
684
  function sendMessage(message) {
377
685
  if (ws && ws.readyState === WebSocket.OPEN) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.39",
3
+ "version": "1.5.41",
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": {