@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,1117 @@
1
+ import { TeamStore, getTeamStore } from './team-store.js';
2
+ import { TeammateSpawner, getTeammateSpawner } from './teammate-spawner.js';
3
+ import { MessageBroker, MessageClient, getMessageBroker, removeMessageBroker, getTeammateClient } from './message-broker.js';
4
+ import {
5
+ TeamToolParams,
6
+ TeamMember,
7
+ TeamTask,
8
+ MessageDeliveryInfo,
9
+ LEAD_PERMISSIONS,
10
+ TEAMMATE_PERMISSIONS,
11
+ MemberPermissions,
12
+ } from './types.js';
13
+ import { colors, icons } from '../theme.js';
14
+
15
+ export class TeamCoordinator {
16
+ private store: TeamStore;
17
+ private spawner: TeammateSpawner;
18
+ private brokers: Map<string, MessageBroker> = new Map();
19
+
20
+ constructor() {
21
+ this.store = getTeamStore();
22
+ this.spawner = getTeammateSpawner();
23
+ }
24
+
25
+ private async getBroker(teamId: string): Promise<MessageBroker> {
26
+ if (!this.brokers.has(teamId)) {
27
+ const broker = getMessageBroker(teamId);
28
+ if (!broker.isConnected()) {
29
+ await broker.start();
30
+ }
31
+ this.brokers.set(teamId, broker);
32
+ }
33
+ return this.brokers.get(teamId)!;
34
+ }
35
+
36
+ private async broadcastTaskUpdate(
37
+ teamId: string,
38
+ fromMemberId: string,
39
+ taskId: string,
40
+ action: 'created' | 'claimed' | 'completed' | 'released' | 'deleted',
41
+ taskInfo?: { title: string; assignee?: string; result?: string }
42
+ ): Promise<void> {
43
+ const envBrokerPort = process.env.XAGENT_BROKER_PORT;
44
+ const content = JSON.stringify({
45
+ taskId,
46
+ action,
47
+ ...taskInfo,
48
+ timestamp: Date.now()
49
+ });
50
+
51
+ // Teammate processes use MessageClient to broadcast
52
+ if (envBrokerPort && fromMemberId === process.env.XAGENT_MEMBER_ID) {
53
+ await this.broadcastAsTeammate(teamId, fromMemberId, parseInt(envBrokerPort, 10), content, 'task_update');
54
+ return;
55
+ }
56
+
57
+ // Lead processes use broker directly
58
+ try {
59
+ const broker = await this.getBroker(teamId);
60
+ broker.sendMessage(fromMemberId, 'broadcast', content, 'task_update');
61
+ } catch (error) {
62
+ console.warn('[Team] Failed to broadcast task update:', error);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Broadcast message as teammate using persistent MessageClient
68
+ */
69
+ private async broadcastAsTeammate(
70
+ teamId: string,
71
+ memberId: string,
72
+ brokerPort: number,
73
+ content: string,
74
+ type: string
75
+ ): Promise<void> {
76
+ // Try to use the persistent client first
77
+ const persistentClient = getTeammateClient();
78
+
79
+ if (persistentClient && persistentClient.isConnected()) {
80
+ if (type === 'task_update') {
81
+ persistentClient.sendTaskUpdate('', '', content);
82
+ } else {
83
+ persistentClient.broadcast(content);
84
+ }
85
+ return;
86
+ }
87
+
88
+ // Fallback: create temporary connection
89
+ return this.broadcastWithTempClient(teamId, memberId, brokerPort, content, type);
90
+ }
91
+
92
+ /**
93
+ * Fallback: broadcast with temporary MessageClient connection
94
+ */
95
+ private async broadcastWithTempClient(
96
+ teamId: string,
97
+ memberId: string,
98
+ brokerPort: number,
99
+ content: string,
100
+ type: string
101
+ ): Promise<void> {
102
+ return new Promise((resolve) => {
103
+ const client = new MessageClient(teamId, memberId, brokerPort);
104
+ let resolved = false;
105
+
106
+ const cleanup = () => {
107
+ if (!resolved) {
108
+ resolved = true;
109
+ client.disconnect().catch(() => {});
110
+ }
111
+ };
112
+
113
+ // Timeout after 5 seconds
114
+ const timeout = setTimeout(() => {
115
+ cleanup();
116
+ resolve(); // Resolve anyway, broadcast is fire-and-forget
117
+ }, 5000);
118
+
119
+ client.on('connected', () => {
120
+ if (type === 'task_update') {
121
+ client.sendTaskUpdate('', '', content);
122
+ } else {
123
+ client.broadcast(content);
124
+ }
125
+
126
+ // Small delay before closing
127
+ setTimeout(() => {
128
+ clearTimeout(timeout);
129
+ cleanup();
130
+ resolve();
131
+ }, 300);
132
+ });
133
+
134
+ client.on('error', () => {
135
+ clearTimeout(timeout);
136
+ cleanup();
137
+ resolve(); // Resolve anyway, broadcast is fire-and-forget
138
+ });
139
+
140
+ client.on('disconnected', () => {
141
+ if (!resolved) {
142
+ clearTimeout(timeout);
143
+ cleanup();
144
+ resolve();
145
+ }
146
+ });
147
+
148
+ client.connect().catch(() => {
149
+ clearTimeout(timeout);
150
+ cleanup();
151
+ resolve();
152
+ });
153
+ });
154
+ }
155
+
156
+ private checkPermission(
157
+ memberPermissions: MemberPermissions,
158
+ action: string
159
+ ): boolean {
160
+ const permissionMap: Record<string, keyof MemberPermissions> = {
161
+ createTask: 'canCreateTask',
162
+ assignTask: 'canAssignTask',
163
+ claimTask: 'canClaimTask',
164
+ completeTask: 'canCompleteTask',
165
+ deleteTask: 'canDeleteTask',
166
+ messageAll: 'canMessageAll',
167
+ messageDirect: 'canMessageDirect',
168
+ shutdownTeam: 'canShutdownTeam',
169
+ shutdownMember: 'canShutdownMember',
170
+ inviteMembers: 'canInviteMembers',
171
+ };
172
+
173
+ const permissionKey = permissionMap[action];
174
+ if (!permissionKey) {
175
+ return true;
176
+ }
177
+ return memberPermissions[permissionKey];
178
+ }
179
+
180
+ async execute(
181
+ params: TeamToolParams
182
+ ): Promise<{ success: boolean; message: string; result?: any }> {
183
+ const memberId = process.env.XAGENT_MEMBER_ID;
184
+ // Role determined by action type:
185
+ // - create: caller is lead
186
+ // - other actions: if XAGENT_MEMBER_ID is not set, caller is lead
187
+ const isTeamLead = params.team_action === 'create' || !memberId;
188
+
189
+ const permissions = isTeamLead ? LEAD_PERMISSIONS : TEAMMATE_PERMISSIONS;
190
+
191
+ switch (params.team_action) {
192
+ case 'create':
193
+ return this.createTeam(params);
194
+ case 'spawn':
195
+ if (!this.checkPermission(permissions, 'inviteMembers')) {
196
+ return {
197
+ success: false,
198
+ message: 'Permission denied: Only team lead can invite new members',
199
+ };
200
+ }
201
+ return this.spawnTeammate(params);
202
+ case 'message': {
203
+ const canMessage = params.message?.to_member_id === 'broadcast'
204
+ ? this.checkPermission(permissions, 'messageAll')
205
+ : this.checkPermission(permissions, 'messageDirect');
206
+ if (!canMessage) {
207
+ return {
208
+ success: false,
209
+ message: 'Permission denied: You cannot send broadcast messages',
210
+ };
211
+ }
212
+ return this.sendTeamMessage(params);
213
+ }
214
+ case 'task_create':
215
+ if (!this.checkPermission(permissions, 'createTask')) {
216
+ return {
217
+ success: false,
218
+ message: 'Permission denied: You cannot create tasks',
219
+ };
220
+ }
221
+ return this.createTeamTask(params, memberId);
222
+ case 'task_update':
223
+ return this.updateTeamTask(params, memberId, permissions);
224
+ case 'task_delete':
225
+ if (!this.checkPermission(permissions, 'deleteTask')) {
226
+ return {
227
+ success: false,
228
+ message: 'Permission denied: Only lead can delete tasks',
229
+ };
230
+ }
231
+ return this.deleteTeamTask(params);
232
+ case 'task_list':
233
+ return this.listTeamTasks(params);
234
+ case 'shutdown':
235
+ if (!params.team_id) {
236
+ return { success: false, message: 'team_id is required for shutdown' };
237
+ }
238
+ {
239
+ const team = await this.store.getTeam(params.team_id);
240
+ if (!team) {
241
+ return { success: false, message: `Team ${params.team_id} not found` };
242
+ }
243
+ const actualMemberId = memberId || team.leadMemberId;
244
+
245
+ // Prevent shutting down self - use cleanup to shutdown entire team
246
+ if (params.member_id === 'self' || params.member_id === actualMemberId) {
247
+ return {
248
+ success: false,
249
+ message: 'Cannot shutdown yourself.',
250
+ };
251
+ }
252
+
253
+ // Prevent shutting down lead - lead should only be removed via cleanup
254
+ if (params.member_id === team.leadMemberId) {
255
+ return {
256
+ success: false,
257
+ message: 'Cannot shutdown team lead.',
258
+ };
259
+ }
260
+ }
261
+ if (!params.member_id) {
262
+ return {
263
+ success: false,
264
+ message: 'member_id is required for shutdown',
265
+ };
266
+ }
267
+ if (!this.checkPermission(permissions, 'shutdownMember')) {
268
+ return {
269
+ success: false,
270
+ message: 'Permission denied: Only team lead can shutdown other members',
271
+ };
272
+ }
273
+ return this.shutdownTeammate(params, params.member_id);
274
+ case 'cleanup':
275
+ if (!this.checkPermission(permissions, 'shutdownTeam')) {
276
+ return {
277
+ success: false,
278
+ message: 'Permission denied: Only team lead can cleanup the team',
279
+ };
280
+ }
281
+ return this.cleanupTeam(params);
282
+ case 'list_teams':
283
+ return this.listTeams();
284
+ case 'get_status':
285
+ return this.getTeamStatus(params);
286
+ default:
287
+ throw new Error(`Unknown team action: ${params.team_action}`);
288
+ }
289
+ }
290
+
291
+ private async createTeam(
292
+ params: TeamToolParams
293
+ ): Promise<{ success: boolean; message: string; result?: any }> {
294
+ if (!params.team_name) {
295
+ return { success: false, message: 'team_name is required' };
296
+ }
297
+ if (!params.teammates || params.teammates.length === 0) {
298
+ return { success: false, message: 'teammates is required' };
299
+ }
300
+
301
+ // 检查每个 teammate 配置
302
+ for (let i = 0; i < params.teammates.length; i++) {
303
+ const t = params.teammates[i];
304
+ if (!t.name) {
305
+ return { success: false, message: `teammates[${i}].name is required` };
306
+ }
307
+ if (!t.role) {
308
+ return { success: false, message: `teammates[${i}].role is required` };
309
+ }
310
+ if (!t.prompt) {
311
+ return { success: false, message: `teammates[${i}].prompt is required` };
312
+ }
313
+ }
314
+
315
+ const team = await this.store.createTeam(
316
+ params.team_name || 'unnamed-team',
317
+ process.env.XAGENT_SESSION_ID || 'lead',
318
+ process.cwd()
319
+ );
320
+
321
+ const displayMode = 'auto';
322
+
323
+ const broker = await this.getBroker(team.teamId);
324
+ const brokerPort = broker.getPort();
325
+
326
+ console.log(
327
+ colors.primaryBright(`\n${icons.rocket} Team "${team.teamName}" created`)
328
+ );
329
+ console.log(colors.textMuted(` ${icons.arrow} Broker: port ${brokerPort}`));
330
+
331
+ const spawnedMembers: TeamMember[] = [];
332
+ const initialTasks: { taskId: string; title: string; assignee: string }[] = [];
333
+
334
+ const leadMember = team.members[0];
335
+ const leadMemberId = leadMember?.memberId;
336
+
337
+ // Spawn teammates with lead ID
338
+ if (params.teammates && params.teammates.length > 0) {
339
+ console.log(colors.text(` ${icons.bullet} Members:`));
340
+
341
+ for (const teammateConfig of params.teammates) {
342
+ // Create initial task BEFORE spawning, so we can pass task ID to teammate
343
+ const task = await this.store.createTask(team.teamId, {
344
+ title: `Initial task for ${teammateConfig.name}`,
345
+ description: teammateConfig.prompt,
346
+ priority: 'high',
347
+ }, 'lead');
348
+
349
+ initialTasks.push({
350
+ taskId: task.taskId,
351
+ title: task.title,
352
+ assignee: '', // Will be set after spawn
353
+ });
354
+
355
+ // Spawn teammate with initial task ID and lead ID
356
+ const member = await this.spawner.spawnTeammate(
357
+ team.teamId,
358
+ teammateConfig,
359
+ team.workDir,
360
+ displayMode,
361
+ brokerPort,
362
+ task.taskId, // Pass initial task ID
363
+ leadMemberId // Pass lead member ID
364
+ );
365
+ spawnedMembers.push(member);
366
+ const displayName = member.name || member.memberId.slice(0, 8);
367
+ const statusIcon = member.status === 'active' ? '🟢' : '🟡';
368
+
369
+ console.log(
370
+ ` ${statusIcon} ${colors.primary(displayName)} ${colors.textMuted(`(${member.memberRole || member.role})`)}`
371
+ );
372
+
373
+ // Mark task as in_progress and assign to the teammate
374
+ await this.store.updateTask(team.teamId, task.taskId, {
375
+ status: 'in_progress',
376
+ assignee: member.memberId,
377
+ }, task.version);
378
+
379
+ // Update initialTasks with assignee
380
+ initialTasks[initialTasks.length - 1].assignee = member.memberId;
381
+ }
382
+ }
383
+
384
+ return {
385
+ success: true,
386
+ message: `Team "${team.teamName}" created successfully with ${initialTasks.length} initial tasks`,
387
+ result: {
388
+ team_id: team.teamId,
389
+ team_name: team.teamName,
390
+ display_mode: displayMode,
391
+ lead_id: leadMemberId,
392
+ your_role: 'lead',
393
+ your_member_id: leadMemberId,
394
+ broker_port: brokerPort,
395
+ is_team_lead: true,
396
+ members: spawnedMembers.map((m) => ({
397
+ id: m.memberId,
398
+ name: m.name,
399
+ role: m.memberRole || m.role,
400
+ display_mode: m.displayMode,
401
+ })),
402
+ initial_tasks: initialTasks,
403
+ },
404
+ };
405
+ }
406
+
407
+ private async spawnTeammate(
408
+ params: TeamToolParams
409
+ ): Promise<{ success: boolean; message: string; result?: any }> {
410
+ if (!params.team_id) {
411
+ return { success: false, message: 'team_id is required' };
412
+ }
413
+ if (!params.teammates || params.teammates.length === 0) {
414
+ return { success: false, message: 'teammates[0] is required' };
415
+ }
416
+
417
+ // 只检查第一个 teammate
418
+ const t = params.teammates[0];
419
+ if (!t.name) {
420
+ return { success: false, message: 'teammates[0].name is required' };
421
+ }
422
+ if (!t.role) {
423
+ return { success: false, message: 'teammates[0].role is required' };
424
+ }
425
+ if (!t.prompt) {
426
+ return { success: false, message: 'teammates[0].prompt is required' };
427
+ }
428
+
429
+ const team = await this.store.getTeam(params.team_id);
430
+ if (!team) {
431
+ return { success: false, message: `Team ${params.team_id} not found` };
432
+ }
433
+
434
+ const displayMode = 'auto';
435
+ const broker = await this.getBroker(params.team_id);
436
+ const brokerPort = broker.getPort();
437
+ const leadMemberId = team.leadMemberId;
438
+
439
+ // 只 spawn 一个 teammate
440
+ const teammateConfig = params.teammates[0];
441
+ const member = await this.spawner.spawnTeammate(
442
+ params.team_id,
443
+ teammateConfig,
444
+ team.workDir,
445
+ displayMode,
446
+ brokerPort,
447
+ undefined, // No initial task ID for dynamic spawn
448
+ leadMemberId
449
+ );
450
+
451
+ const displayName = member.name || member.memberId.slice(0, 8);
452
+ const statusIcon = member.status === 'active' ? '🟢' : '🟡';
453
+ console.log(
454
+ ` ${statusIcon} ${colors.success('Spawned:')} ${colors.primary(displayName)} ${colors.textMuted(`(${member.memberRole || member.role})`)}`
455
+ );
456
+
457
+ return {
458
+ success: true,
459
+ message: `Spawned teammate: ${displayName}`,
460
+ result: {
461
+ team_id: params.team_id,
462
+ member_id: member.memberId,
463
+ name: member.name,
464
+ role: member.memberRole || member.role,
465
+ },
466
+ };
467
+ }
468
+
469
+ private async sendTeamMessage(
470
+ params: TeamToolParams
471
+ ): Promise<{ success: boolean; message: string; result?: any }> {
472
+ if (!params.team_id) {
473
+ throw new Error('team_id is required');
474
+ }
475
+ if (!params.message) {
476
+ throw new Error('message is required');
477
+ }
478
+
479
+ const team = await this.store.getTeam(params.team_id);
480
+ if (!team) {
481
+ throw new Error(`Team ${params.team_id} not found`);
482
+ }
483
+
484
+ // Determine fromMemberId: use env var for teammates, or team.leadMemberId for lead
485
+ const envMemberId = process.env.XAGENT_MEMBER_ID;
486
+ const envBrokerPort = process.env.XAGENT_BROKER_PORT;
487
+ const fromMemberId = envMemberId || team.leadMemberId;
488
+
489
+ // Resolve target: convert 'lead' to actual leadMemberId for direct messages
490
+ let targetMemberId = params.message.to_member_id || 'broadcast';
491
+ if (targetMemberId === 'lead') {
492
+ targetMemberId = team.leadMemberId;
493
+ }
494
+
495
+ // Check if this is a teammate process (has broker port env var)
496
+ // Teammate should use MessageClient to connect to lead's broker
497
+ if (envBrokerPort && envMemberId) {
498
+ return this.sendMessageAsTeammate(
499
+ params.team_id,
500
+ envMemberId,
501
+ parseInt(envBrokerPort, 10),
502
+ targetMemberId,
503
+ params.message.content
504
+ );
505
+ }
506
+
507
+ // Lead process: use broker directly
508
+ const broker = await this.getBroker(params.team_id);
509
+
510
+ try {
511
+ const { message, deliveryInfo } = await broker.sendMessageWithAck(
512
+ fromMemberId,
513
+ targetMemberId,
514
+ params.message.content
515
+ );
516
+
517
+ const isBroadcast =
518
+ params.message.to_member_id === 'broadcast' || !params.message.to_member_id;
519
+ const ackCount = Array.isArray(deliveryInfo)
520
+ ? deliveryInfo.filter((d) => d.status === 'acknowledged').length
521
+ : deliveryInfo.status === 'acknowledged'
522
+ ? 1
523
+ : 0;
524
+ const totalCount = Array.isArray(deliveryInfo) ? deliveryInfo.length : 1;
525
+
526
+ return {
527
+ success: true,
528
+ message: isBroadcast
529
+ ? `Message broadcasted and acknowledged by ${ackCount}/${totalCount} members`
530
+ : `Message delivered and acknowledged`,
531
+ result: {
532
+ message_id: message.messageId,
533
+ delivered_to: message.toMemberId,
534
+ delivery_status: Array.isArray(deliveryInfo)
535
+ ? deliveryInfo.map((d) => ({
536
+ member_id: d.acknowledgedBy?.[0],
537
+ status: d.status,
538
+ }))
539
+ : { status: deliveryInfo.status, acknowledged_at: deliveryInfo.acknowledgedAt },
540
+ },
541
+ };
542
+ } catch (error) {
543
+ return {
544
+ success: false,
545
+ message: `Failed to send message: ${error instanceof Error ? error.message : String(error)}`,
546
+ result: undefined,
547
+ };
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Send message as teammate using persistent MessageClient
553
+ */
554
+ private async sendMessageAsTeammate(
555
+ teamId: string,
556
+ memberId: string,
557
+ brokerPort: number,
558
+ targetMemberId: string,
559
+ content: string
560
+ ): Promise<{ success: boolean; message: string; result?: any }> {
561
+ // Try to use the persistent client first
562
+ const persistentClient = getTeammateClient();
563
+
564
+ if (persistentClient && persistentClient.isConnected()) {
565
+ // Send message using appropriate method
566
+ if (targetMemberId === 'broadcast') {
567
+ persistentClient.broadcast(content);
568
+ } else {
569
+ persistentClient.sendDirect(targetMemberId, content);
570
+ }
571
+
572
+ const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
573
+
574
+ return {
575
+ success: true,
576
+ message: `Message sent to ${targetMemberId}`,
577
+ result: {
578
+ message_id: messageId,
579
+ delivered_to: targetMemberId,
580
+ delivery_status: { status: 'sent' },
581
+ },
582
+ };
583
+ }
584
+
585
+ // Fallback: create temporary connection if persistent client not available
586
+ return this.sendMessageWithTempClient(teamId, memberId, brokerPort, targetMemberId, content);
587
+ }
588
+
589
+ /**
590
+ * Fallback: send message with temporary MessageClient connection
591
+ */
592
+ private async sendMessageWithTempClient(
593
+ teamId: string,
594
+ memberId: string,
595
+ brokerPort: number,
596
+ targetMemberId: string,
597
+ content: string
598
+ ): Promise<{ success: boolean; message: string; result?: any }> {
599
+ return new Promise((resolve) => {
600
+ const client = new MessageClient(teamId, memberId, brokerPort);
601
+ let resolved = false;
602
+
603
+ const cleanup = () => {
604
+ if (!resolved) {
605
+ resolved = true;
606
+ client.disconnect().catch(() => {});
607
+ }
608
+ };
609
+
610
+ // Timeout after 10 seconds
611
+ const timeout = setTimeout(() => {
612
+ cleanup();
613
+ resolve({
614
+ success: false,
615
+ message: 'Failed to send message: timeout',
616
+ result: undefined,
617
+ });
618
+ }, 10000);
619
+
620
+ client.on('connected', () => {
621
+ // Send message using appropriate method
622
+ if (targetMemberId === 'broadcast') {
623
+ client.broadcast(content);
624
+ } else {
625
+ client.sendDirect(targetMemberId, content);
626
+ }
627
+
628
+ const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
629
+
630
+ // Give a small delay for the message to be sent before closing
631
+ setTimeout(() => {
632
+ clearTimeout(timeout);
633
+ cleanup();
634
+ resolve({
635
+ success: true,
636
+ message: `Message sent to ${targetMemberId}`,
637
+ result: {
638
+ message_id: messageId,
639
+ delivered_to: targetMemberId,
640
+ delivery_status: { status: 'sent' },
641
+ },
642
+ });
643
+ }, 500);
644
+ });
645
+
646
+ client.on('error', (err: Error) => {
647
+ clearTimeout(timeout);
648
+ cleanup();
649
+ resolve({
650
+ success: false,
651
+ message: `Failed to send message: ${err.message}`,
652
+ result: undefined,
653
+ });
654
+ });
655
+
656
+ client.on('disconnected', () => {
657
+ if (!resolved) {
658
+ clearTimeout(timeout);
659
+ cleanup();
660
+ resolve({
661
+ success: false,
662
+ message: 'Failed to send message: disconnected from broker',
663
+ result: undefined,
664
+ });
665
+ }
666
+ });
667
+
668
+ client.connect().catch((err) => {
669
+ clearTimeout(timeout);
670
+ cleanup();
671
+ resolve({
672
+ success: false,
673
+ message: `Failed to connect to broker: ${err.message}`,
674
+ result: undefined,
675
+ });
676
+ });
677
+ });
678
+ }
679
+
680
+ async getMessageDeliveryInfo(
681
+ teamId: string,
682
+ messageId: string
683
+ ): Promise<MessageDeliveryInfo | undefined> {
684
+ const broker = this.brokers.get(teamId);
685
+ if (!broker) {
686
+ return undefined;
687
+ }
688
+ return broker.getDeliveryInfo(messageId);
689
+ }
690
+
691
+ private async createTeamTask(
692
+ params: TeamToolParams,
693
+ createdBy: string | undefined
694
+ ): Promise<{ success: boolean; message: string; result?: any }> {
695
+ if (!params.team_id) {
696
+ throw new Error('team_id is required');
697
+ }
698
+ if (!params.task_config) {
699
+ throw new Error('task_config is required');
700
+ }
701
+
702
+ // Get team to resolve leadMemberId if createdBy is not provided
703
+ const team = await this.store.getTeam(params.team_id);
704
+ if (!team) {
705
+ throw new Error(`Team ${params.team_id} not found`);
706
+ }
707
+ const actualCreatedBy = createdBy || team.leadMemberId;
708
+
709
+ const task = await this.store.createTask(params.team_id, params.task_config, actualCreatedBy);
710
+
711
+ console.log(colors.success(`✓ Task created: ${task.title} (${task.taskId})`));
712
+
713
+ await this.broadcastTaskUpdate(params.team_id!, actualCreatedBy, task.taskId, 'created', {
714
+ title: task.title
715
+ });
716
+
717
+ return {
718
+ success: true,
719
+ message: `Task "${task.title}" created successfully`,
720
+ result: {
721
+ task_id: task.taskId,
722
+ title: task.title,
723
+ description: task.description,
724
+ status: task.status,
725
+ priority: task.priority,
726
+ assignee: task.assignee,
727
+ dependencies: task.dependencies,
728
+ created_at: task.createdAt,
729
+ },
730
+ };
731
+ }
732
+
733
+ private async updateTeamTask(
734
+ params: TeamToolParams,
735
+ memberId: string | undefined,
736
+ permissions: MemberPermissions
737
+ ): Promise<{ success: boolean; message: string; result?: any }> {
738
+ if (!params.team_id) {
739
+ throw new Error('team_id is required');
740
+ }
741
+ if (!params.task_update) {
742
+ throw new Error('task_update is required');
743
+ }
744
+
745
+ // Get team to resolve leadMemberId if memberId is not provided
746
+ const team = await this.store.getTeam(params.team_id);
747
+ if (!team) {
748
+ throw new Error(`Team ${params.team_id} not found`);
749
+ }
750
+ const actualMemberId = memberId || team.leadMemberId;
751
+
752
+ const { task_id, action, result } = params.task_update;
753
+
754
+ if (action === 'claim') {
755
+ if (!this.checkPermission(permissions, 'claimTask')) {
756
+ return {
757
+ success: false,
758
+ message: 'Permission denied: You cannot claim tasks',
759
+ };
760
+ }
761
+
762
+ try {
763
+ const claimedTask = await this.store.claimTask(params.team_id, task_id, actualMemberId);
764
+ if (!claimedTask) {
765
+ return {
766
+ success: false,
767
+ message: `Task ${task_id} not found`,
768
+ };
769
+ }
770
+
771
+ await this.broadcastTaskUpdate(params.team_id, actualMemberId, task_id, 'claimed', {
772
+ title: claimedTask.title,
773
+ assignee: claimedTask.assignee
774
+ });
775
+
776
+ return {
777
+ success: true,
778
+ message: `Task ${task_id} claimed successfully`,
779
+ result: {
780
+ task_id: claimedTask.taskId,
781
+ status: claimedTask.status,
782
+ assignee: claimedTask.assignee,
783
+ },
784
+ };
785
+ } catch (error) {
786
+ const errorMessage = error instanceof Error ? error.message : String(error);
787
+ return {
788
+ success: false,
789
+ message: errorMessage,
790
+ };
791
+ }
792
+ }
793
+
794
+ if (action === 'complete') {
795
+ if (!this.checkPermission(permissions, 'completeTask')) {
796
+ return {
797
+ success: false,
798
+ message: 'Permission denied: You cannot complete tasks',
799
+ };
800
+ }
801
+
802
+ const existingTask = await this.store.getTask(params.team_id, task_id);
803
+ if (!existingTask) {
804
+ return { success: false, message: `Task ${task_id} not found` };
805
+ }
806
+
807
+ const task = await this.store.updateTask(params.team_id, task_id, {
808
+ status: 'completed',
809
+ result,
810
+ }, existingTask.version);
811
+ if (!task) {
812
+ return { success: false, message: `Task ${task_id} update failed` };
813
+ }
814
+
815
+ await this.broadcastTaskUpdate(params.team_id, actualMemberId, task_id, 'completed', {
816
+ title: task.title,
817
+ result: task.result
818
+ });
819
+
820
+ return {
821
+ success: true,
822
+ message: `Task ${task_id} completed`,
823
+ result: {
824
+ task_id: task.taskId,
825
+ status: task.status,
826
+ result: task.result,
827
+ },
828
+ };
829
+ }
830
+
831
+ if (action === 'release') {
832
+ const existingTask = await this.store.getTask(params.team_id, task_id);
833
+ if (!existingTask) {
834
+ return { success: false, message: `Task ${task_id} not found` };
835
+ }
836
+
837
+ const task = await this.store.updateTask(params.team_id, task_id, {
838
+ status: 'pending',
839
+ assignee: undefined,
840
+ }, existingTask.version);
841
+ if (!task) {
842
+ return { success: false, message: `Task ${task_id} update failed` };
843
+ }
844
+
845
+ await this.broadcastTaskUpdate(params.team_id, actualMemberId, task_id, 'released', {
846
+ title: task.title
847
+ });
848
+
849
+ return {
850
+ success: true,
851
+ message: `Task ${task_id} released back to pool`,
852
+ result: {
853
+ task_id: task.taskId,
854
+ status: task.status,
855
+ },
856
+ };
857
+ }
858
+
859
+ throw new Error(`Unknown task action: ${action}`);
860
+ }
861
+
862
+ private async deleteTeamTask(
863
+ params: TeamToolParams
864
+ ): Promise<{ success: boolean; message: string; result?: any }> {
865
+ if (!params.team_id) {
866
+ throw new Error('team_id is required');
867
+ }
868
+
869
+ const team = await this.store.getTeam(params.team_id);
870
+ if (!team) {
871
+ throw new Error(`Team ${params.team_id} not found`);
872
+ }
873
+
874
+ const memberId = process.env.XAGENT_MEMBER_ID || team.leadMemberId;
875
+ const taskId = params.task_update?.task_id;
876
+ if (!taskId) {
877
+ throw new Error('task_id is required for deletion');
878
+ }
879
+
880
+ const existingTask = await this.store.getTask(params.team_id, taskId);
881
+ if (!existingTask) {
882
+ return { success: false, message: `Task ${taskId} not found` };
883
+ }
884
+
885
+ const deleted = await this.store.deleteTask(params.team_id, taskId, existingTask.version);
886
+ if (!deleted) {
887
+ return { success: false, message: `Task ${taskId} was modified by another member` };
888
+ }
889
+
890
+ console.log(colors.success(`✓ Task ${taskId} deleted`));
891
+
892
+ await this.broadcastTaskUpdate(params.team_id, memberId, taskId, 'deleted', {
893
+ title: existingTask.title
894
+ });
895
+
896
+ return {
897
+ success: true,
898
+ message: `Task ${taskId} deleted`,
899
+ result: { task_id: taskId },
900
+ };
901
+ }
902
+
903
+ private async listTeamTasks(
904
+ params: TeamToolParams
905
+ ): Promise<{ success: boolean; message: string; result?: any }> {
906
+ if (!params.team_id) {
907
+ throw new Error('team_id is required');
908
+ }
909
+
910
+ const team = await this.store.getTeam(params.team_id);
911
+ if (!team) {
912
+ throw new Error(`Team ${params.team_id} not found`);
913
+ }
914
+
915
+ const filter = params.task_filter || 'all';
916
+ let tasks: TeamTask[];
917
+
918
+ switch (filter) {
919
+ case 'pending':
920
+ tasks = (await this.store.getTasks(params.team_id)).filter(
921
+ (t) => t.status === 'pending'
922
+ );
923
+ break;
924
+ case 'available':
925
+ tasks = await this.store.getAvailableTasks(params.team_id);
926
+ break;
927
+ case 'in_progress':
928
+ tasks = (await this.store.getTasks(params.team_id)).filter(
929
+ (t) => t.status === 'in_progress'
930
+ );
931
+ break;
932
+ case 'completed':
933
+ tasks = (await this.store.getTasks(params.team_id)).filter(
934
+ (t) => t.status === 'completed'
935
+ );
936
+ break;
937
+ default:
938
+ tasks = await this.store.getTasks(params.team_id);
939
+ }
940
+
941
+ return {
942
+ success: true,
943
+ message: `Found ${tasks.length} tasks (filter: ${filter})`,
944
+ result: {
945
+ team_id: params.team_id,
946
+ filter,
947
+ total_count: tasks.length,
948
+ tasks: tasks.map((t) => ({
949
+ task_id: t.taskId,
950
+ title: t.title,
951
+ description: t.description,
952
+ status: t.status,
953
+ priority: t.priority,
954
+ assignee: t.assignee,
955
+ dependencies: t.dependencies,
956
+ created_at: t.createdAt,
957
+ updated_at: t.updatedAt,
958
+ result: t.result,
959
+ })),
960
+ },
961
+ };
962
+ }
963
+
964
+ private async shutdownTeammate(
965
+ params: TeamToolParams,
966
+ memberId: string
967
+ ): Promise<{ success: boolean; message: string; result?: any }> {
968
+ if (!params.team_id) {
969
+ throw new Error('team_id is required');
970
+ }
971
+ if (!memberId) {
972
+ throw new Error('member_id is required');
973
+ }
974
+
975
+ const result = await this.spawner.shutdownTeammate(params.team_id, memberId);
976
+
977
+ if (result.success) {
978
+ console.log(colors.textMuted(`${icons.check} Teammate ${memberId.slice(0, 8)} shut down${result.reason ? `: ${result.reason}` : ''}`));
979
+ } else {
980
+ console.log(colors.error(`${icons.cross} Failed to shutdown ${memberId.slice(0, 8)}: ${result.reason}`));
981
+ }
982
+
983
+ return {
984
+ success: result.success,
985
+ message: result.success
986
+ ? `Teammate ${memberId} shut down${result.reason ? `: ${result.reason}` : ''}`
987
+ : `Failed to shutdown ${memberId}: ${result.reason}`,
988
+ result: { member_id: memberId, reason: result.reason },
989
+ };
990
+ }
991
+
992
+ private async cleanupTeam(
993
+ params: TeamToolParams
994
+ ): Promise<{ success: boolean; message: string; result?: any }> {
995
+ if (!params.team_id) {
996
+ throw new Error('team_id is required');
997
+ }
998
+
999
+ const team = await this.store.getTeam(params.team_id);
1000
+ if (!team) {
1001
+ throw new Error(`Team ${params.team_id} not found`);
1002
+ }
1003
+
1004
+ // Auto-shutdown all active teammates (role !== 'lead')
1005
+ const activeTeammates = team.members.filter(
1006
+ (m) => m.status === 'active' && m.role !== 'lead'
1007
+ );
1008
+ const shutdownResults: { memberId: string; success: boolean; reason?: string }[] = [];
1009
+
1010
+ for (const teammate of activeTeammates) {
1011
+ const result = await this.spawner.shutdownTeammate(params.team_id, teammate.memberId);
1012
+ shutdownResults.push({
1013
+ memberId: teammate.memberId,
1014
+ success: result.success,
1015
+ reason: result.reason,
1016
+ });
1017
+ const memberName = teammate.name || teammate.memberId.slice(0, 8);
1018
+ if (result.success) {
1019
+ console.log(colors.textMuted(` ${icons.check} ${memberName}`));
1020
+ } else {
1021
+ console.log(colors.error(` ${icons.cross} ${memberName}: ${result.reason}`));
1022
+ }
1023
+ }
1024
+
1025
+ // Stop the message broker
1026
+ const broker = this.brokers.get(params.team_id);
1027
+ if (broker) {
1028
+ await broker.stop();
1029
+ this.brokers.delete(params.team_id);
1030
+ removeMessageBroker(params.team_id);
1031
+ }
1032
+
1033
+ await this.store.deleteTeam(params.team_id);
1034
+
1035
+ console.log(colors.success(`\n${icons.check} Team cleaned up (${shutdownResults.filter(r => r.success).length}/${activeTeammates.length} teammates shutdown)`));
1036
+
1037
+ return {
1038
+ success: true,
1039
+ message: `Team ${params.team_id} cleaned up (${shutdownResults.filter(r => r.success).length}/${activeTeammates.length} teammates auto-shutdown)`,
1040
+ result: {
1041
+ team_id: params.team_id,
1042
+ auto_shutdown: shutdownResults,
1043
+ },
1044
+ };
1045
+ }
1046
+
1047
+ private async listTeams(): Promise<{ success: boolean; message: string; result?: any }> {
1048
+ const teams = await this.store.listTeams();
1049
+
1050
+ return {
1051
+ success: true,
1052
+ message: `Found ${teams.length} team(s)`,
1053
+ result: {
1054
+ total_count: teams.length,
1055
+ teams: teams.map((t) => ({
1056
+ team_id: t.teamId,
1057
+ team_name: t.teamName,
1058
+ member_count: t.members.length,
1059
+ status: t.status,
1060
+ created_at: t.createdAt,
1061
+ work_dir: t.workDir,
1062
+ })),
1063
+ },
1064
+ };
1065
+ }
1066
+
1067
+ private async getTeamStatus(
1068
+ params: TeamToolParams
1069
+ ): Promise<{ success: boolean; message: string; result?: any }> {
1070
+ if (!params.team_id) {
1071
+ throw new Error('team_id is required');
1072
+ }
1073
+
1074
+ const status = await this.store.getTeamStatus(params.team_id);
1075
+
1076
+ if (!status.team) {
1077
+ throw new Error(`Team ${params.team_id} not found`);
1078
+ }
1079
+
1080
+ const envMemberId = process.env.XAGENT_MEMBER_ID;
1081
+ const isTeamLead = !envMemberId;
1082
+ const yourRole = isTeamLead ? 'lead' : 'teammate';
1083
+ const yourMemberId = envMemberId || status.team.leadMemberId;
1084
+
1085
+ return {
1086
+ success: true,
1087
+ message: `Team status retrieved`,
1088
+ result: {
1089
+ team_id: params.team_id,
1090
+ team_name: status.team.teamName,
1091
+ status: status.team.status,
1092
+ your_role: yourRole,
1093
+ your_member_id: yourMemberId,
1094
+ member_count: status.memberCount,
1095
+ members: status.team.members.map((m) => ({
1096
+ id: m.memberId,
1097
+ name: m.name,
1098
+ role: m.memberRole || m.role,
1099
+ status: m.status,
1100
+ display_mode: m.displayMode,
1101
+ })),
1102
+ active_task_count: status.activeTaskCount,
1103
+ completed_task_count: status.completedTaskCount,
1104
+ created_at: status.team.createdAt,
1105
+ },
1106
+ };
1107
+ }
1108
+ }
1109
+
1110
+ let teamCoordinatorInstance: TeamCoordinator | null = null;
1111
+
1112
+ export function getTeamCoordinator(): TeamCoordinator {
1113
+ if (!teamCoordinatorInstance) {
1114
+ teamCoordinatorInstance = new TeamCoordinator();
1115
+ }
1116
+ return teamCoordinatorInstance;
1117
+ }