termify-agent 1.0.41 → 1.0.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/dist/agent.js CHANGED
@@ -12,16 +12,21 @@ import { WireGuardManager } from './network/wireguard-manager.js';
12
12
  import { NatTraversal } from './network/nat-traversal.js';
13
13
  import { KeyRotation } from './network/key-rotation.js';
14
14
  import { getDaemonClient } from './daemon-client.js';
15
- import { getConfig, isConfigured } from './config.js';
15
+ import { getConfig, getServers, isConfigured } from './config.js';
16
16
  import { logger } from './utils/logger.js';
17
17
  /**
18
- * Main agent class that coordinates WebSocket connection and PTY/SSH management
18
+ * Main agent class that coordinates WebSocket connections and PTY/SSH management.
19
+ * Supports multiple simultaneous server connections.
19
20
  */
20
21
  export class Agent extends EventEmitter {
21
- wsClient;
22
+ /** Map of serverUrl -> WSClient for multi-server support */
23
+ wsClients = new Map();
24
+ /** Map of serverUrl -> TunnelManager (tunnels are per-server) */
25
+ tunnelManagers = new Map();
26
+ /** Map of terminalId -> serverUrl (routes terminal I/O to the server that created it) */
27
+ terminalServerMap = new Map();
22
28
  ptyManager;
23
29
  sshManager;
24
- tunnelManager;
25
30
  statsManager;
26
31
  portScanner;
27
32
  daemonClient;
@@ -31,26 +36,28 @@ export class Agent extends EventEmitter {
31
36
  isRunning = false;
32
37
  agentId = null;
33
38
  agentName = null;
39
+ /** Track which servers have connected (for events) */
40
+ connectedServers = new Set();
41
+ primaryServerUrl = null;
34
42
  constructor() {
35
43
  super();
36
- this.wsClient = new WSClient();
37
44
  this.ptyManager = new PTYManager();
38
45
  this.sshManager = new SSHManager();
39
- this.tunnelManager = new TunnelManager(this.wsClient);
40
- this.statsManager = new StatsManager(5); // Collect stats every 5 seconds
46
+ this.statsManager = new StatsManager(5);
41
47
  this.portScanner = new PortScanner();
42
48
  this.daemonClient = getDaemonClient();
43
49
  this.wgManager = new WireGuardManager();
44
50
  this.natTraversal = new NatTraversal();
45
51
  this.keyRotation = new KeyRotation();
46
- // Key rotation: timer fires -> rotate key -> propose to server
52
+ // Key rotation: timer fires -> rotate key -> propose to primary server
47
53
  this.keyRotation.on('rotate-needed', async () => {
48
54
  if (!this.wgManager.getStatus().available)
49
55
  return;
50
56
  try {
51
57
  const result = await this.wgManager.rotateKey();
52
58
  const version = this.keyRotation.proposeRotation();
53
- this.wsClient.sendWgRotatePropose(result.publicKey, version);
59
+ const primaryWs = this.getPrimaryWsClient();
60
+ primaryWs?.sendWgRotatePropose(result.publicKey, version);
54
61
  logger.info(`Key rotation proposed: v${version}`);
55
62
  }
56
63
  catch (err) {
@@ -61,39 +68,152 @@ export class Agent extends EventEmitter {
61
68
  this.wgManager.on('ready', () => {
62
69
  logger.info('WireGuard interface ready');
63
70
  this.reportWgStatus();
64
- // Start periodic key rotation once the interface is up
65
71
  this.keyRotation.start();
66
72
  });
67
73
  this.wgManager.on('error', (err) => {
68
74
  logger.error('WireGuard error:', err.message);
69
75
  this.reportWgStatus();
70
76
  });
71
- // Pass wsClient to PTYManager for git updates
72
- this.ptyManager.setWSClient(this.wsClient);
77
+ // Pass WSClient shim to PTYManager for per-terminal git/MCP routing
78
+ this.ptyManager.setWSClient(this.createPtyWsShim());
73
79
  // Pass daemon client to PTYManager for process monitoring
74
80
  this.ptyManager.setDaemonClient(this.daemonClient);
75
- // Start periodic cleanup of ephemeral terminals (chat-prewarm-*, chat-exec-*)
81
+ // Start periodic cleanup of ephemeral terminals
76
82
  this.ptyManager.startEphemeralCleanup();
77
- this.setupHandlers();
83
+ this.setupSharedHandlers();
78
84
  }
79
85
  /**
80
- * Setup event handlers between WebSocket client and PTY/SSH managers
86
+ * Create a WSClient-like shim that routes per-terminal calls to the correct server
81
87
  */
82
- setupHandlers() {
83
- // WebSocket -> PTY: Handle local terminal requests
84
- this.wsClient.on('terminalCreate', (terminalId, cols, rows, options) => {
85
- // If terminal already exists (e.g. from reconnect), just report RUNNING
88
+ createPtyWsShim() {
89
+ const self = this;
90
+ return {
91
+ sendGitBranch(terminalId, branch, isGitRepo) {
92
+ self.getWsClientForTerminal(terminalId)?.sendGitBranch(terminalId, branch, isGitRepo);
93
+ },
94
+ sendGitBranches(terminalId, branches) {
95
+ self.getWsClientForTerminal(terminalId)?.sendGitBranches(terminalId, branches);
96
+ },
97
+ sendGitLog(terminalId, commits) {
98
+ self.getWsClientForTerminal(terminalId)?.sendGitLog(terminalId, commits);
99
+ },
100
+ sendGitStatus(terminalId, status) {
101
+ self.getWsClientForTerminal(terminalId)?.sendGitStatus(terminalId, status);
102
+ },
103
+ sendGitOperationResult(terminalId, operation, success, error) {
104
+ self.getWsClientForTerminal(terminalId)?.sendGitOperationResult(terminalId, operation, success, error);
105
+ },
106
+ sendTerminalMcpStatus(terminalId, state, message, needsInputPrompt) {
107
+ self.getWsClientForTerminal(terminalId)?.sendTerminalMcpStatus(terminalId, state, message, needsInputPrompt);
108
+ },
109
+ sendTerminalCwd(terminalId, cwd) {
110
+ self.getWsClientForTerminal(terminalId)?.sendTerminalCwd(terminalId, cwd);
111
+ },
112
+ get connected() {
113
+ // Return true if any server is connected (PTYManager uses this to gate operations)
114
+ return self.connectedServers.size > 0;
115
+ },
116
+ };
117
+ }
118
+ /**
119
+ * Get the WSClient responsible for a given terminal
120
+ */
121
+ getWsClientForTerminal(terminalId) {
122
+ const serverUrl = this.terminalServerMap.get(terminalId);
123
+ if (serverUrl)
124
+ return this.wsClients.get(serverUrl);
125
+ // Fallback to primary
126
+ return this.getPrimaryWsClient() || undefined;
127
+ }
128
+ /**
129
+ * Get the primary server's WSClient
130
+ */
131
+ getPrimaryWsClient() {
132
+ if (this.primaryServerUrl) {
133
+ return this.wsClients.get(this.primaryServerUrl) || null;
134
+ }
135
+ // Fallback: first client
136
+ const first = this.wsClients.values().next();
137
+ return first.done ? null : first.value;
138
+ }
139
+ /**
140
+ * Broadcast to ALL connected WSClients
141
+ */
142
+ broadcastAll(fn) {
143
+ for (const ws of this.wsClients.values()) {
144
+ if (ws.connected)
145
+ fn(ws);
146
+ }
147
+ }
148
+ /**
149
+ * Setup shared PTY/SSH data handlers (these route output to the correct server)
150
+ */
151
+ setupSharedHandlers() {
152
+ // PTY -> WebSocket: Forward output to the server that owns the terminal
153
+ this.ptyManager.setDataHandler((terminalId, data) => {
154
+ this.getWsClientForTerminal(terminalId)?.sendTerminalOutput(terminalId, data);
155
+ });
156
+ this.ptyManager.setExitHandler((terminalId, exitCode) => {
157
+ const ws = this.getWsClientForTerminal(terminalId);
158
+ ws?.sendTerminalExit(terminalId, exitCode);
159
+ ws?.sendTerminalStatus(terminalId, 'STOPPED');
160
+ this.terminalServerMap.delete(terminalId);
161
+ this.emit('terminalsChanged');
162
+ });
163
+ this.ptyManager.setWorkingHandler((terminalId, isWorking, activeProcess) => {
164
+ this.getWsClientForTerminal(terminalId)?.sendTerminalWorking(terminalId, isWorking, activeProcess);
165
+ this.emit('terminalsChanged');
166
+ });
167
+ // SSH -> WebSocket: Forward output to the server that owns the terminal
168
+ this.sshManager.setDataHandler((terminalId, data) => {
169
+ this.getWsClientForTerminal(terminalId)?.sendTerminalOutput(terminalId, data);
170
+ });
171
+ this.sshManager.setExitHandler((terminalId, exitCode) => {
172
+ const ws = this.getWsClientForTerminal(terminalId);
173
+ ws?.sendTerminalExit(terminalId, exitCode);
174
+ ws?.sendTerminalStatus(terminalId, 'STOPPED');
175
+ this.terminalServerMap.delete(terminalId);
176
+ this.emit('terminalsChanged');
177
+ });
178
+ this.sshManager.setErrorHandler((terminalId, error) => {
179
+ logger.error(`SSH error for ${terminalId}: ${error}`);
180
+ this.getWsClientForTerminal(terminalId)?.sendTerminalStatus(terminalId, 'CRASHED', error);
181
+ });
182
+ // Port scanning — broadcast to ALL servers
183
+ this.portScanner.on('ports', (ports) => {
184
+ this.broadcastAll(ws => ws.sendPorts(ports));
185
+ });
186
+ }
187
+ /**
188
+ * Add a server connection with all event wiring
189
+ */
190
+ addServerConnection(entry) {
191
+ if (this.wsClients.has(entry.serverUrl)) {
192
+ logger.warn(`Already connected to ${entry.serverUrl}`);
193
+ return;
194
+ }
195
+ const wsClient = new WSClient(entry);
196
+ const tunnelManager = new TunnelManager(wsClient);
197
+ const serverUrl = entry.serverUrl;
198
+ this.wsClients.set(serverUrl, wsClient);
199
+ this.tunnelManagers.set(serverUrl, tunnelManager);
200
+ if (entry.isPrimary) {
201
+ this.primaryServerUrl = serverUrl;
202
+ }
203
+ // === Terminal creation: register terminalServerMap ===
204
+ wsClient.on('terminalCreate', (terminalId, cols, rows, options) => {
86
205
  if (this.ptyManager.has(terminalId)) {
87
206
  logger.info(`Terminal ${terminalId} already exists, reporting RUNNING`);
88
- this.wsClient.sendTerminalStatus(terminalId, 'RUNNING');
207
+ wsClient.sendTerminalStatus(terminalId, 'RUNNING');
89
208
  return;
90
209
  }
91
- logger.info(`Creating local terminal ${terminalId}`);
210
+ this.terminalServerMap.set(terminalId, serverUrl);
211
+ logger.info(`Creating local terminal ${terminalId} (server: ${entry.name})`);
92
212
  const config = getConfig();
93
- const serverUrl = (config.serverUrl || '').replace(/\/$/, '');
94
- const termifyStatusUrl = serverUrl ? `${serverUrl}/api/mcp/status` : undefined;
95
- const termifySessionId = config.agentId || config.machineId || terminalId;
96
- const termifyApiUrl = serverUrl ? `${serverUrl}/api` : undefined;
213
+ const srvUrl = (entry.serverUrl || '').replace(/\/$/, '');
214
+ const termifyStatusUrl = srvUrl ? `${srvUrl}/api/mcp/status` : undefined;
215
+ const termifySessionId = entry.agentId || config.machineId || terminalId;
216
+ const termifyApiUrl = srvUrl ? `${srvUrl}/api` : undefined;
97
217
  const env = {
98
218
  ...(options.env || {}),
99
219
  };
@@ -103,8 +223,8 @@ export class Agent extends EventEmitter {
103
223
  env.TERMIFY_TERMINAL_ID = terminalId;
104
224
  if (!env.TERMIFY_SESSION_ID)
105
225
  env.TERMIFY_SESSION_ID = termifySessionId;
106
- if (!env.TERMIFY_TOKEN && config.accessToken)
107
- env.TERMIFY_TOKEN = config.accessToken;
226
+ if (!env.TERMIFY_TOKEN && entry.accessToken)
227
+ env.TERMIFY_TOKEN = entry.accessToken;
108
228
  if (!env.TERMIFY_API_URL && termifyApiUrl)
109
229
  env.TERMIFY_API_URL = termifyApiUrl;
110
230
  const created = this.ptyManager.create(terminalId, {
@@ -115,45 +235,51 @@ export class Agent extends EventEmitter {
115
235
  env,
116
236
  });
117
237
  if (created) {
118
- this.wsClient.sendTerminalStatus(terminalId, 'RUNNING');
238
+ wsClient.sendTerminalStatus(terminalId, 'RUNNING');
119
239
  this.emit('terminalsChanged');
120
240
  }
121
241
  else {
122
- this.wsClient.sendTerminalStatus(terminalId, 'CRASHED', 'Failed to create terminal');
242
+ wsClient.sendTerminalStatus(terminalId, 'CRASHED', 'Failed to create terminal');
243
+ this.terminalServerMap.delete(terminalId);
123
244
  }
124
245
  });
125
- // WebSocket -> SSH: Handle SSH terminal requests (bastion pattern)
126
- this.wsClient.on('terminalCreateSSH', async (terminalId, cols, rows, sshConfig) => {
127
- logger.info(`Creating SSH terminal ${terminalId} -> ${sshConfig.host}:${sshConfig.port}`);
246
+ // SSH terminal creation
247
+ wsClient.on('terminalCreateSSH', async (terminalId, cols, rows, sshConfig) => {
248
+ this.terminalServerMap.set(terminalId, serverUrl);
249
+ logger.info(`Creating SSH terminal ${terminalId} -> ${sshConfig.host}:${sshConfig.port} (server: ${entry.name})`);
128
250
  const connected = await this.sshManager.connect(terminalId, cols, rows, sshConfig);
129
251
  if (connected) {
130
- this.wsClient.sendTerminalStatus(terminalId, 'RUNNING');
252
+ wsClient.sendTerminalStatus(terminalId, 'RUNNING');
131
253
  this.emit('terminalsChanged');
132
254
  }
133
255
  else {
134
- this.wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Failed to connect to ${sshConfig.host}`);
256
+ wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Failed to connect to ${sshConfig.host}`);
257
+ this.terminalServerMap.delete(terminalId);
135
258
  }
136
259
  });
137
- // SSH over tunnel - create SSH session using tunnel Duplex stream
138
- this.wsClient.on('terminalSSHTunnel', async (terminalId, tunnelId, cols, rows, sshConfig) => {
139
- logger.info(`Creating SSH tunnel terminal ${terminalId} via tunnel ${tunnelId}`);
140
- if (!this.tunnelManager.has(tunnelId)) {
260
+ // SSH over tunnel
261
+ wsClient.on('terminalSSHTunnel', async (terminalId, tunnelId, cols, rows, sshConfig) => {
262
+ this.terminalServerMap.set(terminalId, serverUrl);
263
+ logger.info(`Creating SSH tunnel terminal ${terminalId} via tunnel ${tunnelId} (server: ${entry.name})`);
264
+ if (!tunnelManager.has(tunnelId)) {
141
265
  logger.error(`Tunnel ${tunnelId} not found for SSH terminal ${terminalId}`);
142
- this.wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Tunnel ${tunnelId} not found`);
266
+ wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Tunnel ${tunnelId} not found`);
267
+ this.terminalServerMap.delete(terminalId);
143
268
  return;
144
269
  }
145
- const tunnelStream = this.tunnelManager.createTunnelStream(tunnelId);
270
+ const tunnelStream = tunnelManager.createTunnelStream(tunnelId);
146
271
  const connected = await this.sshManager.connectOverTunnel(terminalId, cols, rows, sshConfig, tunnelStream);
147
272
  if (connected) {
148
- this.wsClient.sendTerminalStatus(terminalId, 'RUNNING');
273
+ wsClient.sendTerminalStatus(terminalId, 'RUNNING');
149
274
  this.emit('terminalsChanged');
150
275
  }
151
276
  else {
152
- this.wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Failed to SSH over tunnel to ${sshConfig.host}`);
277
+ wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Failed to SSH over tunnel to ${sshConfig.host}`);
278
+ this.terminalServerMap.delete(terminalId);
153
279
  }
154
280
  });
155
- // Terminal input - route to appropriate manager
156
- this.wsClient.on('terminalInput', (terminalId, data) => {
281
+ // Terminal input - route to appropriate manager (terminals are shared)
282
+ wsClient.on('terminalInput', (terminalId, data) => {
157
283
  if (this.sshManager.has(terminalId)) {
158
284
  this.sshManager.write(terminalId, data);
159
285
  }
@@ -161,8 +287,8 @@ export class Agent extends EventEmitter {
161
287
  this.ptyManager.write(terminalId, data);
162
288
  }
163
289
  });
164
- // Terminal paste files - write to disk locally and paste @paths into the terminal.
165
- this.wsClient.on('terminalPasteFiles', async (terminalId, files) => {
290
+ // Terminal paste files
291
+ wsClient.on('terminalPasteFiles', async (terminalId, files) => {
166
292
  try {
167
293
  if (!Array.isArray(files) || files.length === 0)
168
294
  return;
@@ -189,8 +315,6 @@ export class Agent extends EventEmitter {
189
315
  await fs.writeFile(outPath, buf);
190
316
  writtenPaths.push(outPath);
191
317
  }
192
- // Paste paths into whichever terminal type is active.
193
- // NOTE: For SSH terminals, these files exist locally on the agent, not on the remote host.
194
318
  for (const p of writtenPaths) {
195
319
  const text = `@${p}\r`;
196
320
  if (this.sshManager.has(terminalId)) {
@@ -205,8 +329,8 @@ export class Agent extends EventEmitter {
205
329
  logger.warn(`[PasteFiles] Failed for terminal ${terminalId}: ${err instanceof Error ? err.message : String(err)}`);
206
330
  }
207
331
  });
208
- // Terminal resize - route to appropriate manager
209
- this.wsClient.on('terminalResize', (terminalId, cols, rows) => {
332
+ // Terminal resize
333
+ wsClient.on('terminalResize', (terminalId, cols, rows) => {
210
334
  if (this.sshManager.has(terminalId)) {
211
335
  this.sshManager.resize(terminalId, cols, rows);
212
336
  }
@@ -214,8 +338,8 @@ export class Agent extends EventEmitter {
214
338
  this.ptyManager.resize(terminalId, cols, rows);
215
339
  }
216
340
  });
217
- // Terminal stop - route to appropriate manager
218
- this.wsClient.on('terminalStop', (terminalId) => {
341
+ // Terminal stop
342
+ wsClient.on('terminalStop', (terminalId) => {
219
343
  if (this.sshManager.has(terminalId)) {
220
344
  this.sshManager.kill(terminalId);
221
345
  }
@@ -223,109 +347,78 @@ export class Agent extends EventEmitter {
223
347
  this.ptyManager.kill(terminalId);
224
348
  }
225
349
  });
226
- // PTY -> WebSocket: Forward output to server
227
- this.ptyManager.setDataHandler((terminalId, data) => {
228
- this.wsClient.sendTerminalOutput(terminalId, data);
229
- });
230
- this.ptyManager.setExitHandler((terminalId, exitCode) => {
231
- this.wsClient.sendTerminalExit(terminalId, exitCode);
232
- this.wsClient.sendTerminalStatus(terminalId, 'STOPPED');
233
- this.emit('terminalsChanged');
234
- });
235
- // PTY -> WebSocket: Forward working state changes with active process name
236
- this.ptyManager.setWorkingHandler((terminalId, isWorking, activeProcess) => {
237
- this.wsClient.sendTerminalWorking(terminalId, isWorking, activeProcess);
238
- this.emit('terminalsChanged');
239
- });
240
- // SSH -> WebSocket: Forward output to server
241
- this.sshManager.setDataHandler((terminalId, data) => {
242
- this.wsClient.sendTerminalOutput(terminalId, data);
243
- });
244
- this.sshManager.setExitHandler((terminalId, exitCode) => {
245
- this.wsClient.sendTerminalExit(terminalId, exitCode);
246
- this.wsClient.sendTerminalStatus(terminalId, 'STOPPED');
247
- this.emit('terminalsChanged');
248
- });
249
- this.sshManager.setErrorHandler((terminalId, error) => {
250
- logger.error(`SSH error for ${terminalId}: ${error}`);
251
- this.wsClient.sendTerminalStatus(terminalId, 'CRASHED', error);
252
- });
253
- // Tunnel operations: WSClient -> TunnelManager
254
- this.wsClient.on('tunnelCreate', (tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs) => {
255
- this.tunnelManager.handleCreate(tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs);
350
+ // Tunnel operations
351
+ wsClient.on('tunnelCreate', (tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs) => {
352
+ tunnelManager.handleCreate(tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs);
256
353
  });
257
- this.wsClient.on('tunnelClose', (tunnelId, reason) => {
258
- this.tunnelManager.handleClose(tunnelId, reason);
354
+ wsClient.on('tunnelClose', (tunnelId, reason) => {
355
+ tunnelManager.handleClose(tunnelId, reason);
259
356
  });
260
- this.wsClient.on('tunnelFlow', (tunnelId, action) => {
261
- this.tunnelManager.handleFlow(tunnelId, action);
357
+ wsClient.on('tunnelFlow', (tunnelId, action) => {
358
+ tunnelManager.handleFlow(tunnelId, action);
262
359
  });
263
- this.wsClient.on('tunnelBinaryData', (data) => {
264
- this.tunnelManager.handleBinaryFrame(data);
360
+ wsClient.on('tunnelBinaryData', (data) => {
361
+ tunnelManager.handleBinaryFrame(data);
265
362
  });
266
- // Git operations: WSClient -> PTYManager
267
- this.wsClient.on('gitCheckout', async (terminalId, branch) => {
363
+ // Git operations PTYManager (response goes back to the same server via shim)
364
+ wsClient.on('gitCheckout', async (terminalId, branch) => {
268
365
  const result = await this.ptyManager.gitCheckout(terminalId, branch);
269
366
  if (result.success) {
270
367
  const branchInfo = await this.ptyManager.getGitBranches(terminalId);
271
- this.wsClient.sendGitBranches(terminalId, branchInfo);
368
+ wsClient.sendGitBranches(terminalId, branchInfo);
272
369
  const status = await this.ptyManager.getGitStatus(terminalId);
273
- this.wsClient.sendGitStatus(terminalId, status);
370
+ wsClient.sendGitStatus(terminalId, status);
274
371
  }
275
372
  });
276
- this.wsClient.on('gitCreateBranch', async (terminalId, name) => {
373
+ wsClient.on('gitCreateBranch', async (terminalId, name) => {
277
374
  const result = await this.ptyManager.gitCreateBranch(terminalId, name);
278
375
  if (result.success) {
279
376
  const branchInfo = await this.ptyManager.getGitBranches(terminalId);
280
- this.wsClient.sendGitBranches(terminalId, branchInfo);
377
+ wsClient.sendGitBranches(terminalId, branchInfo);
281
378
  const status = await this.ptyManager.getGitStatus(terminalId);
282
- this.wsClient.sendGitStatus(terminalId, status);
379
+ wsClient.sendGitStatus(terminalId, status);
283
380
  }
284
381
  });
285
- this.wsClient.on('gitRefresh', async (terminalId) => {
382
+ wsClient.on('gitRefresh', async (terminalId) => {
286
383
  const branchInfo = await this.ptyManager.getGitBranches(terminalId);
287
- this.wsClient.sendGitBranches(terminalId, branchInfo);
384
+ wsClient.sendGitBranches(terminalId, branchInfo);
288
385
  const commits = await this.ptyManager.getGitLog(terminalId);
289
- this.wsClient.sendGitLog(terminalId, commits);
386
+ wsClient.sendGitLog(terminalId, commits);
290
387
  const status = await this.ptyManager.getGitStatus(terminalId);
291
- this.wsClient.sendGitStatus(terminalId, status);
388
+ wsClient.sendGitStatus(terminalId, status);
292
389
  });
293
- this.wsClient.on('gitFetch', async (terminalId) => {
390
+ wsClient.on('gitFetch', async (terminalId) => {
294
391
  const result = await this.ptyManager.gitFetch(terminalId);
295
- this.wsClient.sendGitOperationResult(terminalId, 'fetch', result.success, result.error);
392
+ wsClient.sendGitOperationResult(terminalId, 'fetch', result.success, result.error);
296
393
  if (result.success) {
297
394
  const status = await this.ptyManager.getGitStatus(terminalId);
298
- this.wsClient.sendGitStatus(terminalId, status);
395
+ wsClient.sendGitStatus(terminalId, status);
299
396
  }
300
397
  });
301
- this.wsClient.on('gitPush', async (terminalId) => {
398
+ wsClient.on('gitPush', async (terminalId) => {
302
399
  const result = await this.ptyManager.gitPush(terminalId);
303
- this.wsClient.sendGitOperationResult(terminalId, 'push', result.success, result.error);
400
+ wsClient.sendGitOperationResult(terminalId, 'push', result.success, result.error);
304
401
  if (result.success) {
305
402
  const status = await this.ptyManager.getGitStatus(terminalId);
306
- this.wsClient.sendGitStatus(terminalId, status);
403
+ wsClient.sendGitStatus(terminalId, status);
307
404
  }
308
405
  });
309
- this.wsClient.on('gitPull', async (terminalId) => {
406
+ wsClient.on('gitPull', async (terminalId) => {
310
407
  const result = await this.ptyManager.gitPull(terminalId);
311
- this.wsClient.sendGitOperationResult(terminalId, 'pull', result.success, result.error);
408
+ wsClient.sendGitOperationResult(terminalId, 'pull', result.success, result.error);
312
409
  if (result.success) {
313
410
  const branchInfo = await this.ptyManager.getGitBranches(terminalId);
314
- this.wsClient.sendGitBranches(terminalId, branchInfo);
411
+ wsClient.sendGitBranches(terminalId, branchInfo);
315
412
  const status = await this.ptyManager.getGitStatus(terminalId);
316
- this.wsClient.sendGitStatus(terminalId, status);
413
+ wsClient.sendGitStatus(terminalId, status);
317
414
  }
318
415
  });
319
- this.wsClient.on('gitStatus', async (terminalId) => {
416
+ wsClient.on('gitStatus', async (terminalId) => {
320
417
  const status = await this.ptyManager.getGitStatus(terminalId);
321
- this.wsClient.sendGitStatus(terminalId, status);
322
- });
323
- // Port scanning
324
- this.portScanner.on('ports', (ports) => {
325
- this.wsClient.sendPorts(ports);
418
+ wsClient.sendGitStatus(terminalId, status);
326
419
  });
327
420
  // Preview proxy requests
328
- this.wsClient.on('previewRequest', async (requestId, port, reqPath, method, headers, body) => {
421
+ wsClient.on('previewRequest', async (requestId, port, reqPath, method, headers, body) => {
329
422
  try {
330
423
  const url = `http://localhost:${port}${reqPath}`;
331
424
  const fetchOpts = { method, headers };
@@ -341,155 +434,159 @@ export class Agent extends EventEmitter {
341
434
  });
342
435
  if (isText) {
343
436
  const text = await response.text();
344
- this.wsClient.sendPreviewResponse(requestId, response.status, responseHeaders, text, false);
437
+ wsClient.sendPreviewResponse(requestId, response.status, responseHeaders, text, false);
345
438
  }
346
439
  else {
347
440
  const buf = Buffer.from(await response.arrayBuffer());
348
- this.wsClient.sendPreviewResponse(requestId, response.status, responseHeaders, buf.toString('base64'), true);
441
+ wsClient.sendPreviewResponse(requestId, response.status, responseHeaders, buf.toString('base64'), true);
349
442
  }
350
443
  }
351
444
  catch (err) {
352
- this.wsClient.sendPreviewResponse(requestId, 502, {}, `Agent could not reach localhost:${port} - ${err.message}`, false);
445
+ wsClient.sendPreviewResponse(requestId, 502, {}, `Agent could not reach localhost:${port} - ${err.message}`, false);
353
446
  }
354
447
  });
355
- // WireGuard feature toggle from server
356
- this.wsClient.on('wgFeature', (enabled, version) => {
357
- logger.info(`WireGuard feature: enabled=${enabled} version=${version}`);
358
- });
359
- // WG peer signaling
360
- this.wsClient.on('wgPeersFull', async (selfVirtualIp, peers) => {
361
- if (!this.wgManager.getStatus().available)
362
- return;
363
- try {
364
- // Ensure interface is up with our virtual IP
365
- await this.wgManager.ensureInterface(selfVirtualIp);
366
- // Apply all peers
367
- const wgPeers = peers.map((p) => ({
368
- agentId: p.agentId,
369
- publicKey: p.publicKey,
370
- allowedIps: `${p.virtualIp}/32`,
371
- endpoint: p.endpointHint,
372
- persistentKeepalive: 25,
373
- }));
374
- await this.wgManager.setPeers(wgPeers);
375
- // Report ready
376
- const status = this.wgManager.getStatus();
377
- if (status.publicKey && status.listenPort) {
378
- this.wsClient.sendWgReady(status.interfaceName || 'termify0', status.publicKey, status.listenPort);
448
+ // WireGuard: only wire to primary server
449
+ if (entry.isPrimary) {
450
+ wsClient.on('wgFeature', (enabled, version) => {
451
+ logger.info(`WireGuard feature: enabled=${enabled} version=${version}`);
452
+ });
453
+ wsClient.on('wgPeersFull', async (selfVirtualIp, peers) => {
454
+ if (!this.wgManager.getStatus().available)
455
+ return;
456
+ try {
457
+ await this.wgManager.ensureInterface(selfVirtualIp);
458
+ const wgPeers = peers.map((p) => ({
459
+ agentId: p.agentId,
460
+ publicKey: p.publicKey,
461
+ allowedIps: `${p.virtualIp}/32`,
462
+ endpoint: p.endpointHint,
463
+ persistentKeepalive: 25,
464
+ }));
465
+ await this.wgManager.setPeers(wgPeers);
466
+ const status = this.wgManager.getStatus();
467
+ if (status.publicKey && status.listenPort) {
468
+ wsClient.sendWgReady(status.interfaceName || 'termify0', status.publicKey, status.listenPort);
469
+ }
470
+ logger.info(`WG peers configured: ${peers.length} peers, virtualIp=${selfVirtualIp}`);
379
471
  }
380
- logger.info(`WG peers configured: ${peers.length} peers, virtualIp=${selfVirtualIp}`);
381
- }
382
- catch (err) {
383
- logger.error('Failed to configure WG peers:', err);
384
- }
385
- });
386
- this.wsClient.on('wgPeerAdd', async (peer) => {
387
- if (!this.wgManager.getStatus().available)
388
- return;
389
- try {
390
- await this.wgManager.addPeer({
391
- agentId: peer.agentId,
392
- publicKey: peer.publicKey,
393
- allowedIps: `${peer.virtualIp}/32`,
394
- endpoint: peer.endpointHint,
395
- persistentKeepalive: 25,
396
- });
397
- logger.info(`WG peer added: ${peer.agentId}`);
398
- }
399
- catch (err) {
400
- logger.error(`Failed to add WG peer ${peer.agentId}:`, err);
401
- }
402
- });
403
- this.wsClient.on('wgPeerRemove', async (_agentId, publicKey) => {
404
- if (!this.wgManager.getStatus().available)
405
- return;
406
- try {
407
- await this.wgManager.removePeer(publicKey);
408
- logger.info(`WG peer removed: ${_agentId}`);
409
- }
410
- catch (err) {
411
- logger.error(`Failed to remove WG peer:`, err);
412
- }
413
- });
414
- // NAT traversal
415
- this.wsClient.on('wgNatProbe', async (token, servers) => {
416
- if (!this.wgManager.getStatus().available)
417
- return;
418
- try {
419
- await this.natTraversal.probe({ token, servers });
420
- logger.info('NAT probe sent to ' + servers.length + ' discovery servers');
421
- }
422
- catch (err) {
423
- logger.error('NAT probe failed:', err);
424
- }
425
- });
426
- this.wsClient.on('wgEndpointCandidates', async (peerAgentId, peerPublicKey, candidates) => {
427
- if (!this.wgManager.getStatus().available)
428
- return;
429
- try {
430
- if (candidates.length > 0) {
431
- const best = candidates[0];
432
- const endpoint = `${best.ip}:${best.port}`;
433
- await this.wgManager.updatePeerEndpoint(peerPublicKey, endpoint);
434
- logger.info(`Updated endpoint for peer ${peerAgentId}: ${endpoint}`);
472
+ catch (err) {
473
+ logger.error('Failed to configure WG peers:', err);
474
+ }
475
+ });
476
+ wsClient.on('wgPeerAdd', async (peer) => {
477
+ if (!this.wgManager.getStatus().available)
478
+ return;
479
+ try {
480
+ await this.wgManager.addPeer({
481
+ agentId: peer.agentId,
482
+ publicKey: peer.publicKey,
483
+ allowedIps: `${peer.virtualIp}/32`,
484
+ endpoint: peer.endpointHint,
485
+ persistentKeepalive: 25,
486
+ });
487
+ logger.info(`WG peer added: ${peer.agentId}`);
488
+ }
489
+ catch (err) {
490
+ logger.error(`Failed to add WG peer ${peer.agentId}:`, err);
491
+ }
492
+ });
493
+ wsClient.on('wgPeerRemove', async (_agentId, publicKey) => {
494
+ if (!this.wgManager.getStatus().available)
495
+ return;
496
+ try {
497
+ await this.wgManager.removePeer(publicKey);
498
+ logger.info(`WG peer removed: ${_agentId}`);
499
+ }
500
+ catch (err) {
501
+ logger.error(`Failed to remove WG peer:`, err);
502
+ }
503
+ });
504
+ wsClient.on('wgNatProbe', async (token, servers) => {
505
+ if (!this.wgManager.getStatus().available)
506
+ return;
507
+ try {
508
+ await this.natTraversal.probe({ token, servers });
509
+ logger.info('NAT probe sent to ' + servers.length + ' discovery servers');
510
+ }
511
+ catch (err) {
512
+ logger.error('NAT probe failed:', err);
513
+ }
514
+ });
515
+ wsClient.on('wgEndpointCandidates', async (peerAgentId, peerPublicKey, candidates) => {
516
+ if (!this.wgManager.getStatus().available)
517
+ return;
518
+ try {
519
+ if (candidates.length > 0) {
520
+ const best = candidates[0];
521
+ const endpoint = `${best.ip}:${best.port}`;
522
+ await this.wgManager.updatePeerEndpoint(peerPublicKey, endpoint);
523
+ logger.info(`Updated endpoint for peer ${peerAgentId}: ${endpoint}`);
524
+ }
525
+ }
526
+ catch (err) {
527
+ logger.error(`Failed to update endpoint for peer ${peerAgentId}:`, err);
528
+ }
529
+ });
530
+ wsClient.on('wgPeerUpdate', async (agentId, oldPublicKey, newPublicKey, keyVersion) => {
531
+ if (!this.wgManager.getStatus().available)
532
+ return;
533
+ try {
534
+ const health = await this.wgManager.getHealth();
535
+ const existingPeer = health?.peers.find(p => p.publicKey === oldPublicKey);
536
+ const allowedIps = existingPeer?.allowedIps || '';
537
+ const endpoint = existingPeer?.endpoint;
538
+ await this.wgManager.updatePeerKey(oldPublicKey, newPublicKey, allowedIps, endpoint);
539
+ wsClient.sendWgRotateApplied(keyVersion);
540
+ logger.info(`Peer ${agentId} key updated to v${keyVersion}`);
541
+ }
542
+ catch (err) {
543
+ logger.error(`Failed to update peer ${agentId} key:`, err);
544
+ }
545
+ });
546
+ wsClient.on('wgRotateCommit', (keyVersion) => {
547
+ this.keyRotation.confirmRotation(keyVersion);
548
+ this.reportWgStatus();
549
+ });
550
+ }
551
+ // Connection events
552
+ wsClient.on('connected', (agentId, agentName) => {
553
+ this.connectedServers.add(serverUrl);
554
+ // Use primary server's agentId/name for dashboard
555
+ if (entry.isPrimary) {
556
+ this.agentId = agentId;
557
+ this.agentName = agentName;
558
+ }
559
+ this.emit('connected', agentId, agentName, serverUrl);
560
+ // Start stats/ports only once (on first server connection)
561
+ if (this.connectedServers.size === 1) {
562
+ this.startStatsCollection();
563
+ this.portScanner.start();
564
+ if (entry.isPrimary) {
565
+ this.initWireGuard().catch(err => {
566
+ logger.warn('WireGuard initialization failed:', err);
567
+ });
435
568
  }
436
- }
437
- catch (err) {
438
- logger.error(`Failed to update endpoint for peer ${peerAgentId}:`, err);
439
569
  }
440
570
  });
441
- // Key rotation: another peer rotated their key
442
- this.wsClient.on('wgPeerUpdate', async (agentId, oldPublicKey, newPublicKey, keyVersion) => {
443
- if (!this.wgManager.getStatus().available)
444
- return;
445
- try {
446
- // Find peer's allowedIps from current WG config
447
- const health = await this.wgManager.getHealth();
448
- const existingPeer = health?.peers.find(p => p.publicKey === oldPublicKey);
449
- const allowedIps = existingPeer?.allowedIps || '';
450
- const endpoint = existingPeer?.endpoint;
451
- await this.wgManager.updatePeerKey(oldPublicKey, newPublicKey, allowedIps, endpoint);
452
- // ACK the update
453
- this.wsClient.sendWgRotateApplied(keyVersion);
454
- logger.info(`Peer ${agentId} key updated to v${keyVersion}`);
455
- }
456
- catch (err) {
457
- logger.error(`Failed to update peer ${agentId} key:`, err);
571
+ wsClient.on('disconnected', (reason) => {
572
+ this.connectedServers.delete(serverUrl);
573
+ this.emit('disconnected', reason, serverUrl);
574
+ // Stop stats/ports only when ALL servers are disconnected
575
+ if (this.connectedServers.size === 0) {
576
+ this.statsManager.stop();
577
+ this.portScanner.stop();
458
578
  }
459
579
  });
460
- // Key rotation: server confirms all peers accepted our new key
461
- this.wsClient.on('wgRotateCommit', (keyVersion) => {
462
- this.keyRotation.confirmRotation(keyVersion);
463
- // Report updated status
464
- this.reportWgStatus();
465
- });
466
- // Connection events
467
- this.wsClient.on('connected', (agentId, agentName) => {
468
- this.agentId = agentId;
469
- this.agentName = agentName;
470
- this.emit('connected', agentId, agentName);
471
- // Start stats collection and port scanning when connected
472
- this.startStatsCollection();
473
- this.portScanner.start();
474
- // Initialize WireGuard (non-blocking, don't delay connection)
475
- this.initWireGuard().catch(err => {
476
- logger.warn('WireGuard initialization failed:', err);
477
- });
478
- });
479
- this.wsClient.on('disconnected', (reason) => {
480
- // Stop stats and port scanning when disconnected
481
- this.statsManager.stop();
482
- this.portScanner.stop();
483
- this.emit('disconnected', reason);
484
- });
485
- this.wsClient.on('authFailed', (error) => {
486
- logger.error(`Authentication failed: ${error}`);
487
- this.emit('authFailed', error);
488
- this.stop();
580
+ wsClient.on('authFailed', (error) => {
581
+ logger.error(`Authentication failed for ${entry.name}: ${error}`);
582
+ if (entry.isPrimary) {
583
+ this.emit('authFailed', error);
584
+ this.stop();
585
+ }
489
586
  });
490
587
  }
491
588
  /**
492
- * Start the agent
589
+ * Start the agent — connects to all configured servers
493
590
  */
494
591
  async start(options = {}) {
495
592
  if (!isConfigured()) {
@@ -513,78 +610,99 @@ export class Agent extends EventEmitter {
513
610
  else {
514
611
  logger.warn('termify-daemon not available, falling back to stats-agent and pgrep');
515
612
  }
516
- // Connect to server
517
- this.wsClient.connect();
613
+ // Connect to all configured servers
614
+ const servers = getServers();
615
+ if (servers.length === 0) {
616
+ // Backward compat: no servers array, use legacy config
617
+ const cfg = getConfig();
618
+ const legacy = {
619
+ name: 'primary',
620
+ serverUrl: cfg.serverUrl,
621
+ wsUrl: cfg.wsUrl,
622
+ accessToken: cfg.accessToken,
623
+ agentId: cfg.agentId,
624
+ lastConnected: cfg.lastConnected,
625
+ isPrimary: true,
626
+ };
627
+ this.addServerConnection(legacy);
628
+ this.wsClients.get(legacy.serverUrl)?.connect();
629
+ }
630
+ else {
631
+ for (const entry of servers) {
632
+ if (!entry.accessToken)
633
+ continue; // Skip unconfigured servers
634
+ this.addServerConnection(entry);
635
+ this.wsClients.get(entry.serverUrl)?.connect();
636
+ }
637
+ }
518
638
  return true;
519
639
  }
520
640
  /**
521
- * Stop the agent
641
+ * Stop the agent — disconnects from ALL servers
522
642
  */
523
643
  stop() {
524
- if (!this.isRunning) {
644
+ if (!this.isRunning)
525
645
  return;
526
- }
527
646
  logger.info('Stopping agent...');
528
647
  this.isRunning = false;
529
- // Stop stats collection and port scanning
530
648
  this.statsManager.stop();
531
649
  this.portScanner.stop();
532
650
  this.daemonClient.stopStatsCollection();
533
651
  this.daemonClient.disconnect();
534
- // Shut down NAT traversal, key rotation, and WireGuard interface
535
652
  this.keyRotation.stop();
536
653
  this.natTraversal.shutdown();
537
654
  this.wgManager.shutdown().catch(err => {
538
655
  logger.warn('WireGuard shutdown error:', err);
539
656
  });
540
657
  // Close all tunnels
541
- this.tunnelManager.closeAll();
542
- // Kill all terminals (both local and SSH)
658
+ for (const tm of this.tunnelManagers.values()) {
659
+ tm.closeAll();
660
+ }
543
661
  this.ptyManager.killAll();
544
662
  this.sshManager.killAll();
545
- // Disconnect from server
546
- this.wsClient.disconnect();
663
+ // Disconnect ALL servers
664
+ for (const ws of this.wsClients.values()) {
665
+ ws.disconnect();
666
+ }
667
+ this.connectedServers.clear();
668
+ this.terminalServerMap.clear();
547
669
  this.emit('stopped');
548
670
  }
549
671
  /**
550
- * Hibernate the agent - kills PTYs silently so server marks them HIBERNATED.
551
- * Used during graceful shutdown (Cmd+Q, SIGTERM) to preserve terminal state.
672
+ * Hibernate the agent kills PTYs silently, disconnects ALL servers
552
673
  */
553
674
  hibernate() {
554
- if (!this.isRunning) {
675
+ if (!this.isRunning)
555
676
  return;
556
- }
557
677
  logger.info('Hibernating agent...');
558
678
  this.isRunning = false;
559
- // Stop stats collection and port scanning
560
679
  this.statsManager.stop();
561
680
  this.portScanner.stop();
562
681
  this.daemonClient.stopStatsCollection();
563
682
  this.daemonClient.disconnect();
564
- // Shut down NAT traversal, key rotation, and WireGuard interface
565
683
  this.keyRotation.stop();
566
684
  this.natTraversal.shutdown();
567
685
  this.wgManager.shutdown().catch(err => {
568
686
  logger.warn('WireGuard shutdown error:', err);
569
687
  });
570
- // Close all tunnels
571
- this.tunnelManager.closeAll();
572
- // Hibernate local terminals (silent kill - no exit events sent to server)
688
+ for (const tm of this.tunnelManagers.values()) {
689
+ tm.closeAll();
690
+ }
573
691
  this.ptyManager.hibernateAll();
574
- // SSH can't hibernate - just kill them
575
692
  this.sshManager.killAll();
576
- // Disconnect from server (this triggers server-side HIBERNATED marking)
577
- this.wsClient.disconnect();
693
+ for (const ws of this.wsClients.values()) {
694
+ ws.disconnect();
695
+ }
696
+ this.connectedServers.clear();
697
+ this.terminalServerMap.clear();
578
698
  this.emit('stopped');
579
699
  }
580
700
  /**
581
- * Start collecting and sending system stats
701
+ * Start collecting and sending system stats — broadcast to ALL servers
582
702
  */
583
703
  startStatsCollection() {
584
- // Always use stats-agent for complete system metrics
585
- // (daemon only provides basic CPU/RAM, missing disks, network, per-core CPU, swap)
586
704
  this.statsManager.setStatsCallback((stats) => {
587
- this.wsClient.sendStats(stats);
705
+ this.broadcastAll(ws => ws.sendStats(stats));
588
706
  this.emit('stats', stats);
589
707
  });
590
708
  const started = this.statsManager.start();
@@ -596,12 +714,10 @@ export class Agent extends EventEmitter {
596
714
  }
597
715
  }
598
716
  /**
599
- * Initialize WireGuard detection and report status.
600
- * Non-blocking — called with .catch() so it never delays the connection.
717
+ * Initialize WireGuard (primary server only)
601
718
  */
602
719
  async initWireGuard() {
603
720
  const installResult = await this.wgManager.ensureInstalled();
604
- // Report initial status regardless of availability
605
721
  this.reportWgStatus();
606
722
  if (installResult.available) {
607
723
  const status = this.wgManager.getStatus();
@@ -613,15 +729,15 @@ export class Agent extends EventEmitter {
613
729
  logger.info(`WireGuard not available: ${installResult.availability}`);
614
730
  return;
615
731
  }
616
- // Interface setup will happen when we receive our virtual IP from signaling (Phase 2)
617
732
  logger.info(`WireGuard detected: backend=${installResult.backend}, version=${installResult.version}`);
618
733
  }
619
734
  /**
620
- * Report WireGuard status to server using real data from the WireGuardManager.
735
+ * Report WireGuard status to primary server
621
736
  */
622
737
  reportWgStatus() {
623
738
  const status = this.wgManager.getStatus();
624
- this.wsClient.sendWgStatus({
739
+ const primaryWs = this.getPrimaryWsClient();
740
+ primaryWs?.sendWgStatus({
625
741
  type: 'agent.wg.status',
626
742
  available: status.available,
627
743
  availability: status.availability,
@@ -629,7 +745,7 @@ export class Agent extends EventEmitter {
629
745
  version: status.version,
630
746
  publicKey: status.publicKey,
631
747
  listenPort: status.listenPort,
632
- natType: 'unknown', // Phase 3 will detect this
748
+ natType: 'unknown',
633
749
  connectedPeers: status.connectedPeers,
634
750
  });
635
751
  }
@@ -639,50 +755,38 @@ export class Agent extends EventEmitter {
639
755
  handleShutdown(signal) {
640
756
  logger.info(`Received ${signal}, hibernating...`);
641
757
  this.hibernate();
642
- // Small delay to let WS disconnect message flush before exiting
643
758
  setTimeout(() => process.exit(0), 200);
644
759
  }
645
- /**
646
- * Check if agent is running
647
- */
648
760
  get running() {
649
761
  return this.isRunning;
650
762
  }
651
- /**
652
- * Check if connected to server
653
- */
654
763
  get connected() {
655
- return this.wsClient.connected;
764
+ return this.connectedServers.size > 0;
656
765
  }
657
- /**
658
- * Get current agent ID
659
- */
660
766
  get id() {
661
767
  return this.agentId;
662
768
  }
663
- /**
664
- * Get current agent name
665
- */
666
769
  get name() {
667
770
  return this.agentName;
668
771
  }
669
- /**
670
- * Get number of active terminals (local + SSH)
671
- */
672
772
  get terminalCount() {
673
773
  return this.ptyManager.count + this.sshManager.count;
674
774
  }
675
775
  /**
676
- * Get status information
776
+ * Get number of connected servers
677
777
  */
778
+ get serverCount() {
779
+ return this.connectedServers.size;
780
+ }
678
781
  getStatus() {
679
782
  const config = getConfig();
680
783
  return {
681
784
  running: this.isRunning,
682
- connected: this.wsClient.connected,
785
+ connected: this.connectedServers.size > 0,
683
786
  agentId: this.agentId,
684
787
  agentName: this.agentName,
685
788
  terminalCount: this.ptyManager.count,
789
+ serverCount: this.connectedServers.size,
686
790
  config: {
687
791
  serverUrl: config.serverUrl,
688
792
  machineId: config.machineId,
@@ -690,9 +794,6 @@ export class Agent extends EventEmitter {
690
794
  },
691
795
  };
692
796
  }
693
- /**
694
- * Get combined list of all terminals for dashboard display
695
- */
696
797
  getTerminals() {
697
798
  const locals = this.ptyManager.listTerminals().map(t => ({
698
799
  id: t.id,
@@ -710,9 +811,6 @@ export class Agent extends EventEmitter {
710
811
  }));
711
812
  return [...locals, ...sshs];
712
813
  }
713
- /**
714
- * Get WireGuard info for dashboard display
715
- */
716
814
  getWireGuardInfo() {
717
815
  const status = this.wgManager.getStatus();
718
816
  return {