@xagent-ai/cli 1.3.6 → 1.4.0

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.
Files changed (69) hide show
  1. package/README.md +9 -0
  2. package/README_CN.md +9 -0
  3. package/dist/cli.js +26 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/mcp.d.ts +8 -1
  6. package/dist/mcp.d.ts.map +1 -1
  7. package/dist/mcp.js +53 -20
  8. package/dist/mcp.js.map +1 -1
  9. package/dist/sdk-output-adapter.d.ts +79 -0
  10. package/dist/sdk-output-adapter.d.ts.map +1 -1
  11. package/dist/sdk-output-adapter.js +118 -0
  12. package/dist/sdk-output-adapter.js.map +1 -1
  13. package/dist/session.d.ts +88 -1
  14. package/dist/session.d.ts.map +1 -1
  15. package/dist/session.js +351 -5
  16. package/dist/session.js.map +1 -1
  17. package/dist/slash-commands.d.ts.map +1 -1
  18. package/dist/slash-commands.js +3 -5
  19. package/dist/slash-commands.js.map +1 -1
  20. package/dist/smart-approval.d.ts.map +1 -1
  21. package/dist/smart-approval.js +1 -0
  22. package/dist/smart-approval.js.map +1 -1
  23. package/dist/system-prompt-generator.d.ts +15 -1
  24. package/dist/system-prompt-generator.d.ts.map +1 -1
  25. package/dist/system-prompt-generator.js +36 -27
  26. package/dist/system-prompt-generator.js.map +1 -1
  27. package/dist/team-manager/index.d.ts +6 -0
  28. package/dist/team-manager/index.d.ts.map +1 -0
  29. package/dist/team-manager/index.js +6 -0
  30. package/dist/team-manager/index.js.map +1 -0
  31. package/dist/team-manager/message-broker.d.ts +128 -0
  32. package/dist/team-manager/message-broker.d.ts.map +1 -0
  33. package/dist/team-manager/message-broker.js +638 -0
  34. package/dist/team-manager/message-broker.js.map +1 -0
  35. package/dist/team-manager/team-coordinator.d.ts +45 -0
  36. package/dist/team-manager/team-coordinator.d.ts.map +1 -0
  37. package/dist/team-manager/team-coordinator.js +887 -0
  38. package/dist/team-manager/team-coordinator.js.map +1 -0
  39. package/dist/team-manager/team-store.d.ts +49 -0
  40. package/dist/team-manager/team-store.d.ts.map +1 -0
  41. package/dist/team-manager/team-store.js +436 -0
  42. package/dist/team-manager/team-store.js.map +1 -0
  43. package/dist/team-manager/teammate-spawner.d.ts +86 -0
  44. package/dist/team-manager/teammate-spawner.d.ts.map +1 -0
  45. package/dist/team-manager/teammate-spawner.js +605 -0
  46. package/dist/team-manager/teammate-spawner.js.map +1 -0
  47. package/dist/team-manager/types.d.ts +164 -0
  48. package/dist/team-manager/types.d.ts.map +1 -0
  49. package/dist/team-manager/types.js +27 -0
  50. package/dist/team-manager/types.js.map +1 -0
  51. package/dist/tools.d.ts +41 -1
  52. package/dist/tools.d.ts.map +1 -1
  53. package/dist/tools.js +288 -32
  54. package/dist/tools.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/cli.ts +20 -0
  57. package/src/mcp.ts +64 -25
  58. package/src/sdk-output-adapter.ts +177 -0
  59. package/src/session.ts +423 -15
  60. package/src/slash-commands.ts +3 -7
  61. package/src/smart-approval.ts +1 -0
  62. package/src/system-prompt-generator.ts +59 -26
  63. package/src/team-manager/index.ts +5 -0
  64. package/src/team-manager/message-broker.ts +751 -0
  65. package/src/team-manager/team-coordinator.ts +1117 -0
  66. package/src/team-manager/team-store.ts +558 -0
  67. package/src/team-manager/teammate-spawner.ts +800 -0
  68. package/src/team-manager/types.ts +206 -0
  69. package/src/tools.ts +316 -33
@@ -0,0 +1,751 @@
1
+ import * as net from 'net';
2
+ import { EventEmitter } from 'events';
3
+ import { TeamMessage, MessageAck, MessageDeliveryInfo } from './types.js';
4
+ import crypto from 'crypto';
5
+
6
+ const generateId = () => crypto.randomUUID();
7
+
8
+ export interface MessageBrokerOptions {
9
+ port?: number;
10
+ host?: string;
11
+ ackTimeout?: number;
12
+ maxDeliveryInfoAge?: number; // Max age for delivery info cleanup
13
+ }
14
+
15
+ export interface ConnectedClient {
16
+ memberId: string;
17
+ socket: net.Socket;
18
+ joinedAt: number;
19
+ }
20
+
21
+ export interface PendingAck {
22
+ message: TeamMessage;
23
+ targetMemberId: string;
24
+ sentAt: number;
25
+ resolve: (info: MessageDeliveryInfo) => void;
26
+ reject: (error: Error) => void;
27
+ timer: NodeJS.Timeout;
28
+ }
29
+
30
+ // Default timeout for delivery info cleanup (5 minutes)
31
+ const DEFAULT_DELIVERY_INFO_MAX_AGE = 5 * 60 * 1000;
32
+ // Cleanup interval (1 minute)
33
+ const CLEANUP_INTERVAL = 60 * 1000;
34
+
35
+ export class MessageBroker extends EventEmitter {
36
+ private server: net.Server | null = null;
37
+ private clients: Map<string, ConnectedClient> = new Map();
38
+ private port: number;
39
+ private host: string;
40
+ private teamId: string;
41
+ private isRunning: boolean = false;
42
+ private pendingAcks: Map<string, PendingAck> = new Map();
43
+ private ackTimeout: number;
44
+ private deliveryInfo: Map<string, MessageDeliveryInfo> = new Map();
45
+ private maxDeliveryInfoAge: number;
46
+ private cleanupTimer: NodeJS.Timeout | null = null;
47
+
48
+ constructor(teamId: string, options: MessageBrokerOptions = {}) {
49
+ super();
50
+ this.teamId = teamId;
51
+ this.port = options.port || 0;
52
+ this.host = options.host || '127.0.0.1';
53
+ this.ackTimeout = options.ackTimeout || 30000;
54
+ this.maxDeliveryInfoAge = options.maxDeliveryInfoAge || DEFAULT_DELIVERY_INFO_MAX_AGE;
55
+ }
56
+
57
+ async start(): Promise<number> {
58
+ return new Promise((resolve, reject) => {
59
+ this.server = net.createServer((socket) => {
60
+ this.handleConnection(socket);
61
+ });
62
+
63
+ this.server.listen(this.port, this.host, () => {
64
+ const address = this.server?.address() as net.AddressInfo;
65
+ this.port = address.port;
66
+ this.isRunning = true;
67
+ this.emit('started', { port: this.port, host: this.host });
68
+
69
+ // Start cleanup timer
70
+ this.startCleanupTimer();
71
+
72
+ resolve(this.port);
73
+ });
74
+
75
+ this.server.on('error', (err) => {
76
+ this.emit('error', err);
77
+ reject(err);
78
+ });
79
+ });
80
+ }
81
+
82
+ async stop(): Promise<void> {
83
+ // Stop cleanup timer
84
+ this.stopCleanupTimer();
85
+
86
+ // Clean up all pending ACKs
87
+ for (const [_key, pending] of this.pendingAcks) {
88
+ clearTimeout(pending.timer);
89
+ pending.reject(new Error('Broker shutting down'));
90
+ }
91
+ this.pendingAcks.clear();
92
+
93
+ // Clean up delivery info
94
+ this.deliveryInfo.clear();
95
+
96
+ return new Promise((resolve) => {
97
+ for (const [_memberId, client] of this.clients) {
98
+ try {
99
+ client.socket.destroy();
100
+ } catch {
101
+ // ignore
102
+ }
103
+ }
104
+ this.clients.clear();
105
+
106
+ if (this.server) {
107
+ this.server.close(() => {
108
+ this.isRunning = false;
109
+ this.emit('stopped');
110
+ resolve();
111
+ });
112
+ } else {
113
+ resolve();
114
+ }
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Start periodic cleanup of stale delivery info
120
+ */
121
+ private startCleanupTimer(): void {
122
+ this.cleanupTimer = setInterval(() => {
123
+ this.cleanupStaleData();
124
+ }, CLEANUP_INTERVAL);
125
+ }
126
+
127
+ /**
128
+ * Stop cleanup timer
129
+ */
130
+ private stopCleanupTimer(): void {
131
+ if (this.cleanupTimer) {
132
+ clearInterval(this.cleanupTimer);
133
+ this.cleanupTimer = null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Clean up stale delivery info entries
139
+ */
140
+ private cleanupStaleData(): void {
141
+ const now = Date.now();
142
+ const staleThreshold = now - this.maxDeliveryInfoAge;
143
+
144
+ // Clean up stale delivery info
145
+ for (const [messageId, info] of this.deliveryInfo) {
146
+ if (info.sentAt < staleThreshold) {
147
+ this.deliveryInfo.delete(messageId);
148
+ }
149
+ }
150
+
151
+ // Clean up orphaned pending ACKs (shouldn't happen, but safety check)
152
+ for (const [key, pending] of this.pendingAcks) {
153
+ if (pending.sentAt < staleThreshold) {
154
+ clearTimeout(pending.timer);
155
+ this.pendingAcks.delete(key);
156
+
157
+ // Resolve with failed status instead of leaving hanging
158
+ const info: MessageDeliveryInfo = {
159
+ messageId: pending.message.messageId,
160
+ status: 'failed',
161
+ sentAt: pending.sentAt,
162
+ failedReason: 'Stale entry cleaned up'
163
+ };
164
+ pending.resolve(info);
165
+ }
166
+ }
167
+ }
168
+
169
+ private handleConnection(socket: net.Socket): void {
170
+ let memberId: string | null = null;
171
+ let buffer = '';
172
+
173
+ socket.on('data', (data) => {
174
+ buffer += data.toString();
175
+
176
+ const messages = buffer.split('\n');
177
+ buffer = messages.pop() || '';
178
+
179
+ for (const msgStr of messages) {
180
+ if (!msgStr.trim()) continue;
181
+
182
+ try {
183
+ const msg = JSON.parse(msgStr);
184
+
185
+ if (msg.type === 'register' && msg.memberId) {
186
+ memberId = msg.memberId;
187
+ this.clients.set(memberId as string, {
188
+ memberId: memberId as string,
189
+ socket,
190
+ joinedAt: Date.now()
191
+ });
192
+ this.emit('client:connected', { memberId });
193
+ this.sendToSocket(socket, { type: 'registered', memberId });
194
+ } else if (memberId) {
195
+ this.handleMessage(memberId, msg);
196
+ }
197
+ } catch (parseError) {
198
+ // Log parse errors for debugging but don't crash
199
+ this.emit('parse-error', { data: msgStr, error: parseError });
200
+ }
201
+ }
202
+ });
203
+
204
+ socket.on('close', () => {
205
+ if (memberId) {
206
+ this.clients.delete(memberId);
207
+ this.emit('client:disconnected', { memberId });
208
+
209
+ // Clean up pending ACKs for this member
210
+ this.cleanupPendingAcksForMember(memberId);
211
+ }
212
+ });
213
+
214
+ socket.on('error', (error) => {
215
+ if (memberId) {
216
+ this.clients.delete(memberId);
217
+ this.emit('client:error', { memberId, error });
218
+
219
+ // Clean up pending ACKs for this member
220
+ this.cleanupPendingAcksForMember(memberId);
221
+ }
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Clean up pending ACKs for a disconnected member
227
+ */
228
+ private cleanupPendingAcksForMember(memberId: string): void {
229
+ for (const [key, pending] of this.pendingAcks) {
230
+ if (pending.targetMemberId === memberId) {
231
+ clearTimeout(pending.timer);
232
+ this.pendingAcks.delete(key);
233
+
234
+ const info = this.deliveryInfo.get(pending.message.messageId);
235
+ if (info) {
236
+ info.status = 'failed';
237
+ info.failedReason = `Client ${memberId} disconnected`;
238
+ }
239
+
240
+ pending.reject(new Error(`Client ${memberId} disconnected`));
241
+ }
242
+ }
243
+ }
244
+
245
+ private handleMessage(fromMemberId: string, msg: any): void {
246
+ if (msg.type === 'ack') {
247
+ this.handleAck(fromMemberId, msg);
248
+ } else if (msg.type === 'direct' || msg.type === 'broadcast') {
249
+ this.routeMessage(fromMemberId, msg);
250
+ } else if (msg.type === 'task_update') {
251
+ this.broadcast({
252
+ messageId: generateId(),
253
+ teamId: this.teamId,
254
+ fromMemberId,
255
+ toMemberId: 'broadcast',
256
+ content: msg.content,
257
+ timestamp: Date.now(),
258
+ type: 'task_update',
259
+ read: false
260
+ }, fromMemberId);
261
+ }
262
+ }
263
+
264
+ private handleAck(fromMemberId: string, ack: MessageAck): void {
265
+ const pendingKey = `${ack.messageId}:${fromMemberId}`;
266
+ const pending = this.pendingAcks.get(pendingKey);
267
+
268
+ if (pending) {
269
+ clearTimeout(pending.timer);
270
+ this.pendingAcks.delete(pendingKey);
271
+
272
+ const info = this.deliveryInfo.get(ack.messageId);
273
+ if (info) {
274
+ info.status = 'acknowledged';
275
+ info.acknowledgedAt = ack.timestamp;
276
+ if (!info.acknowledgedBy) {
277
+ info.acknowledgedBy = [];
278
+ }
279
+ info.acknowledgedBy.push(fromMemberId);
280
+ }
281
+
282
+ this.emit('message:acknowledged', { messageId: ack.messageId, fromMemberId, status: ack.status });
283
+ pending.resolve(this.deliveryInfo.get(ack.messageId)!);
284
+ }
285
+ }
286
+
287
+ private routeMessage(fromMemberId: string, msg: any): void {
288
+ const message: TeamMessage = {
289
+ messageId: generateId(),
290
+ teamId: this.teamId,
291
+ fromMemberId,
292
+ toMemberId: msg.toMemberId || 'broadcast',
293
+ content: msg.content,
294
+ timestamp: Date.now(),
295
+ type: msg.type || 'direct',
296
+ read: false
297
+ };
298
+
299
+ if (msg.toMemberId === 'broadcast') {
300
+ this.broadcast(message, fromMemberId);
301
+ } else if (msg.toMemberId) {
302
+ this.sendToMember(msg.toMemberId, message);
303
+ }
304
+ }
305
+
306
+ private broadcast(message: TeamMessage, excludeMemberId?: string): void {
307
+ const msgStr = JSON.stringify(message) + '\n';
308
+
309
+ for (const [memberId, client] of this.clients) {
310
+ if (memberId !== excludeMemberId) {
311
+ try {
312
+ client.socket.write(msgStr);
313
+ } catch {
314
+ // socket might be closed
315
+ }
316
+ }
317
+ }
318
+
319
+ this.emit('message:broadcast', message);
320
+ }
321
+
322
+ private sendToMember(memberId: string, message: TeamMessage, requiresAck: boolean = true): Promise<MessageDeliveryInfo> {
323
+ return new Promise((resolve, reject) => {
324
+ const client = this.clients.get(memberId);
325
+ const info: MessageDeliveryInfo = {
326
+ messageId: message.messageId,
327
+ status: 'pending',
328
+ sentAt: Date.now()
329
+ };
330
+ this.deliveryInfo.set(message.messageId, info);
331
+
332
+ if (!client) {
333
+ info.status = 'failed';
334
+ info.failedReason = 'client not found';
335
+ this.emit('message:failed', { memberId, message, reason: 'client not found' });
336
+ reject(new Error(`Client ${memberId} not found`));
337
+ return;
338
+ }
339
+
340
+ try {
341
+ const msgWithAck = { ...message, requiresAck };
342
+ client.socket.write(JSON.stringify(msgWithAck) + '\n');
343
+ info.status = 'sent';
344
+ this.emit('message:sent', { memberId, message });
345
+
346
+ if (requiresAck) {
347
+ const pendingKey = `${message.messageId}:${memberId}`;
348
+ const timer = setTimeout(() => {
349
+ // Clean up on timeout
350
+ this.pendingAcks.delete(pendingKey);
351
+ info.status = 'failed';
352
+ info.failedReason = 'ack timeout';
353
+ this.emit('message:timeout', { messageId: message.messageId, memberId });
354
+ reject(new Error(`ACK timeout for message ${message.messageId} to ${memberId}`));
355
+ }, this.ackTimeout);
356
+
357
+ this.pendingAcks.set(pendingKey, {
358
+ message,
359
+ targetMemberId: memberId,
360
+ sentAt: Date.now(),
361
+ resolve,
362
+ reject,
363
+ timer
364
+ });
365
+ } else {
366
+ resolve(info);
367
+ }
368
+ } catch (err) {
369
+ info.status = 'failed';
370
+ info.failedReason = String(err);
371
+ this.emit('message:failed', { memberId, message, error: err });
372
+ reject(err);
373
+ }
374
+ });
375
+ }
376
+
377
+ private sendToSocket(socket: net.Socket, msg: object): void {
378
+ try {
379
+ socket.write(JSON.stringify(msg) + '\n');
380
+ } catch {
381
+ // ignore
382
+ }
383
+ }
384
+
385
+ sendMessage(fromMemberId: string, toMemberId: string | 'broadcast', content: string, type: TeamMessage['type'] = 'direct'): TeamMessage {
386
+ const message: TeamMessage = {
387
+ messageId: generateId(),
388
+ teamId: this.teamId,
389
+ fromMemberId,
390
+ toMemberId,
391
+ content,
392
+ timestamp: Date.now(),
393
+ type,
394
+ read: false,
395
+ requiresAck: true
396
+ };
397
+
398
+ if (toMemberId === 'broadcast') {
399
+ this.broadcast(message, fromMemberId);
400
+ } else {
401
+ this.sendToMember(toMemberId, message);
402
+ }
403
+
404
+ return message;
405
+ }
406
+
407
+ async sendMessageWithAck(
408
+ fromMemberId: string,
409
+ toMemberId: string | 'broadcast',
410
+ content: string,
411
+ type: TeamMessage['type'] = 'direct'
412
+ ): Promise<{ message: TeamMessage; deliveryInfo: MessageDeliveryInfo | MessageDeliveryInfo[] }> {
413
+ const message: TeamMessage = {
414
+ messageId: generateId(),
415
+ teamId: this.teamId,
416
+ fromMemberId,
417
+ toMemberId,
418
+ content,
419
+ timestamp: Date.now(),
420
+ type,
421
+ read: false,
422
+ requiresAck: true
423
+ };
424
+
425
+ if (toMemberId === 'broadcast') {
426
+ const results = await this.broadcastWithAck(message, fromMemberId);
427
+ return { message, deliveryInfo: results };
428
+ } else {
429
+ const info = await this.sendToMember(toMemberId, message, true);
430
+ return { message, deliveryInfo: info };
431
+ }
432
+ }
433
+
434
+ private async broadcastWithAck(message: TeamMessage, excludeMemberId?: string): Promise<MessageDeliveryInfo[]> {
435
+ const promises: Promise<MessageDeliveryInfo>[] = [];
436
+
437
+ for (const [memberId, _client] of this.clients) {
438
+ if (memberId !== excludeMemberId) {
439
+ promises.push(this.sendToMember(memberId, { ...message }, true));
440
+ }
441
+ }
442
+
443
+ return Promise.allSettled(promises).then(results =>
444
+ results.map(r => r.status === 'fulfilled' ? r.value : {
445
+ messageId: message.messageId,
446
+ status: 'failed' as const,
447
+ sentAt: Date.now(),
448
+ failedReason: r.status === 'rejected' ? String(r.reason) : undefined
449
+ })
450
+ );
451
+ }
452
+
453
+ getDeliveryInfo(messageId: string): MessageDeliveryInfo | undefined {
454
+ return this.deliveryInfo.get(messageId);
455
+ }
456
+
457
+ getPort(): number {
458
+ return this.port;
459
+ }
460
+
461
+ getConnectedMembers(): string[] {
462
+ return Array.from(this.clients.keys());
463
+ }
464
+
465
+ isClientConnected(memberId: string): boolean {
466
+ return this.clients.has(memberId);
467
+ }
468
+
469
+ isConnected(): boolean {
470
+ return this.isRunning;
471
+ }
472
+
473
+ /**
474
+ * Get stats about the broker state
475
+ */
476
+ getStats(): {
477
+ connectedClients: number;
478
+ pendingAcks: number;
479
+ deliveryInfoEntries: number;
480
+ } {
481
+ return {
482
+ connectedClients: this.clients.size,
483
+ pendingAcks: this.pendingAcks.size,
484
+ deliveryInfoEntries: this.deliveryInfo.size
485
+ };
486
+ }
487
+ }
488
+
489
+ export class MessageClient extends EventEmitter {
490
+ private socket: net.Socket | null = null;
491
+ private connected: boolean = false;
492
+ private buffer: string = '';
493
+ private reconnectAttempts: number = 0;
494
+ private maxReconnectAttempts: number = 5;
495
+ private reconnectDelay: number = 1000;
496
+ private reconnectTimer: NodeJS.Timeout | null = null;
497
+ private isShuttingDown: boolean = false;
498
+
499
+ constructor(
500
+ private teamId: string,
501
+ private memberId: string,
502
+ private port: number,
503
+ private host: string = '127.0.0.1'
504
+ ) {
505
+ super();
506
+ }
507
+
508
+ async connect(): Promise<void> {
509
+ if (this.isShuttingDown) {
510
+ throw new Error('Client is shutting down');
511
+ }
512
+
513
+ return new Promise((resolve, reject) => {
514
+ this.socket = new net.Socket();
515
+
516
+ const connectHandler = () => {
517
+ this.connected = true;
518
+ this.reconnectAttempts = 0;
519
+
520
+ // Disable idle timeout after connection - we want persistent connections
521
+ // for real-time team communication
522
+ this.socket?.setTimeout(0);
523
+
524
+ const registerMsg = JSON.stringify({
525
+ type: 'register',
526
+ memberId: this.memberId,
527
+ teamId: this.teamId
528
+ }) + '\n';
529
+
530
+ this.socket?.write(registerMsg);
531
+
532
+ this.emit('connected');
533
+ resolve();
534
+ };
535
+
536
+ const errorHandler = (err: Error) => {
537
+ if (!this.connected) {
538
+ reject(err);
539
+ } else {
540
+ this.emit('error', err);
541
+ this.handleDisconnect();
542
+ }
543
+ };
544
+
545
+ // Set initial connection timeout (10 seconds to establish connection)
546
+ this.socket.setTimeout(10000);
547
+
548
+ this.socket.connect(this.port, this.host, connectHandler);
549
+ this.socket.on('error', errorHandler);
550
+ this.socket.on('close', () => this.handleDisconnect());
551
+ // Remove timeout handler since we disable timeout after connection
552
+ this.socket.on('timeout', () => {
553
+ // This should only fire during initial connection phase
554
+ // After connection, timeout is disabled (setTimeout(0))
555
+ if (!this.connected) {
556
+ this.socket?.destroy();
557
+ reject(new Error('Connection timeout'));
558
+ }
559
+ });
560
+ this.socket.on('data', (data) => this.handleData(data));
561
+ });
562
+ }
563
+
564
+ private handleDisconnect(): void {
565
+ const wasConnected = this.connected;
566
+ this.connected = false;
567
+
568
+ if (wasConnected) {
569
+ this.emit('disconnected');
570
+ }
571
+
572
+ // Don't reconnect if we're shutting down
573
+ if (this.isShuttingDown) {
574
+ return;
575
+ }
576
+
577
+ // Attempt reconnection
578
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
579
+ this.reconnectAttempts++;
580
+
581
+ // Clear any existing reconnect timer
582
+ if (this.reconnectTimer) {
583
+ clearTimeout(this.reconnectTimer);
584
+ }
585
+
586
+ const delay = this.reconnectDelay * this.reconnectAttempts;
587
+ this.reconnectTimer = setTimeout(() => {
588
+ this.connect().catch(() => {
589
+ this.emit('reconnect:failed', { attempt: this.reconnectAttempts });
590
+ });
591
+ }, delay);
592
+
593
+ this.emit('reconnecting', { attempt: this.reconnectAttempts, delay });
594
+ } else {
595
+ this.emit('reconnect:exhausted', { attempts: this.reconnectAttempts });
596
+ }
597
+ }
598
+
599
+ private handleData(data: Buffer): void {
600
+ this.buffer += data.toString();
601
+
602
+ const messages = this.buffer.split('\n');
603
+ this.buffer = messages.pop() || '';
604
+
605
+ for (const msgStr of messages) {
606
+ if (!msgStr.trim()) continue;
607
+
608
+ try {
609
+ const msg = JSON.parse(msgStr);
610
+
611
+ if (msg.type === 'registered') {
612
+ this.emit('registered', msg);
613
+ } else {
614
+ if (msg.requiresAck && msg.messageId) {
615
+ this.sendAck(msg.messageId, 'received');
616
+ }
617
+ this.emit('message', msg);
618
+ }
619
+ } catch (parseError) {
620
+ this.emit('parse-error', { data: msgStr, error: parseError });
621
+ }
622
+ }
623
+ }
624
+
625
+ private sendAck(messageId: string, status: 'received' | 'processed', error?: string): void {
626
+ const ack: MessageAck = {
627
+ messageId,
628
+ fromMemberId: this.memberId,
629
+ status,
630
+ timestamp: Date.now(),
631
+ error
632
+ };
633
+ this.send({ type: 'ack', ...ack });
634
+ }
635
+
636
+ acknowledgeMessage(messageId: string, status: 'received' | 'processed' = 'processed', error?: string): void {
637
+ this.sendAck(messageId, status, error);
638
+ }
639
+
640
+ sendDirect(toMemberId: string, content: string): void {
641
+ this.send({
642
+ type: 'direct',
643
+ toMemberId,
644
+ content
645
+ });
646
+ }
647
+
648
+ broadcast(content: string): void {
649
+ this.send({
650
+ type: 'broadcast',
651
+ toMemberId: 'broadcast',
652
+ content
653
+ });
654
+ }
655
+
656
+ sendTaskUpdate(taskId: string, action: string, content: string): void {
657
+ this.send({
658
+ type: 'task_update',
659
+ taskId,
660
+ action,
661
+ content
662
+ });
663
+ }
664
+
665
+ private send(msg: object): void {
666
+ if (this.socket && this.connected) {
667
+ try {
668
+ this.socket.write(JSON.stringify(msg) + '\n');
669
+ } catch (error) {
670
+ this.emit('send:failed', { message: msg, error });
671
+ }
672
+ } else {
673
+ this.emit('send:failed', { message: msg, error: new Error('Not connected') });
674
+ }
675
+ }
676
+
677
+ async disconnect(): Promise<void> {
678
+ this.isShuttingDown = true;
679
+
680
+ // Clear any pending reconnect timer
681
+ if (this.reconnectTimer) {
682
+ clearTimeout(this.reconnectTimer);
683
+ this.reconnectTimer = null;
684
+ }
685
+
686
+ return new Promise((resolve) => {
687
+ if (this.socket) {
688
+ this.socket.destroy();
689
+ this.socket = null;
690
+ }
691
+ this.connected = false;
692
+ resolve();
693
+ });
694
+ }
695
+
696
+ isConnected(): boolean {
697
+ return this.connected;
698
+ }
699
+
700
+ /**
701
+ * Reset reconnection attempts (useful after successful operation)
702
+ */
703
+ resetReconnectAttempts(): void {
704
+ this.reconnectAttempts = 0;
705
+ }
706
+ }
707
+
708
+ const brokerInstances: Map<string, MessageBroker> = new Map();
709
+ let teammateClientInstance: MessageClient | null = null;
710
+
711
+ export function getMessageBroker(teamId: string): MessageBroker {
712
+ if (!brokerInstances.has(teamId)) {
713
+ brokerInstances.set(teamId, new MessageBroker(teamId));
714
+ }
715
+ return brokerInstances.get(teamId)!;
716
+ }
717
+
718
+ export function removeMessageBroker(teamId: string): void {
719
+ const broker = brokerInstances.get(teamId);
720
+ if (broker) {
721
+ broker.stop().catch(() => {});
722
+ }
723
+ brokerInstances.delete(teamId);
724
+ }
725
+
726
+ /**
727
+ * Set the persistent MessageClient for teammate process
728
+ * This is called once during teammate initialization
729
+ */
730
+ export function setTeammateClient(client: MessageClient): void {
731
+ teammateClientInstance = client;
732
+ }
733
+
734
+ /**
735
+ * Get the persistent MessageClient for teammate process
736
+ * Returns null if not a teammate process or not initialized
737
+ */
738
+ export function getTeammateClient(): MessageClient | null {
739
+ return teammateClientInstance;
740
+ }
741
+
742
+ /**
743
+ * Clear the persistent MessageClient for teammate process
744
+ * Called during cleanup to properly disconnect
745
+ */
746
+ export function clearTeammateClient(): void {
747
+ if (teammateClientInstance) {
748
+ teammateClientInstance.disconnect().catch(() => {});
749
+ teammateClientInstance = null;
750
+ }
751
+ }