shell-mirror 1.5.40 → 1.5.42

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,274 @@ 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
+ // Send initial prompt after terminal is ready
150
+ setTimeout(() => {
151
+ // Send a newline to trigger the shell prompt
152
+ terminal.write('\n');
153
+ }, 500);
154
+
155
+ terminal.on('exit', (code) => {
156
+ logToFile(`[SESSION] Terminal process exited for session ${sessionId} with code ${code}`);
157
+ session.status = 'crashed';
158
+ // Notify connected clients
159
+ session.connectedClients.forEach(clientId => {
160
+ this.sendToClient(clientId, {
161
+ type: 'session-ended',
162
+ sessionId: sessionId,
163
+ reason: 'terminal-exit',
164
+ code: code
165
+ });
166
+ });
167
+ });
168
+
169
+ this.sessions[sessionId] = session;
170
+
171
+ // Associate with client if provided
172
+ if (clientId) {
173
+ this.clientSessions[clientId] = sessionId;
174
+ session.connectedClients.push(clientId);
175
+ }
176
+
177
+ logToFile(`[SESSION] ✅ Session created: ${sessionId} (PID: ${terminal.pid})`);
178
+ return sessionId;
179
+ }
180
+
181
+ getSession(sessionId) {
182
+ return this.sessions[sessionId] || null;
183
+ }
184
+
185
+ connectClientToSession(clientId, sessionId) {
186
+ const session = this.sessions[sessionId];
187
+ if (!session) {
188
+ logToFile(`[SESSION] ❌ Cannot connect client ${clientId} - session ${sessionId} not found`);
189
+ return false;
190
+ }
191
+
192
+ // Disconnect client from any existing session
193
+ this.disconnectClient(clientId);
194
+
195
+ // Connect to new session
196
+ this.clientSessions[clientId] = sessionId;
197
+ if (!session.connectedClients.includes(clientId)) {
198
+ session.connectedClients.push(clientId);
199
+ }
200
+ session.lastActivity = Date.now();
201
+
202
+ logToFile(`[SESSION] ✅ Client ${clientId} connected to session ${sessionId}`);
203
+
204
+ // Send buffered output to newly connected client
205
+ const bufferedOutput = session.buffer.getAll();
206
+ if (bufferedOutput) {
207
+ this.sendToClient(clientId, { type: 'output', data: bufferedOutput });
208
+ }
209
+
210
+ return true;
211
+ }
212
+
213
+ disconnectClient(clientId) {
214
+ const sessionId = this.clientSessions[clientId];
215
+ if (sessionId && this.sessions[sessionId]) {
216
+ const session = this.sessions[sessionId];
217
+ session.connectedClients = session.connectedClients.filter(id => id !== clientId);
218
+ logToFile(`[SESSION] Client ${clientId} disconnected from session ${sessionId}`);
219
+ }
220
+ delete this.clientSessions[clientId];
221
+ }
222
+
223
+ getClientSession(clientId) {
224
+ const sessionId = this.clientSessions[clientId];
225
+ return sessionId ? this.sessions[sessionId] : null;
226
+ }
227
+
228
+ getAllSessions() {
229
+ return Object.values(this.sessions).map(session => ({
230
+ id: session.id,
231
+ name: session.name,
232
+ lastActivity: session.lastActivity,
233
+ createdAt: session.createdAt,
234
+ status: session.status,
235
+ connectedClients: session.connectedClients.length
236
+ }));
237
+ }
238
+
239
+ terminateSession(sessionId) {
240
+ const session = this.sessions[sessionId];
241
+ if (!session) return false;
242
+
243
+ logToFile(`[SESSION] Terminating session: ${sessionId}`);
244
+
245
+ // Notify connected clients
246
+ session.connectedClients.forEach(clientId => {
247
+ this.sendToClient(clientId, {
248
+ type: 'session-terminated',
249
+ sessionId: sessionId
250
+ });
251
+ delete this.clientSessions[clientId];
252
+ });
253
+
254
+ // Kill terminal process
255
+ if (session.terminal) {
256
+ session.terminal.kill();
257
+ }
258
+
259
+ delete this.sessions[sessionId];
260
+ logToFile(`[SESSION] ✅ Session terminated: ${sessionId}`);
261
+ return true;
262
+ }
263
+
264
+ sendToClient(clientId, message) {
265
+ // This will be connected to the WebRTC data channel sending logic
266
+ // For now, we'll use a global dataChannel reference
267
+ // In a full implementation, this would use a clientId-to-dataChannel mapping
268
+ if (typeof dataChannel !== 'undefined' && dataChannel && dataChannel.readyState === 'open') {
269
+ try {
270
+ dataChannel.send(JSON.stringify(message));
271
+ } catch (err) {
272
+ logToFile(`[SESSION] Error sending to client ${clientId}: ${err.message}`);
273
+ }
274
+ } else {
275
+ logToFile(`[SESSION] ⚠️ Cannot send to client ${clientId} - data channel not available`);
276
+ }
277
+ }
278
+
279
+ writeToSession(sessionId, data) {
280
+ const session = this.sessions[sessionId];
281
+ if (session && session.terminal) {
282
+ session.terminal.write(data);
283
+ session.lastActivity = Date.now();
284
+ return true;
285
+ }
286
+ return false;
287
+ }
288
+
289
+ resizeSession(sessionId, cols, rows) {
290
+ const session = this.sessions[sessionId];
291
+ if (session && session.terminal) {
292
+ session.terminal.resize(cols, rows);
293
+ session.lastActivity = Date.now();
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+
299
+ cleanupIdleSessions() {
300
+ const now = Date.now();
301
+ Object.keys(this.sessions).forEach(sessionId => {
302
+ const session = this.sessions[sessionId];
303
+ const idleTime = now - session.lastActivity;
304
+
305
+ if (idleTime > this.defaultSessionTimeout && session.connectedClients.length === 0) {
306
+ logToFile(`[SESSION] Auto-cleanup idle session: ${sessionId} (idle for ${Math.floor(idleTime / 60000)} minutes)`);
307
+ this.terminateSession(sessionId);
308
+ }
309
+ });
310
+ }
311
+ }
312
+
313
+ // Initialize session manager
314
+ const sessionManager = new SessionManager();
315
+
316
+ // Cleanup idle sessions every 30 minutes
317
+ setInterval(() => {
318
+ sessionManager.cleanupIdleSessions();
319
+ }, 30 * 60 * 1000);
320
+
56
321
  let ws;
57
322
  let peerConnection;
58
323
  let dataChannel;
59
- let term;
60
324
 
61
325
  const iceServers = [
62
326
  // Google STUN servers (primary)
@@ -95,16 +359,71 @@ function connectToSignalingServer() {
95
359
 
96
360
  switch (data.type) {
97
361
  case 'client-hello':
98
- logToFile(`🔄 Received client-hello from ${data.from}. Initiating WebRTC connection.`);
362
+ logToFile(`🔄 Received client-hello from ${data.from}. Processing session request.`);
99
363
  try {
364
+ let sessionId;
365
+ let isNewSession = false;
366
+ let availableSessions = sessionManager.getAllSessions();
367
+
368
+ // Handle session request from client
369
+ if (data.sessionRequest) {
370
+ if (data.sessionRequest.sessionId) {
371
+ // Connect to existing session
372
+ sessionId = data.sessionRequest.sessionId;
373
+ logToFile(`[SESSION] Client requesting existing session: ${sessionId}`);
374
+ if (!sessionManager.getSession(sessionId)) {
375
+ logToFile(`[SESSION] ⚠️ Requested session ${sessionId} not found, creating new session`);
376
+ sessionId = sessionManager.createSession(data.sessionRequest.sessionName, data.from);
377
+ isNewSession = true;
378
+ }
379
+ } else if (data.sessionRequest.newSession) {
380
+ // Create new session
381
+ sessionId = sessionManager.createSession(data.sessionRequest.sessionName, data.from);
382
+ isNewSession = true;
383
+ logToFile(`[SESSION] Client requesting new session: ${sessionId}`);
384
+ } else {
385
+ // Default: create new session if no specific request
386
+ sessionId = sessionManager.createSession(null, data.from);
387
+ isNewSession = true;
388
+ }
389
+ } else {
390
+ // Backward compatibility: no session request means create default session
391
+ sessionId = sessionManager.createSession(null, data.from);
392
+ isNewSession = true;
393
+ }
394
+
395
+ if (!sessionId) {
396
+ logToFile(`[SESSION] ❌ Failed to create/connect to session`);
397
+ sendMessage({
398
+ type: 'error',
399
+ message: 'Failed to create session - maximum sessions reached',
400
+ to: data.from,
401
+ from: AGENT_ID
402
+ });
403
+ break;
404
+ }
405
+
406
+ // Connect client to session
407
+ sessionManager.connectClientToSession(data.from, sessionId);
408
+
100
409
  await createPeerConnection(data.from);
101
410
  logToFile('📡 PeerConnection created, generating offer...');
102
411
  const offer = await peerConnection.createOffer();
103
412
  logToFile(`📋 Offer created: ${offer.type}`);
104
413
  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');
414
+
415
+ // Send WebRTC offer with session assignment
416
+ sendMessage({
417
+ type: 'offer',
418
+ sdp: offer.sdp,
419
+ to: data.from,
420
+ from: AGENT_ID,
421
+ sessionId: sessionId,
422
+ sessionName: sessionManager.getSession(sessionId).name,
423
+ isNewSession: isNewSession,
424
+ availableSessions: availableSessions
425
+ });
426
+ logToFile('✅ WebRTC offer sent with session assignment');
108
427
 
109
428
  // Force ICE gathering if it hasn't started within 2 seconds
110
429
  logToFile('[AGENT] 🔧 Setting up ICE gathering fallback timer...');
@@ -204,8 +523,7 @@ async function createPeerConnection(clientId) {
204
523
  dataChannel = peerConnection.createDataChannel('terminal', {
205
524
  ordered: true
206
525
  });
207
- setupDataChannel();
208
- setupTerminal();
526
+ setupDataChannel(clientId);
209
527
 
210
528
  peerConnection.ondatachannel = (event) => {
211
529
  logToFile('[AGENT] Additional data channel received (this should not happen)');
@@ -235,11 +553,11 @@ async function createPeerConnection(clientId) {
235
553
  break;
236
554
  case 'failed':
237
555
  logToFile('[AGENT] ❌ ICE connection failed - no viable candidates');
238
- cleanup();
556
+ cleanup(clientId);
239
557
  break;
240
558
  case 'disconnected':
241
559
  logToFile('[AGENT] ⚠️ ICE connection disconnected');
242
- cleanup();
560
+ cleanup(clientId);
243
561
  break;
244
562
  case 'closed':
245
563
  logToFile('[AGENT] 🔐 ICE connection closed');
@@ -299,11 +617,12 @@ async function createPeerConnection(clientId) {
299
617
  };
300
618
  }
301
619
 
302
- function cleanup() {
303
- if (term) {
304
- term.kill();
305
- term = null;
620
+ function cleanup(clientId = null) {
621
+ // Disconnect client from session manager
622
+ if (clientId) {
623
+ sessionManager.disconnectClient(clientId);
306
624
  }
625
+
307
626
  if (dataChannel) {
308
627
  dataChannel.close();
309
628
  dataChannel = null;
@@ -314,7 +633,7 @@ function cleanup() {
314
633
  }
315
634
  }
316
635
 
317
- function setupDataChannel() {
636
+ function setupDataChannel(clientId) {
318
637
  dataChannel.onopen = () => {
319
638
  logToFile('[AGENT] ✅ Data channel is open!');
320
639
  };
@@ -322,11 +641,35 @@ function setupDataChannel() {
322
641
  dataChannel.onmessage = (event) => {
323
642
  try {
324
643
  const message = JSON.parse(event.data);
325
- if (term && message.type === 'input') {
326
- term.write(message.data);
327
- } else if (term && message.type === 'resize') {
328
- logToFile(`[AGENT] Resizing terminal to ${message.cols}x${message.rows}`);
329
- term.resize(message.cols, message.rows);
644
+ const session = sessionManager.getClientSession(clientId);
645
+
646
+ if (!session) {
647
+ logToFile(`[AGENT] ⚠️ No session found for client ${clientId}`);
648
+ return;
649
+ }
650
+
651
+ if (message.type === 'input') {
652
+ sessionManager.writeToSession(session.id, message.data);
653
+ } else if (message.type === 'resize') {
654
+ logToFile(`[AGENT] Resizing session ${session.id} to ${message.cols}x${message.rows}`);
655
+ sessionManager.resizeSession(session.id, message.cols, message.rows);
656
+ } else if (message.type === 'session-switch') {
657
+ // Handle session switching
658
+ logToFile(`[AGENT] Client ${clientId} switching to session ${message.sessionId}`);
659
+ if (sessionManager.connectClientToSession(clientId, message.sessionId)) {
660
+ // Send confirmation and buffered output
661
+ const newSession = sessionManager.getSession(message.sessionId);
662
+ dataChannel.send(JSON.stringify({
663
+ type: 'session-switched',
664
+ sessionId: message.sessionId,
665
+ sessionName: newSession.name
666
+ }));
667
+ } else {
668
+ dataChannel.send(JSON.stringify({
669
+ type: 'error',
670
+ message: `Session ${message.sessionId} not found`
671
+ }));
672
+ }
330
673
  }
331
674
  } catch (err) {
332
675
  logToFile(`[AGENT] Error parsing data channel message: ${err.message}`);
@@ -335,7 +678,7 @@ function setupDataChannel() {
335
678
 
336
679
  dataChannel.onclose = () => {
337
680
  logToFile('[AGENT] Data channel closed.');
338
- cleanup();
681
+ cleanup(clientId);
339
682
  };
340
683
 
341
684
  dataChannel.onerror = (error) => {
@@ -343,50 +686,6 @@ function setupDataChannel() {
343
686
  };
344
687
  }
345
688
 
346
- function setupTerminal() {
347
- logToFile('[AGENT] Spawning new terminal');
348
-
349
- // Use zsh (default on modern macOS) instead of bash for Mac-like experience
350
- const macShell = os.platform() === 'darwin' ? '/bin/zsh' : shell;
351
-
352
- // Create enhanced environment for Mac terminal appearance
353
- const terminalEnv = {
354
- ...process.env,
355
- TERM: 'xterm-256color', // Enable full 256 color support
356
- COLORTERM: 'truecolor', // Enable true color support
357
- LANG: 'en_US.UTF-8', // Proper locale for Mac
358
- LC_ALL: 'en_US.UTF-8', // Full UTF-8 support
359
- SHELL: macShell, // Set proper shell
360
- TERM_PROGRAM: 'Terminal', // Mimic macOS Terminal.app
361
- TERM_PROGRAM_VERSION: '2.12.7' // Recent Terminal.app version
362
- };
363
-
364
- term = pty.spawn(macShell, ['--login'], {
365
- name: 'xterm-256color', // Full color terminal emulation
366
- cols: 120, // Wider default like Mac terminal
367
- rows: 30,
368
- cwd: process.env.HOME,
369
- env: terminalEnv,
370
- encoding: 'utf8' // Ensure UTF-8 encoding
371
- });
372
-
373
- term.on('data', (data) => {
374
- if (dataChannel && dataChannel.readyState === 'open') {
375
- try {
376
- dataChannel.send(JSON.stringify({ type: 'output', data: data }));
377
- } catch (err) {
378
- logToFile(`[AGENT] Error sending terminal output: ${err.message}`);
379
- }
380
- }
381
- });
382
-
383
- term.on('exit', (code) => {
384
- logToFile(`[AGENT] Terminal process exited with code ${code}`);
385
- cleanup();
386
- });
387
-
388
- logToFile(`[AGENT] ✅ Terminal spawned (PID: ${term.pid})`);
389
- }
390
689
 
391
690
  function sendMessage(message) {
392
691
  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.40",
3
+ "version": "1.5.42",
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": {