thinkncollab-shell 0.0.1 → 0.0.3

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * WebSocket Manager for ThinkNCollab Shell
3
- * Handles all real-time communication
3
+ * Uses thinknsh: prefixed events for terminal-specific communication
4
4
  */
5
5
 
6
6
  const { io } = require('socket.io-client');
@@ -9,377 +9,315 @@ const EventEmitter = require('events');
9
9
  class WebSocketManager extends EventEmitter {
10
10
  constructor(config = {}) {
11
11
  super();
12
-
12
+
13
13
  this.config = {
14
- serverUrl: config.serverUrl || 'https://api.thinkncollab.com',
14
+ serverUrl: config.serverUrl || 'https://thinkncollab.com',
15
15
  reconnectAttempts: config.reconnectAttempts || 5,
16
- reconnectDelay: config.reconnectDelay || 1000,
17
- autoConnect: config.autoConnect || false,
16
+ reconnectDelay: config.reconnectDelay || 1000,
18
17
  ...config
19
18
  };
20
-
21
- this.socket = null;
22
- this.connected = false;
23
- this.userId = null;
24
- this.roomId = null;
19
+
20
+ this.socket = null;
21
+ this.connected = false;
22
+ this.currentUser = null; // { userId, name, userType }
23
+ this.roomId = null;
25
24
  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);
25
+ this.messageQueue = [];
36
26
  }
37
-
38
- /**
39
- * Connect to WebSocket server
40
- */
27
+
28
+ // ─── Connect ──────────────────────────────────────────────────────────────
29
+
41
30
  connect(authToken = null) {
42
31
  return new Promise((resolve, reject) => {
43
32
  try {
44
- const options = {
45
- transports: ['websocket', 'polling'],
46
- reconnection: true,
33
+ // Connect to /thinknsh namespace — isolated from web
34
+ this.socket = io(this.config.serverUrl + '/thinknsh', {
35
+ transports: ['websocket', 'polling'],
36
+ reconnection: true,
47
37
  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
38
+ reconnectionDelay: this.config.reconnectDelay,
39
+ auth: authToken ? { token: authToken } : {},
40
+ });
41
+
42
+ const timeout = setTimeout(() => reject(new Error('Connection timeout')), 10000);
43
+
59
44
  this.socket.on('connect', () => {
60
- this.connected = true;
45
+ clearTimeout(timeout);
46
+ this.connected = true;
61
47
  this.reconnectCount = 0;
62
- this.userId = this.socket.id;
63
-
64
- // Process any pending messages
48
+ this._authToken = authToken;
49
+
50
+ // Announce terminal to backend — join personal notification room
51
+ if (this.currentUser) {
52
+ console.log('🖥️ Emitting thinknsh:connected for', this.currentUser.userId);
53
+ this.socket.emit('thinknsh:connected', {
54
+ userId: this.currentUser.userId,
55
+ userName: this.currentUser.name,
56
+ shellToken: authToken,
57
+ });
58
+ }
59
+
65
60
  this.processPendingMessages();
66
-
67
61
  this.emit('connected', { socketId: this.socket.id });
68
62
  resolve({ success: true, socketId: this.socket.id });
69
63
  });
70
-
71
- // Connection error
64
+
72
65
  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`));
66
+ clearTimeout(timeout);
67
+ if (this.reconnectCount === 0) {
68
+ reject(new Error(`Cannot connect to ${this.config.serverUrl}`));
77
69
  }
78
70
  this.reconnectCount++;
71
+ this.emit('error', { message: error.message });
79
72
  });
80
-
81
- // Disconnection
73
+
82
74
  this.socket.on('disconnect', (reason) => {
83
75
  this.connected = false;
84
76
  this.emit('disconnected', { reason });
85
-
86
- if (reason === 'io server disconnect') {
87
- // Server disconnected, don't reconnect
88
- this.socket = null;
89
- }
90
77
  });
91
-
92
- // Reconnection
78
+
93
79
  this.socket.on('reconnect', (attemptNumber) => {
94
80
  this.connected = true;
95
81
  this.emit('reconnected', { attemptNumber });
96
-
97
- // Rejoin room if we were in one
98
- if (this.roomId) {
99
- this.joinRoom(this.roomId);
82
+ // Rejoin room after reconnect
83
+ if (this.roomId && this.currentUser) {
84
+ this._emitJoin(this.roomId);
100
85
  }
101
86
  });
102
-
103
- // Custom event handlers
104
- this.setupEventHandlers();
105
-
87
+
88
+ this.setupEventHandlers(authToken);
89
+
106
90
  } catch (error) {
107
91
  reject(error);
108
92
  }
109
93
  });
110
94
  }
111
-
112
- /**
113
- * Setup custom event handlers
114
- */
115
- setupEventHandlers() {
116
- // Room events
117
- this.socket.on('room:joined', (data) => {
95
+
96
+ // ─── thinknsh: event handlers ─────────────────────────────────────────────
97
+
98
+ setupEventHandlers(authToken) {
99
+
100
+ // Terminal ready confirmed by server
101
+ this.socket.on('thinknsh:ready', (data) => {
102
+ this.emit('ready', data);
103
+ });
104
+
105
+ // Room join confirmed
106
+ this.socket.on('thinknsh:joined', (data) => {
118
107
  this.roomId = data.roomId;
119
108
  this.emit('roomJoined', data);
120
109
  });
121
-
122
- this.socket.on('room:left', (data) => {
123
- this.roomId = null;
124
- this.emit('roomLeft', data);
110
+
111
+ // Incoming message from terminal or web
112
+ this.socket.on('thinknsh:message', (data) => {
113
+ this.emit('message', {
114
+ username: data.name,
115
+ userId: data.userId,
116
+ message: data.message,
117
+ source: data.source || 'web',
118
+ timestamp: data.timestamp,
119
+ });
125
120
  });
126
-
127
- this.socket.on('room:participants', (data) => {
128
- this.emit('roomParticipants', data);
121
+
122
+ // Also listen to standard chat-message (from web users)
123
+ this.socket.on('chat-message', (data) => {
124
+ this.emit('message', {
125
+ username: data.name,
126
+ userId: data.userId,
127
+ message: data.message,
128
+ source: data.source || 'web',
129
+ timestamp: data.timestamp,
130
+ });
129
131
  });
130
-
131
- // Message events
132
- this.socket.on('message:new', (data) => {
133
- this.emit('message', data);
132
+
133
+ // User joined from terminal
134
+ this.socket.on('thinknsh:userJoined', (data) => {
135
+ this.emit('userJoined', { username: data.name, source: 'shell' });
134
136
  });
135
-
136
- this.socket.on('message:history', (data) => {
137
- this.emit('messageHistory', data);
137
+
138
+ // User joined from web
139
+ this.socket.on('user-joined', (name) => {
140
+ this.emit('userJoined', { username: name, source: 'web' });
138
141
  });
139
-
140
- // User events
141
- this.socket.on('user:joined', (data) => {
142
- this.emit('userJoined', data);
142
+
143
+ // User left
144
+ this.socket.on('user-left', (name) => {
145
+ this.emit('userLeft', { username: name });
143
146
  });
144
-
145
- this.socket.on('user:left', (data) => {
146
- this.emit('userLeft', data);
147
+
148
+ // User list updated
149
+ this.socket.on('user-list', (users) => {
150
+ this.emit('userList', { users });
147
151
  });
148
-
149
- this.socket.on('user:typing', (data) => {
150
- this.emit('userTyping', data);
152
+
153
+ // Notifications from NotificationEngine
154
+ this.socket.on('new-notification', (notification) => {
155
+ this.emit('notification', {
156
+ type: notification.type || 'info',
157
+ title: notification.type || 'Notification',
158
+ message: notification.message || '',
159
+ meta: notification.meta || {},
160
+ });
151
161
  });
152
-
153
- // Notification events
154
- this.socket.on('notification', (data) => {
155
- this.emit('notification', data);
162
+
163
+ // Terminal status (online/offline) — website sends this
164
+ this.socket.on('thinknsh:status', (data) => {
165
+ this.emit('shellStatus', data);
156
166
  });
157
-
158
- // Terminal sharing events
159
- this.socket.on('terminal:output', (data) => {
160
- this.emit('terminalOutput', data);
167
+
168
+ // ── new-notification from NotificationEngine → forward to TCP window ──
169
+ this.socket.on('new-notification', (data) => {
170
+ // This goes to shell.js pushNotification → TCP → notification window
171
+ this.emit('notification', {
172
+ type: data.type || 'notification',
173
+ level: data.type || 'info',
174
+ title: data.type || 'Notification',
175
+ message: data.message || '',
176
+ taskTitle: data.taskTitle || data.meta?.taskTitle || '',
177
+ assignedBy: data.assignedBy || data.meta?.assignedBy || '',
178
+ });
161
179
  });
162
-
163
- this.socket.on('terminal:command', (data) => {
164
- this.emit('terminalCommand', data);
180
+
181
+ // Notification list response
182
+ this.socket.on('thinknsh:notifications', (data) => {
183
+ this.emit('notificationList', data.notifications);
165
184
  });
166
-
167
- // Error events
185
+
186
+ // Error from server
187
+ this.socket.on('thinknsh:error', (data) => {
188
+ this.emit('serverError', data);
189
+ });
190
+
168
191
  this.socket.on('error', (data) => {
169
192
  this.emit('serverError', data);
170
193
  });
171
194
  }
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' });
195
+
196
+ // ─── Set user (call after login) ──────────────────────────────────────────
197
+
198
+ setUser(user, token = null) {
199
+ this.currentUser = user;
200
+ // If already connected, announce now
201
+ if (this.connected && this.socket) {
202
+ this.socket.emit('thinknsh:connected', {
203
+ userId: user.userId,
204
+ shellToken: token,
205
+ });
184
206
  }
185
207
  }
186
-
187
- /**
188
- * Join a collaboration room
189
- */
190
- joinRoom(roomId, password = null) {
208
+
209
+ // ─── Join room ────────────────────────────────────────────────────────────
210
+
211
+ joinRoom(roomId) {
191
212
  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) => {
213
+ if (!this.connected || !this.socket) return reject(new Error('Not connected'));
214
+ if (!this.currentUser) return reject(new Error('Not logged in'));
215
+
216
+ const timeout = setTimeout(() => reject(new Error('Join timeout')), 5000);
217
+
218
+ this.socket.once('thinknsh:joined', (data) => {
208
219
  clearTimeout(timeout);
209
220
  this.roomId = data.roomId;
210
221
  resolve(data);
211
222
  });
212
-
213
- this.socket.once('room:error', (data) => {
214
- clearTimeout(timeout);
215
- reject(new Error(data.message));
216
- });
223
+
224
+ this._emitJoin(roomId);
217
225
  });
218
226
  }
219
-
220
- /**
221
- * Leave current room
222
- */
227
+
228
+ _emitJoin(roomId) {
229
+ this.socket.emit('thinknsh:join', {
230
+ roomId,
231
+ userId: this.currentUser.userId,
232
+ name: this.currentUser.name,
233
+ userType: this.currentUser.userType || 'User',
234
+ });
235
+ }
236
+
237
+ // ─── Leave room ───────────────────────────────────────────────────────────
238
+
223
239
  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 });
240
+ if (this.socket && this.roomId) {
241
+ this.socket.emit('leave-room', { roomId: this.roomId });
230
242
  this.roomId = null;
231
- resolve({ success: true });
232
- });
243
+ }
244
+ return Promise.resolve({ success: true });
233
245
  }
234
-
235
- /**
236
- * Send message to current room
237
- */
238
- sendMessage(message, type = 'text') {
246
+
247
+ // ─── Send message ─────────────────────────────────────────────────────────
248
+
249
+ sendMessage(message) {
239
250
  if (!this.connected || !this.socket) {
240
- // Queue message for later
241
- this.messageQueue.push({ message, type });
251
+ this.messageQueue.push(message);
242
252
  return Promise.resolve({ queued: true });
243
253
  }
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()}` });
254
+ if (!this.roomId) return Promise.reject(new Error('Not in a room — run: join <room-id>'));
255
+ if (!this.currentUser) return Promise.reject(new Error('Not logged in — run: login'));
256
+
257
+ this.socket.emit('thinknsh:message', {
258
+ roomId: this.roomId,
259
+ userId: this.currentUser.userId,
260
+ name: this.currentUser.name,
261
+ userType: this.currentUser.userType || 'User',
262
+ message,
259
263
  });
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
-
264
+
281
265
  return Promise.resolve({ success: true });
282
266
  }
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
- });
267
+
268
+ // ─── Get unread notifications ─────────────────────────────────────────────
269
+
270
+ getNotifications() {
271
+ if (!this.connected || !this.currentUser) return;
272
+ this.socket.emit('thinknsh:getNotifications', { userId: this.currentUser.userId });
298
273
  }
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
274
+
275
+ markNotificationsRead(notificationIds = []) {
276
+ if (!this.connected || !this.currentUser) return;
277
+ this.socket.emit('thinknsh:markRead', {
278
+ userId: this.currentUser.userId,
279
+ notificationIds,
311
280
  });
312
281
  }
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
- });
282
+
283
+ // ─── Helpers ──────────────────────────────────────────────────────────────
284
+
285
+ joinUserRoom(userId) {
286
+ if (!this.socket || !userId) return;
287
+ this.socket.emit('thinknsh:connected', {
288
+ userId,
289
+ userName: this.currentUser?.name || '',
290
+ shellToken: this._token || null,
328
291
  });
329
292
  }
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
- });
293
+
294
+ sendTyping() {} // add later if needed
295
+
296
+ ping() {
297
+ if (this.connected && this.socket) this.socket.emit('ping');
349
298
  }
350
-
351
- /**
352
- * Process pending messages after reconnection
353
- */
299
+
354
300
  processPendingMessages() {
355
- if (this.messageQueue.length > 0) {
356
- this.messageQueue.forEach(item => {
357
- this.sendMessage(item.message, item.type);
358
- });
301
+ if (this.messageQueue.length > 0 && this.roomId && this.currentUser) {
302
+ this.messageQueue.forEach(msg => this.sendMessage(msg));
359
303
  this.messageQueue = [];
360
304
  }
361
305
  }
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;
306
+
307
+ disconnect() {
308
+ if (this.socket) {
309
+ this.socket.disconnect();
310
+ this.socket = null;
311
+ this.connected = false;
312
+ this.currentUser = null;
313
+ this.roomId = null;
314
+ }
382
315
  }
316
+
317
+ isConnected() { return this.connected && !!this.socket?.connected; }
318
+ getCurrentRoom() { return this.roomId; }
319
+ getSocketId() { return this.socket?.id || null; }
320
+ getParticipants() { return Promise.resolve([]); }
383
321
  }
384
322
 
385
323
  module.exports = WebSocketManager;