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.
- package/mac-agent/agent-debug.log +9 -94
- package/mac-agent/agent.js +363 -64
- package/package.json +1 -1
- package/public/app/dashboard.css +172 -0
- package/public/app/dashboard.js +159 -6
- package/public/app/terminal.html +118 -1
- package/public/app/terminal.js +212 -4
|
@@ -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,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}.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
logToFile(`[AGENT]
|
|
329
|
-
|
|
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