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
|
@@ -1,94 +1,9 @@
|
|
|
1
|
-
=== Mac Agent Debug Log Started 2025-08-
|
|
2
|
-
[2025-08-
|
|
3
|
-
[2025-08-
|
|
4
|
-
[2025-08-
|
|
5
|
-
[2025-08-
|
|
6
|
-
[2025-08-
|
|
7
|
-
[2025-08-
|
|
8
|
-
[2025-08-
|
|
9
|
-
[2025-08-
|
|
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
|
package/mac-agent/agent.js
CHANGED
|
@@ -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}.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
logToFile(`[AGENT]
|
|
314
|
-
|
|
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