agentgui 1.0.67 → 1.0.69
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/.prd +92 -0
- package/.prd-browser +607 -0
- package/CLAUDE.md +1559 -125
- package/browser-test-harness.js +371 -0
- package/browser-test.js +409 -0
- package/execute-tests.js +164 -0
- package/lib/claude-runner.js +41 -12
- package/lib/database-service.ts +252 -0
- package/lib/sync-service.ts +275 -0
- package/lib/types.ts +168 -0
- package/package.json +1 -1
- package/readme.md +586 -0
- package/run-e2e-test.sh +88 -0
- package/server.js +274 -8
- package/static/index.html +487 -180
- package/static/js/client.js +558 -0
- package/static/js/event-filter.js +311 -0
- package/static/js/event-processor.js +454 -0
- package/static/js/streaming-renderer.js +813 -0
- package/static/js/syntax-highlighter.js +271 -0
- package/static/js/ui-components.js +433 -0
- package/static/js/websocket-manager.js +482 -0
- package/static/templates/INDEX.html +465 -0
- package/static/templates/README.md +190 -0
- package/static/templates/agent-capabilities.html +56 -0
- package/static/templates/agent-metadata-panel.html +44 -0
- package/static/templates/agent-status-badge.html +30 -0
- package/static/templates/code-annotation-panel.html +155 -0
- package/static/templates/code-suggestion-panel.html +184 -0
- package/static/templates/command-header.html +77 -0
- package/static/templates/command-output-scrollable.html +118 -0
- package/static/templates/elapsed-time.html +54 -0
- package/static/templates/error-alert.html +106 -0
- package/static/templates/error-history-timeline.html +160 -0
- package/static/templates/error-recovery-options.html +109 -0
- package/static/templates/error-stack-trace.html +95 -0
- package/static/templates/error-summary.html +80 -0
- package/static/templates/event-counter.html +48 -0
- package/static/templates/execution-actions.html +97 -0
- package/static/templates/execution-progress-bar.html +80 -0
- package/static/templates/execution-stepper.html +120 -0
- package/static/templates/file-breadcrumb.html +118 -0
- package/static/templates/file-diff-viewer.html +121 -0
- package/static/templates/file-metadata.html +133 -0
- package/static/templates/file-read-panel.html +66 -0
- package/static/templates/file-write-panel.html +120 -0
- package/static/templates/git-branch-remote.html +107 -0
- package/static/templates/git-diff-list.html +101 -0
- package/static/templates/git-log-visualization.html +153 -0
- package/static/templates/git-status-panel.html +115 -0
- package/static/templates/quality-metrics-display.html +170 -0
- package/static/templates/terminal-output-panel.html +87 -0
- package/static/templates/test-results-display.html +144 -0
- package/test-browser.js +457 -0
- package/test-runner.js +182 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Manager
|
|
3
|
+
* Handles WebSocket connection, auto-reconnect, message buffering,
|
|
4
|
+
* and event distribution for streaming events
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class WebSocketManager {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
// Configuration
|
|
10
|
+
this.config = {
|
|
11
|
+
url: config.url || this.getWebSocketURL(),
|
|
12
|
+
reconnectDelays: config.reconnectDelays || [1000, 2000, 4000, 8000, 16000],
|
|
13
|
+
maxReconnectDelay: config.maxReconnectDelay || 30000,
|
|
14
|
+
heartbeatInterval: config.heartbeatInterval || 30000,
|
|
15
|
+
messageTimeout: config.messageTimeout || 60000,
|
|
16
|
+
maxBufferedMessages: config.maxBufferedMessages || 1000,
|
|
17
|
+
...config
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// State
|
|
21
|
+
this.ws = null;
|
|
22
|
+
this.isConnected = false;
|
|
23
|
+
this.isConnecting = false;
|
|
24
|
+
this.reconnectCount = 0;
|
|
25
|
+
this.messageBuffer = [];
|
|
26
|
+
this.requestMap = new Map();
|
|
27
|
+
this.heartbeatTimer = null;
|
|
28
|
+
this.connectionState = 'disconnected';
|
|
29
|
+
|
|
30
|
+
// Statistics
|
|
31
|
+
this.stats = {
|
|
32
|
+
totalConnections: 0,
|
|
33
|
+
totalReconnects: 0,
|
|
34
|
+
totalMessagesSent: 0,
|
|
35
|
+
totalMessagesReceived: 0,
|
|
36
|
+
totalErrors: 0,
|
|
37
|
+
totalTimeouts: 0,
|
|
38
|
+
avgLatency: 0,
|
|
39
|
+
lastConnectedTime: null,
|
|
40
|
+
connectionDuration: 0
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Event listeners
|
|
44
|
+
this.listeners = {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get WebSocket URL from current window location
|
|
49
|
+
*/
|
|
50
|
+
getWebSocketURL() {
|
|
51
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
52
|
+
const baseURL = window.__BASE_URL || '/gm';
|
|
53
|
+
return `${protocol}//${window.location.host}${baseURL}/ws`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Connect to WebSocket server
|
|
58
|
+
*/
|
|
59
|
+
async connect() {
|
|
60
|
+
if (this.isConnected || this.isConnecting) {
|
|
61
|
+
return this.ws;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.isConnecting = true;
|
|
65
|
+
this.setConnectionState('connecting');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
console.log('WebSocket connecting to:', this.config.url);
|
|
69
|
+
|
|
70
|
+
this.ws = new WebSocket(this.config.url);
|
|
71
|
+
|
|
72
|
+
this.ws.onopen = () => this.onOpen();
|
|
73
|
+
this.ws.onmessage = (event) => this.onMessage(event);
|
|
74
|
+
this.ws.onerror = (error) => this.onError(error);
|
|
75
|
+
this.ws.onclose = () => this.onClose();
|
|
76
|
+
|
|
77
|
+
// Wait for connection with timeout
|
|
78
|
+
return await this.waitForConnection(this.config.messageTimeout);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('WebSocket connection error:', error);
|
|
81
|
+
this.isConnecting = false;
|
|
82
|
+
this.stats.totalErrors++;
|
|
83
|
+
await this.scheduleReconnect();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Wait for connection to establish
|
|
90
|
+
*/
|
|
91
|
+
waitForConnection(timeout = 5000) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
reject(new Error('WebSocket connection timeout'));
|
|
95
|
+
}, timeout);
|
|
96
|
+
|
|
97
|
+
const checkConnection = () => {
|
|
98
|
+
if (this.isConnected) {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
resolve(this.ws);
|
|
101
|
+
} else if (this.ws?.readyState === WebSocket.OPEN) {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
resolve(this.ws);
|
|
104
|
+
} else {
|
|
105
|
+
setTimeout(checkConnection, 50);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
checkConnection();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Handle WebSocket open
|
|
115
|
+
*/
|
|
116
|
+
onOpen() {
|
|
117
|
+
console.log('WebSocket connected');
|
|
118
|
+
this.isConnected = true;
|
|
119
|
+
this.isConnecting = false;
|
|
120
|
+
this.reconnectCount = 0;
|
|
121
|
+
this.stats.totalConnections++;
|
|
122
|
+
this.stats.lastConnectedTime = Date.now();
|
|
123
|
+
this.setConnectionState('connected');
|
|
124
|
+
|
|
125
|
+
// Flush buffered messages
|
|
126
|
+
this.flushMessageBuffer();
|
|
127
|
+
|
|
128
|
+
// Start heartbeat
|
|
129
|
+
this.startHeartbeat();
|
|
130
|
+
|
|
131
|
+
// Emit connected event
|
|
132
|
+
this.emit('connected', { timestamp: Date.now() });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle WebSocket message
|
|
137
|
+
*/
|
|
138
|
+
onMessage(event) {
|
|
139
|
+
try {
|
|
140
|
+
const data = JSON.parse(event.data);
|
|
141
|
+
this.stats.totalMessagesReceived++;
|
|
142
|
+
|
|
143
|
+
// Handle pong response
|
|
144
|
+
if (data.type === 'pong') {
|
|
145
|
+
const requestId = data.requestId;
|
|
146
|
+
if (requestId && this.requestMap.has(requestId)) {
|
|
147
|
+
const request = this.requestMap.get(requestId);
|
|
148
|
+
request.resolve({ latency: Date.now() - request.sentTime });
|
|
149
|
+
this.requestMap.delete(requestId);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Route message to listeners
|
|
155
|
+
this.emit('message', data);
|
|
156
|
+
|
|
157
|
+
// Route by type
|
|
158
|
+
if (data.type) {
|
|
159
|
+
this.emit(`message:${data.type}`, data);
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('WebSocket message parse error:', error);
|
|
163
|
+
this.stats.totalErrors++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handle WebSocket error
|
|
169
|
+
*/
|
|
170
|
+
onError(error) {
|
|
171
|
+
console.error('WebSocket error:', error);
|
|
172
|
+
this.stats.totalErrors++;
|
|
173
|
+
this.emit('error', { error, timestamp: Date.now() });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle WebSocket close
|
|
178
|
+
*/
|
|
179
|
+
onClose() {
|
|
180
|
+
console.log('WebSocket disconnected');
|
|
181
|
+
this.isConnected = false;
|
|
182
|
+
this.isConnecting = false;
|
|
183
|
+
this.setConnectionState('disconnected');
|
|
184
|
+
|
|
185
|
+
// Stop heartbeat
|
|
186
|
+
if (this.heartbeatTimer) {
|
|
187
|
+
clearTimeout(this.heartbeatTimer);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Update connection duration
|
|
191
|
+
if (this.stats.lastConnectedTime) {
|
|
192
|
+
this.stats.connectionDuration = Date.now() - this.stats.lastConnectedTime;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.emit('disconnected', { timestamp: Date.now() });
|
|
196
|
+
|
|
197
|
+
// Attempt reconnect
|
|
198
|
+
if (!this.isManuallyDisconnected) {
|
|
199
|
+
this.scheduleReconnect();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Schedule reconnection with exponential backoff
|
|
205
|
+
*/
|
|
206
|
+
async scheduleReconnect() {
|
|
207
|
+
if (this.reconnectCount >= this.config.reconnectDelays.length) {
|
|
208
|
+
this.setConnectionState('reconnect_failed');
|
|
209
|
+
console.error('Max reconnection attempts reached');
|
|
210
|
+
this.emit('reconnect_failed', { attempts: this.reconnectCount });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const delay = this.config.reconnectDelays[this.reconnectCount];
|
|
215
|
+
this.reconnectCount++;
|
|
216
|
+
this.stats.totalReconnects++;
|
|
217
|
+
this.setConnectionState('reconnecting');
|
|
218
|
+
|
|
219
|
+
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectCount}/${this.config.reconnectDelays.length})`);
|
|
220
|
+
|
|
221
|
+
this.emit('reconnecting', { delay, attempt: this.reconnectCount });
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
this.connect().catch((error) => {
|
|
226
|
+
console.error('Reconnection attempt failed:', error);
|
|
227
|
+
});
|
|
228
|
+
resolve();
|
|
229
|
+
}, delay);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Start heartbeat/keepalive
|
|
235
|
+
*/
|
|
236
|
+
startHeartbeat() {
|
|
237
|
+
if (this.heartbeatTimer) {
|
|
238
|
+
clearTimeout(this.heartbeatTimer);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.heartbeatTimer = setInterval(() => {
|
|
242
|
+
if (this.isConnected) {
|
|
243
|
+
this.ping();
|
|
244
|
+
}
|
|
245
|
+
}, this.config.heartbeatInterval);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Send ping message
|
|
250
|
+
*/
|
|
251
|
+
ping() {
|
|
252
|
+
const requestId = `ping-${Date.now()}-${Math.random()}`;
|
|
253
|
+
const request = {
|
|
254
|
+
sentTime: Date.now(),
|
|
255
|
+
resolve: null
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const promise = new Promise((resolve) => {
|
|
259
|
+
request.resolve = resolve;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
this.requestMap.set(requestId, request);
|
|
263
|
+
|
|
264
|
+
// Timeout if no response
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
if (this.requestMap.has(requestId)) {
|
|
267
|
+
this.stats.totalTimeouts++;
|
|
268
|
+
this.requestMap.delete(requestId);
|
|
269
|
+
this.emit('ping_timeout', { requestId });
|
|
270
|
+
}
|
|
271
|
+
}, 5000);
|
|
272
|
+
|
|
273
|
+
this.sendMessage({ type: 'ping', requestId });
|
|
274
|
+
return promise;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Send message through WebSocket
|
|
279
|
+
*/
|
|
280
|
+
sendMessage(data) {
|
|
281
|
+
if (!data || typeof data !== 'object') {
|
|
282
|
+
throw new Error('Invalid message data');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!this.isConnected) {
|
|
286
|
+
// Buffer message if not connected
|
|
287
|
+
this.bufferMessage(data);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
this.ws.send(JSON.stringify(data));
|
|
293
|
+
this.stats.totalMessagesSent++;
|
|
294
|
+
return true;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error('WebSocket send error:', error);
|
|
297
|
+
this.stats.totalErrors++;
|
|
298
|
+
this.bufferMessage(data);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Buffer message for sending when connected
|
|
305
|
+
*/
|
|
306
|
+
bufferMessage(data) {
|
|
307
|
+
if (this.messageBuffer.length >= this.config.maxBufferedMessages) {
|
|
308
|
+
console.warn('Message buffer full, dropping oldest message');
|
|
309
|
+
this.messageBuffer.shift();
|
|
310
|
+
}
|
|
311
|
+
this.messageBuffer.push(data);
|
|
312
|
+
this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Flush buffered messages
|
|
317
|
+
*/
|
|
318
|
+
flushMessageBuffer() {
|
|
319
|
+
if (this.messageBuffer.length === 0) return;
|
|
320
|
+
|
|
321
|
+
console.log(`Flushing ${this.messageBuffer.length} buffered messages`);
|
|
322
|
+
const messages = [...this.messageBuffer];
|
|
323
|
+
this.messageBuffer = [];
|
|
324
|
+
|
|
325
|
+
for (const message of messages) {
|
|
326
|
+
try {
|
|
327
|
+
this.ws.send(JSON.stringify(message));
|
|
328
|
+
this.stats.totalMessagesSent++;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('Error sending buffered message:', error);
|
|
331
|
+
this.bufferMessage(message);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.emit('buffer_flushed', { count: messages.length });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Subscribe to streaming session
|
|
340
|
+
*/
|
|
341
|
+
subscribeToSession(sessionId) {
|
|
342
|
+
return this.sendMessage({
|
|
343
|
+
type: 'subscribe',
|
|
344
|
+
sessionId,
|
|
345
|
+
timestamp: Date.now()
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Unsubscribe from streaming session
|
|
351
|
+
*/
|
|
352
|
+
unsubscribeFromSession(sessionId) {
|
|
353
|
+
return this.sendMessage({
|
|
354
|
+
type: 'unsubscribe',
|
|
355
|
+
sessionId,
|
|
356
|
+
timestamp: Date.now()
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Request session history
|
|
362
|
+
*/
|
|
363
|
+
requestSessionHistory(sessionId, limit = 1000, offset = 0) {
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
const requestId = `history-${Date.now()}-${Math.random()}`;
|
|
366
|
+
|
|
367
|
+
const timeout = setTimeout(() => {
|
|
368
|
+
this.requestMap.delete(requestId);
|
|
369
|
+
this.stats.totalTimeouts++;
|
|
370
|
+
reject(new Error('History request timeout'));
|
|
371
|
+
}, this.config.messageTimeout);
|
|
372
|
+
|
|
373
|
+
this.requestMap.set(requestId, {
|
|
374
|
+
type: 'history',
|
|
375
|
+
resolve: (data) => {
|
|
376
|
+
clearTimeout(timeout);
|
|
377
|
+
resolve(data);
|
|
378
|
+
},
|
|
379
|
+
reject
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
this.sendMessage({
|
|
383
|
+
type: 'request_history',
|
|
384
|
+
requestId,
|
|
385
|
+
sessionId,
|
|
386
|
+
limit,
|
|
387
|
+
offset,
|
|
388
|
+
timestamp: Date.now()
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Set connection state
|
|
395
|
+
*/
|
|
396
|
+
setConnectionState(state) {
|
|
397
|
+
this.connectionState = state;
|
|
398
|
+
this.emit('state_change', { state, timestamp: Date.now() });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Disconnect manually
|
|
403
|
+
*/
|
|
404
|
+
disconnect() {
|
|
405
|
+
this.isManuallyDisconnected = true;
|
|
406
|
+
this.reconnectCount = 0;
|
|
407
|
+
|
|
408
|
+
if (this.heartbeatTimer) {
|
|
409
|
+
clearTimeout(this.heartbeatTimer);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (this.ws) {
|
|
413
|
+
this.ws.close();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
this.messageBuffer = [];
|
|
417
|
+
this.requestMap.clear();
|
|
418
|
+
this.setConnectionState('disconnected');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get connection status
|
|
423
|
+
*/
|
|
424
|
+
getStatus() {
|
|
425
|
+
return {
|
|
426
|
+
isConnected: this.isConnected,
|
|
427
|
+
isConnecting: this.isConnecting,
|
|
428
|
+
connectionState: this.connectionState,
|
|
429
|
+
reconnectCount: this.reconnectCount,
|
|
430
|
+
bufferLength: this.messageBuffer.length,
|
|
431
|
+
stats: { ...this.stats }
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Add event listener
|
|
437
|
+
*/
|
|
438
|
+
on(event, callback) {
|
|
439
|
+
if (!this.listeners[event]) {
|
|
440
|
+
this.listeners[event] = [];
|
|
441
|
+
}
|
|
442
|
+
this.listeners[event].push(callback);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Remove event listener
|
|
447
|
+
*/
|
|
448
|
+
off(event, callback) {
|
|
449
|
+
if (!this.listeners[event]) return;
|
|
450
|
+
const index = this.listeners[event].indexOf(callback);
|
|
451
|
+
if (index > -1) {
|
|
452
|
+
this.listeners[event].splice(index, 1);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Emit event
|
|
458
|
+
*/
|
|
459
|
+
emit(event, data) {
|
|
460
|
+
if (!this.listeners[event]) return;
|
|
461
|
+
this.listeners[event].forEach((callback) => {
|
|
462
|
+
try {
|
|
463
|
+
callback(data);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error(`Listener error for event ${event}:`, error);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Cleanup resources
|
|
472
|
+
*/
|
|
473
|
+
destroy() {
|
|
474
|
+
this.disconnect();
|
|
475
|
+
this.listeners = {};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Export for use in browser
|
|
480
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
481
|
+
module.exports = WebSocketManager;
|
|
482
|
+
}
|