thinkncollab-shell 0.0.1

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.
@@ -0,0 +1,385 @@
1
+ /**
2
+ * WebSocket Manager for ThinkNCollab Shell
3
+ * Handles all real-time communication
4
+ */
5
+
6
+ const { io } = require('socket.io-client');
7
+ const EventEmitter = require('events');
8
+
9
+ class WebSocketManager extends EventEmitter {
10
+ constructor(config = {}) {
11
+ super();
12
+
13
+ this.config = {
14
+ serverUrl: config.serverUrl || 'https://api.thinkncollab.com',
15
+ reconnectAttempts: config.reconnectAttempts || 5,
16
+ reconnectDelay: config.reconnectDelay || 1000,
17
+ autoConnect: config.autoConnect || false,
18
+ ...config
19
+ };
20
+
21
+ this.socket = null;
22
+ this.connected = false;
23
+ this.userId = null;
24
+ this.roomId = null;
25
+ this.reconnectCount = 0;
26
+ this.pendingMessages = [];
27
+ this.messageQueue = [];
28
+
29
+ // Bind methods
30
+ this.connect = this.connect.bind(this);
31
+ this.disconnect = this.disconnect.bind(this);
32
+ this.joinRoom = this.joinRoom.bind(this);
33
+ this.leaveRoom = this.leaveRoom.bind(this);
34
+ this.sendMessage = this.sendMessage.bind(this);
35
+ this.sendNotification = this.sendNotification.bind(this);
36
+ }
37
+
38
+ /**
39
+ * Connect to WebSocket server
40
+ */
41
+ connect(authToken = null) {
42
+ return new Promise((resolve, reject) => {
43
+ try {
44
+ const options = {
45
+ transports: ['websocket', 'polling'],
46
+ reconnection: true,
47
+ reconnectionAttempts: this.config.reconnectAttempts,
48
+ reconnectionDelay: this.config.reconnectDelay,
49
+ query: {}
50
+ };
51
+
52
+ if (authToken) {
53
+ options.query.token = authToken;
54
+ }
55
+
56
+ this.socket = io(this.config.serverUrl, options);
57
+
58
+ // Connection established
59
+ this.socket.on('connect', () => {
60
+ this.connected = true;
61
+ this.reconnectCount = 0;
62
+ this.userId = this.socket.id;
63
+
64
+ // Process any pending messages
65
+ this.processPendingMessages();
66
+
67
+ this.emit('connected', { socketId: this.socket.id });
68
+ resolve({ success: true, socketId: this.socket.id });
69
+ });
70
+
71
+ // Connection error
72
+ this.socket.on('connect_error', (error) => {
73
+ this.emit('error', { type: 'connection', error: error.message });
74
+
75
+ if (this.reconnectCount >= this.config.reconnectAttempts) {
76
+ reject(new Error(`Failed to connect after ${this.config.reconnectAttempts} attempts`));
77
+ }
78
+ this.reconnectCount++;
79
+ });
80
+
81
+ // Disconnection
82
+ this.socket.on('disconnect', (reason) => {
83
+ this.connected = false;
84
+ this.emit('disconnected', { reason });
85
+
86
+ if (reason === 'io server disconnect') {
87
+ // Server disconnected, don't reconnect
88
+ this.socket = null;
89
+ }
90
+ });
91
+
92
+ // Reconnection
93
+ this.socket.on('reconnect', (attemptNumber) => {
94
+ this.connected = true;
95
+ this.emit('reconnected', { attemptNumber });
96
+
97
+ // Rejoin room if we were in one
98
+ if (this.roomId) {
99
+ this.joinRoom(this.roomId);
100
+ }
101
+ });
102
+
103
+ // Custom event handlers
104
+ this.setupEventHandlers();
105
+
106
+ } catch (error) {
107
+ reject(error);
108
+ }
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Setup custom event handlers
114
+ */
115
+ setupEventHandlers() {
116
+ // Room events
117
+ this.socket.on('room:joined', (data) => {
118
+ this.roomId = data.roomId;
119
+ this.emit('roomJoined', data);
120
+ });
121
+
122
+ this.socket.on('room:left', (data) => {
123
+ this.roomId = null;
124
+ this.emit('roomLeft', data);
125
+ });
126
+
127
+ this.socket.on('room:participants', (data) => {
128
+ this.emit('roomParticipants', data);
129
+ });
130
+
131
+ // Message events
132
+ this.socket.on('message:new', (data) => {
133
+ this.emit('message', data);
134
+ });
135
+
136
+ this.socket.on('message:history', (data) => {
137
+ this.emit('messageHistory', data);
138
+ });
139
+
140
+ // User events
141
+ this.socket.on('user:joined', (data) => {
142
+ this.emit('userJoined', data);
143
+ });
144
+
145
+ this.socket.on('user:left', (data) => {
146
+ this.emit('userLeft', data);
147
+ });
148
+
149
+ this.socket.on('user:typing', (data) => {
150
+ this.emit('userTyping', data);
151
+ });
152
+
153
+ // Notification events
154
+ this.socket.on('notification', (data) => {
155
+ this.emit('notification', data);
156
+ });
157
+
158
+ // Terminal sharing events
159
+ this.socket.on('terminal:output', (data) => {
160
+ this.emit('terminalOutput', data);
161
+ });
162
+
163
+ this.socket.on('terminal:command', (data) => {
164
+ this.emit('terminalCommand', data);
165
+ });
166
+
167
+ // Error events
168
+ this.socket.on('error', (data) => {
169
+ this.emit('serverError', data);
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Disconnect from WebSocket server
175
+ */
176
+ disconnect() {
177
+ if (this.socket) {
178
+ this.socket.disconnect();
179
+ this.socket = null;
180
+ this.connected = false;
181
+ this.userId = null;
182
+ this.roomId = null;
183
+ this.emit('disconnected', { reason: 'manual' });
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Join a collaboration room
189
+ */
190
+ joinRoom(roomId, password = null) {
191
+ return new Promise((resolve, reject) => {
192
+ if (!this.connected || !this.socket) {
193
+ reject(new Error('WebSocket not connected'));
194
+ return;
195
+ }
196
+
197
+ const payload = { roomId };
198
+ if (password) payload.password = password;
199
+
200
+ this.socket.emit('room:join', payload);
201
+
202
+ // Wait for response
203
+ const timeout = setTimeout(() => {
204
+ reject(new Error('Join room timeout'));
205
+ }, 5000);
206
+
207
+ this.socket.once('room:joined', (data) => {
208
+ clearTimeout(timeout);
209
+ this.roomId = data.roomId;
210
+ resolve(data);
211
+ });
212
+
213
+ this.socket.once('room:error', (data) => {
214
+ clearTimeout(timeout);
215
+ reject(new Error(data.message));
216
+ });
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Leave current room
222
+ */
223
+ leaveRoom() {
224
+ if (!this.connected || !this.socket || !this.roomId) {
225
+ return Promise.reject(new Error('Not in a room'));
226
+ }
227
+
228
+ return new Promise((resolve) => {
229
+ this.socket.emit('room:leave', { roomId: this.roomId });
230
+ this.roomId = null;
231
+ resolve({ success: true });
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Send message to current room
237
+ */
238
+ sendMessage(message, type = 'text') {
239
+ if (!this.connected || !this.socket) {
240
+ // Queue message for later
241
+ this.messageQueue.push({ message, type });
242
+ return Promise.resolve({ queued: true });
243
+ }
244
+
245
+ if (!this.roomId) {
246
+ return Promise.reject(new Error('Not in a room'));
247
+ }
248
+
249
+ return new Promise((resolve) => {
250
+ const payload = {
251
+ roomId: this.roomId,
252
+ message,
253
+ type,
254
+ timestamp: new Date().toISOString()
255
+ };
256
+
257
+ this.socket.emit('message:send', payload);
258
+ resolve({ success: true, id: `msg_${Date.now()}` });
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Send notification to room/users
264
+ */
265
+ sendNotification(notification, target = 'room') {
266
+ if (!this.connected || !this.socket) {
267
+ return Promise.reject(new Error('WebSocket not connected'));
268
+ }
269
+
270
+ const payload = {
271
+ type: notification.type || 'info',
272
+ title: notification.title,
273
+ message: notification.message,
274
+ data: notification.data,
275
+ target: target === 'room' ? this.roomId : target,
276
+ timestamp: new Date().toISOString()
277
+ };
278
+
279
+ this.socket.emit('notification:send', payload);
280
+
281
+ return Promise.resolve({ success: true });
282
+ }
283
+
284
+ /**
285
+ * Share terminal output with room
286
+ */
287
+ shareTerminalOutput(command, output) {
288
+ if (!this.connected || !this.socket || !this.roomId) {
289
+ return;
290
+ }
291
+
292
+ this.socket.emit('terminal:share', {
293
+ roomId: this.roomId,
294
+ command,
295
+ output,
296
+ timestamp: new Date().toISOString()
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Send typing indicator
302
+ */
303
+ sendTyping(isTyping = true) {
304
+ if (!this.connected || !this.socket || !this.roomId) {
305
+ return;
306
+ }
307
+
308
+ this.socket.emit('user:typing', {
309
+ roomId: this.roomId,
310
+ isTyping
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Get room participants
316
+ */
317
+ getParticipants() {
318
+ if (!this.connected || !this.socket || !this.roomId) {
319
+ return Promise.reject(new Error('Not connected to a room'));
320
+ }
321
+
322
+ return new Promise((resolve) => {
323
+ this.socket.emit('room:getParticipants', { roomId: this.roomId });
324
+
325
+ this.socket.once('room:participants', (data) => {
326
+ resolve(data.participants);
327
+ });
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Get message history
333
+ */
334
+ getMessageHistory(limit = 50) {
335
+ if (!this.connected || !this.socket || !this.roomId) {
336
+ return Promise.reject(new Error('Not connected to a room'));
337
+ }
338
+
339
+ return new Promise((resolve) => {
340
+ this.socket.emit('message:getHistory', {
341
+ roomId: this.roomId,
342
+ limit
343
+ });
344
+
345
+ this.socket.once('message:history', (data) => {
346
+ resolve(data.messages);
347
+ });
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Process pending messages after reconnection
353
+ */
354
+ processPendingMessages() {
355
+ if (this.messageQueue.length > 0) {
356
+ this.messageQueue.forEach(item => {
357
+ this.sendMessage(item.message, item.type);
358
+ });
359
+ this.messageQueue = [];
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Check connection status
365
+ */
366
+ isConnected() {
367
+ return this.connected && this.socket && this.socket.connected;
368
+ }
369
+
370
+ /**
371
+ * Get current room info
372
+ */
373
+ getCurrentRoom() {
374
+ return this.roomId;
375
+ }
376
+
377
+ /**
378
+ * Get socket ID
379
+ */
380
+ getSocketId() {
381
+ return this.userId;
382
+ }
383
+ }
384
+
385
+ module.exports = WebSocketManager;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Notification System for ThinkNCollab Shell
3
+ */
4
+
5
+ const EventEmitter = require('events');
6
+ const chalk = require('chalk');
7
+
8
+ class NotificationManager extends EventEmitter {
9
+ constructor(options = {}) {
10
+ super();
11
+
12
+ this.options = {
13
+ sound: options.sound || false,
14
+ desktop: options.desktop || false,
15
+ ...options
16
+ };
17
+
18
+ this.notifications = [];
19
+ this.maxNotifications = 100;
20
+ this.unreadCount = 0;
21
+ }
22
+
23
+ /**
24
+ * Show a notification
25
+ */
26
+ show(notification) {
27
+ const notif = {
28
+ id: `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
29
+ type: notification.type || 'info',
30
+ title: notification.title || 'Notification',
31
+ message: notification.message,
32
+ timestamp: new Date(),
33
+ read: false,
34
+ data: notification.data || {}
35
+ };
36
+
37
+ this.notifications.unshift(notif);
38
+ this.unreadCount++;
39
+
40
+ // Trim old notifications
41
+ if (this.notifications.length > this.maxNotifications) {
42
+ this.notifications.pop();
43
+ }
44
+
45
+ // Emit event
46
+ this.emit('notification', notif);
47
+
48
+ // Show in console
49
+ this.showInConsole(notif);
50
+
51
+ // Desktop notification
52
+ if (this.options.desktop) {
53
+ this.showDesktopNotification(notif);
54
+ }
55
+
56
+ // Sound
57
+ if (this.options.sound) {
58
+ this.playSound();
59
+ }
60
+
61
+ return notif;
62
+ }
63
+
64
+ /**
65
+ * Show notification in console
66
+ */
67
+ showInConsole(notification) {
68
+ let color = chalk.blue;
69
+ let symbol = 'ℹ';
70
+
71
+ switch (notification.type) {
72
+ case 'success':
73
+ color = chalk.green;
74
+ symbol = '✓';
75
+ break;
76
+ case 'warning':
77
+ color = chalk.yellow;
78
+ symbol = '⚠';
79
+ break;
80
+ case 'error':
81
+ color = chalk.red;
82
+ symbol = '✗';
83
+ break;
84
+ case 'info':
85
+ default:
86
+ color = chalk.blue;
87
+ symbol = 'ℹ';
88
+ break;
89
+ }
90
+
91
+ console.log(color(`${symbol} ${notification.title}: ${notification.message}`));
92
+ }
93
+
94
+ /**
95
+ * Show desktop notification (if supported)
96
+ */
97
+ showDesktopNotification(notification) {
98
+ // Check if running in Electron
99
+ if (process.type === 'renderer' || process.type === 'browser') {
100
+ try {
101
+ const { Notification } = require('electron');
102
+ new Notification({
103
+ title: notification.title,
104
+ body: notification.message,
105
+ silent: !this.options.sound
106
+ }).show();
107
+ } catch (error) {
108
+ // Electron not available
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Play notification sound
115
+ */
116
+ playSound() {
117
+ // TODO: Implement sound playing
118
+ // This would use 'sound-play' or similar package
119
+ }
120
+
121
+ /**
122
+ * Mark notification as read
123
+ */
124
+ markAsRead(id) {
125
+ const notif = this.notifications.find(n => n.id === id);
126
+ if (notif && !notif.read) {
127
+ notif.read = true;
128
+ this.unreadCount--;
129
+ this.emit('read', notif);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Mark all as read
135
+ */
136
+ markAllAsRead() {
137
+ this.notifications.forEach(n => {
138
+ if (!n.read) {
139
+ n.read = true;
140
+ }
141
+ });
142
+ this.unreadCount = 0;
143
+ this.emit('allRead');
144
+ }
145
+
146
+ /**
147
+ * Get unread notifications
148
+ */
149
+ getUnread() {
150
+ return this.notifications.filter(n => !n.read);
151
+ }
152
+
153
+ /**
154
+ * Get all notifications
155
+ */
156
+ getAll() {
157
+ return this.notifications;
158
+ }
159
+
160
+ /**
161
+ * Clear all notifications
162
+ */
163
+ clear() {
164
+ this.notifications = [];
165
+ this.unreadCount = 0;
166
+ this.emit('cleared');
167
+ }
168
+
169
+ /**
170
+ * Create success notification
171
+ */
172
+ success(title, message, data = {}) {
173
+ return this.show({ type: 'success', title, message, data });
174
+ }
175
+
176
+ /**
177
+ * Create error notification
178
+ */
179
+ error(title, message, data = {}) {
180
+ return this.show({ type: 'error', title, message, data });
181
+ }
182
+
183
+ /**
184
+ * Create warning notification
185
+ */
186
+ warning(title, message, data = {}) {
187
+ return this.show({ type: 'warning', title, message, data });
188
+ }
189
+
190
+ /**
191
+ * Create info notification
192
+ */
193
+ info(title, message, data = {}) {
194
+ return this.show({ type: 'info', title, message, data });
195
+ }
196
+ }
197
+
198
+ module.exports = NotificationManager;