@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,558 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+ import {
6
+ Team,
7
+ TeamMember,
8
+ TeamTask,
9
+ TaskCreateConfig,
10
+ MemberRole,
11
+ LEAD_PERMISSIONS,
12
+ TEAMMATE_PERMISSIONS,
13
+ MemberPermissions,
14
+ } from './types.js';
15
+
16
+ const generateId = () => crypto.randomUUID();
17
+
18
+ // Lock timeout in milliseconds
19
+ const LOCK_TIMEOUT_MS = 10000;
20
+ // Maximum retries for acquiring lock
21
+ const MAX_LOCK_RETRIES = 50;
22
+ // Delay between lock retries in milliseconds
23
+ const LOCK_RETRY_DELAY_MS = 100;
24
+
25
+ interface LockInfo {
26
+ memberId: string;
27
+ timestamp: number;
28
+ pid: number;
29
+ }
30
+
31
+ export class TeamStore {
32
+ private baseDir: string;
33
+ private activeLocks: Map<string, NodeJS.Timeout> = new Map();
34
+
35
+ constructor() {
36
+ this.baseDir = path.join(os.homedir(), '.xagent', 'teams');
37
+ }
38
+
39
+ /**
40
+ * Acquire a file-based lock for a task.
41
+ * Uses exclusive file creation for atomicity across processes.
42
+ */
43
+ private async acquireTaskLock(teamId: string, taskId: string, memberId: string): Promise<boolean> {
44
+ const lockDir = path.join(this.getTeamDir(teamId), 'locks');
45
+ await fs.mkdir(lockDir, { recursive: true });
46
+ const lockPath = path.join(lockDir, `${taskId}.lock`);
47
+
48
+ const lockInfo: LockInfo = {
49
+ memberId,
50
+ timestamp: Date.now(),
51
+ pid: process.pid,
52
+ };
53
+
54
+ for (let attempt = 0; attempt < MAX_LOCK_RETRIES; attempt++) {
55
+ try {
56
+ // Try to create lock file exclusively (atomic operation)
57
+ const handle = await fs.open(lockPath, 'wx');
58
+ await handle.writeFile(JSON.stringify(lockInfo));
59
+ await handle.close();
60
+
61
+ // Set up auto-release timeout
62
+ const timeoutId = setTimeout(() => {
63
+ this.releaseTaskLock(teamId, taskId).catch(() => {});
64
+ }, LOCK_TIMEOUT_MS);
65
+ this.activeLocks.set(`${teamId}:${taskId}`, timeoutId);
66
+
67
+ return true;
68
+ } catch (error: any) {
69
+ if (error.code === 'EEXIST') {
70
+ // Lock file exists, check if it's stale
71
+ try {
72
+ const content = await fs.readFile(lockPath, 'utf-8');
73
+ const existingLock: LockInfo = JSON.parse(content);
74
+
75
+ // Check if lock is stale (older than timeout)
76
+ if (Date.now() - existingLock.timestamp > LOCK_TIMEOUT_MS) {
77
+ // Remove stale lock and retry
78
+ await fs.rm(lockPath, { force: true });
79
+ continue;
80
+ }
81
+
82
+ // Lock is held by another process, wait and retry
83
+ await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
84
+ continue;
85
+ } catch {
86
+ // Failed to read lock file, try to remove it
87
+ await fs.rm(lockPath, { force: true });
88
+ continue;
89
+ }
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * Release a task lock.
100
+ */
101
+ private async releaseTaskLock(teamId: string, taskId: string): Promise<void> {
102
+ const lockPath = path.join(this.getTeamDir(teamId), 'locks', `${taskId}.lock`);
103
+
104
+ // Clear auto-release timeout
105
+ const lockKey = `${teamId}:${taskId}`;
106
+ const timeoutId = this.activeLocks.get(lockKey);
107
+ if (timeoutId) {
108
+ clearTimeout(timeoutId);
109
+ this.activeLocks.delete(lockKey);
110
+ }
111
+
112
+ try {
113
+ // Verify we own the lock before releasing
114
+ const content = await fs.readFile(lockPath, 'utf-8');
115
+ const lockInfo: LockInfo = JSON.parse(content);
116
+
117
+ // Only release if we own the lock (same pid or stale)
118
+ if (lockInfo.pid === process.pid || Date.now() - lockInfo.timestamp > LOCK_TIMEOUT_MS) {
119
+ await fs.rm(lockPath, { force: true });
120
+ }
121
+ } catch {
122
+ // Lock file doesn't exist or is corrupted, ignore
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Check if a task is locked by another member.
128
+ */
129
+ async isTaskLocked(teamId: string, taskId: string): Promise<boolean> {
130
+ const lockPath = path.join(this.getTeamDir(teamId), 'locks', `${taskId}.lock`);
131
+
132
+ try {
133
+ const content = await fs.readFile(lockPath, 'utf-8');
134
+ const lockInfo: LockInfo = JSON.parse(content);
135
+
136
+ // Check if lock is stale
137
+ if (Date.now() - lockInfo.timestamp > LOCK_TIMEOUT_MS) {
138
+ return false;
139
+ }
140
+
141
+ return true;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ private getTeamDir(teamId: string): string {
148
+ return path.join(this.baseDir, teamId);
149
+ }
150
+
151
+ async ensureBaseDir(): Promise<void> {
152
+ await fs.mkdir(this.baseDir, { recursive: true });
153
+ }
154
+
155
+ async createTeam(name: string, leadSessionId: string, workDir: string): Promise<Team> {
156
+ await this.ensureBaseDir();
157
+
158
+ const teamId = generateId();
159
+ const leadMemberId = generateId();
160
+
161
+ const leadMember: TeamMember = {
162
+ memberId: leadMemberId,
163
+ name: 'Lead',
164
+ role: 'lead',
165
+ memberRole: 'Team Lead',
166
+ status: 'active',
167
+ permissions: LEAD_PERMISSIONS,
168
+ };
169
+
170
+ const team: Team = {
171
+ teamId,
172
+ teamName: name,
173
+ createdAt: Date.now(),
174
+ leadSessionId,
175
+ leadMemberId,
176
+ members: [leadMember],
177
+ status: 'active',
178
+ workDir,
179
+ sharedTaskList: [],
180
+ };
181
+
182
+ const teamDir = this.getTeamDir(teamId);
183
+ await fs.mkdir(teamDir, { recursive: true });
184
+ await fs.mkdir(path.join(teamDir, 'tasks'), { recursive: true });
185
+
186
+ await this.saveTeam(team);
187
+ return team;
188
+ }
189
+
190
+ async getTeam(teamId: string): Promise<Team | null> {
191
+ try {
192
+ const configPath = path.join(this.getTeamDir(teamId), 'config.json');
193
+ const content = await fs.readFile(configPath, 'utf-8');
194
+ return JSON.parse(content);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ async saveTeam(team: Team): Promise<void> {
201
+ await this.ensureBaseDir();
202
+ const configPath = path.join(this.getTeamDir(team.teamId), 'config.json');
203
+ await fs.writeFile(configPath, JSON.stringify(team, null, 2));
204
+ }
205
+
206
+ async addMember(
207
+ teamId: string,
208
+ member: Omit<TeamMember, 'permissions' | 'role'>
209
+ ): Promise<TeamMember> {
210
+ const team = await this.getTeam(teamId);
211
+ if (!team) {
212
+ throw new Error(`Team ${teamId} not found`);
213
+ }
214
+
215
+ const newMember: TeamMember = {
216
+ ...member,
217
+ role: 'teammate',
218
+ permissions: TEAMMATE_PERMISSIONS,
219
+ };
220
+
221
+ const existingIndex = team.members.findIndex((m) => m.memberId === member.memberId);
222
+ if (existingIndex >= 0) {
223
+ team.members[existingIndex] = newMember;
224
+ } else {
225
+ team.members.push(newMember);
226
+ }
227
+
228
+ await this.saveTeam(team);
229
+ return newMember;
230
+ }
231
+
232
+ async updateMember(
233
+ teamId: string,
234
+ memberId: string,
235
+ updates: Partial<TeamMember>
236
+ ): Promise<void> {
237
+ const team = await this.getTeam(teamId);
238
+ if (team) {
239
+ const member = team.members.find((m) => m.memberId === memberId);
240
+ if (member) {
241
+ Object.assign(member, updates);
242
+ await this.saveTeam(team);
243
+ }
244
+ }
245
+ }
246
+
247
+ async getMember(teamId: string, memberId: string): Promise<TeamMember | null> {
248
+ const team = await this.getTeam(teamId);
249
+ if (team) {
250
+ return team.members.find((m) => m.memberId === memberId) || null;
251
+ }
252
+ return null;
253
+ }
254
+
255
+ async deleteTeam(teamId: string): Promise<void> {
256
+ const teamDir = this.getTeamDir(teamId);
257
+ await fs.rm(teamDir, { recursive: true, force: true });
258
+ }
259
+
260
+ async listTeams(): Promise<Team[]> {
261
+ await this.ensureBaseDir();
262
+ const teams: Team[] = [];
263
+
264
+ try {
265
+ const dirs = await fs.readdir(this.baseDir);
266
+ for (const dir of dirs) {
267
+ const team = await this.getTeam(dir);
268
+ if (team) {
269
+ teams.push(team);
270
+ }
271
+ }
272
+ } catch {
273
+ // ignore
274
+ }
275
+
276
+ return teams.sort((a, b) => b.createdAt - a.createdAt);
277
+ }
278
+
279
+ async createTask(
280
+ teamId: string,
281
+ config: TaskCreateConfig,
282
+ createdBy: string
283
+ ): Promise<TeamTask> {
284
+ const team = await this.getTeam(teamId);
285
+ if (!team) {
286
+ throw new Error(`Team ${teamId} not found`);
287
+ }
288
+
289
+ // Validate that all dependencies exist
290
+ if (config.dependencies && config.dependencies.length > 0) {
291
+ const existingTasks = await this.getTasks(teamId);
292
+ const existingTaskIds = new Set(existingTasks.map(t => t.taskId));
293
+ const invalidDeps = config.dependencies.filter(depId => !existingTaskIds.has(depId));
294
+
295
+ if (invalidDeps.length > 0) {
296
+ throw new Error(`Invalid task dependencies: tasks not found: ${invalidDeps.join(', ')}`);
297
+ }
298
+
299
+ // Check for circular dependencies
300
+ const depCheck = this.checkCircularDependency(config.dependencies, [], existingTasks);
301
+ if (depCheck.hasCycle) {
302
+ throw new Error(`Circular dependency detected: ${depCheck.cyclePath?.join(' -> ')}`);
303
+ }
304
+ }
305
+
306
+ const task: TeamTask = {
307
+ taskId: generateId(),
308
+ teamId,
309
+ title: config.title,
310
+ description: config.description,
311
+ status: 'pending',
312
+ assignee: config.assignee,
313
+ dependencies: config.dependencies || [],
314
+ priority: config.priority || 'medium',
315
+ createdAt: Date.now(),
316
+ updatedAt: Date.now(),
317
+ createdBy,
318
+ version: 1,
319
+ };
320
+
321
+ const tasksDir = path.join(this.getTeamDir(teamId), 'tasks');
322
+ await fs.mkdir(tasksDir, { recursive: true });
323
+
324
+ const taskPath = path.join(tasksDir, `${task.taskId}.json`);
325
+ await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
326
+
327
+ team.sharedTaskList.push(task);
328
+ await this.saveTeam(team);
329
+
330
+ return task;
331
+ }
332
+
333
+ /**
334
+ * Check for circular dependencies in task graph.
335
+ */
336
+ private checkCircularDependency(
337
+ taskIds: string[],
338
+ visited: string[],
339
+ allTasks: TeamTask[]
340
+ ): { hasCycle: boolean; cyclePath?: string[] } {
341
+ const taskMap = new Map(allTasks.map(t => [t.taskId, t]));
342
+
343
+ for (const taskId of taskIds) {
344
+ if (visited.includes(taskId)) {
345
+ return { hasCycle: true, cyclePath: [...visited, taskId] };
346
+ }
347
+
348
+ const task = taskMap.get(taskId);
349
+ if (task && task.dependencies.length > 0) {
350
+ const result = this.checkCircularDependency(
351
+ task.dependencies,
352
+ [...visited, taskId],
353
+ allTasks
354
+ );
355
+ if (result.hasCycle) {
356
+ return result;
357
+ }
358
+ }
359
+ }
360
+
361
+ return { hasCycle: false };
362
+ }
363
+
364
+ async getTask(teamId: string, taskId: string): Promise<TeamTask | null> {
365
+ try {
366
+ const taskPath = path.join(this.getTeamDir(teamId), 'tasks', `${taskId}.json`);
367
+ const content = await fs.readFile(taskPath, 'utf-8');
368
+ return JSON.parse(content);
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+
374
+ async updateTask(
375
+ teamId: string,
376
+ taskId: string,
377
+ updates: Partial<TeamTask>,
378
+ expectedVersion?: number
379
+ ): Promise<TeamTask | null> {
380
+ const team = await this.getTeam(teamId);
381
+ if (!team) {
382
+ return null;
383
+ }
384
+
385
+ const taskIndex = team.sharedTaskList.findIndex((t) => t.taskId === taskId);
386
+ if (taskIndex < 0) {
387
+ const task = await this.getTask(teamId, taskId);
388
+ if (!task) return null;
389
+
390
+ if (expectedVersion !== undefined && task.version !== expectedVersion) {
391
+ return null;
392
+ }
393
+
394
+ Object.assign(task, updates, { updatedAt: Date.now(), version: task.version + 1 });
395
+ const taskPath = path.join(this.getTeamDir(teamId), 'tasks', `${taskId}.json`);
396
+ await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
397
+ return task;
398
+ }
399
+
400
+ if (expectedVersion !== undefined && team.sharedTaskList[taskIndex].version !== expectedVersion) {
401
+ return null;
402
+ }
403
+
404
+ Object.assign(team.sharedTaskList[taskIndex], updates, { updatedAt: Date.now(), version: (team.sharedTaskList[taskIndex].version || 0) + 1 });
405
+ const taskPath = path.join(this.getTeamDir(teamId), 'tasks', `${taskId}.json`);
406
+ await fs.writeFile(taskPath, JSON.stringify(team.sharedTaskList[taskIndex], null, 2));
407
+ await this.saveTeam(team);
408
+
409
+ return team.sharedTaskList[taskIndex];
410
+ }
411
+
412
+ async deleteTask(teamId: string, taskId: string, expectedVersion?: number): Promise<boolean> {
413
+ const team = await this.getTeam(teamId);
414
+ if (!team) {
415
+ return false;
416
+ }
417
+
418
+ const taskIndex = team.sharedTaskList.findIndex((t) => t.taskId === taskId);
419
+ if (taskIndex < 0) {
420
+ return false;
421
+ }
422
+
423
+ if (expectedVersion !== undefined && team.sharedTaskList[taskIndex].version !== expectedVersion) {
424
+ return false;
425
+ }
426
+
427
+ team.sharedTaskList.splice(taskIndex, 1);
428
+
429
+ const taskPath = path.join(this.getTeamDir(teamId), 'tasks', `${taskId}.json`);
430
+ await fs.rm(taskPath, { force: true });
431
+
432
+ await this.saveTeam(team);
433
+ return true;
434
+ }
435
+
436
+ async getTasks(teamId: string): Promise<TeamTask[]> {
437
+ const team = await this.getTeam(teamId);
438
+ if (team) {
439
+ return team.sharedTaskList.sort((a, b) => a.createdAt - b.createdAt);
440
+ }
441
+
442
+ const tasksDir = path.join(this.getTeamDir(teamId), 'tasks');
443
+ const tasks: TeamTask[] = [];
444
+
445
+ try {
446
+ const files = await fs.readdir(tasksDir);
447
+ for (const file of files) {
448
+ if (file.endsWith('.json')) {
449
+ const content = await fs.readFile(path.join(tasksDir, file), 'utf-8');
450
+ tasks.push(JSON.parse(content));
451
+ }
452
+ }
453
+ } catch {
454
+ // directory doesn't exist
455
+ }
456
+
457
+ return tasks.sort((a, b) => a.createdAt - b.createdAt);
458
+ }
459
+
460
+ async getAvailableTasks(teamId: string): Promise<TeamTask[]> {
461
+ const tasks = await this.getTasks(teamId);
462
+ const taskMap = new Map(tasks.map((t) => [t.taskId, t]));
463
+
464
+ return tasks.filter((task) => {
465
+ if (task.status !== 'pending') return false;
466
+ return task.dependencies.every((depId) => {
467
+ const dep = taskMap.get(depId);
468
+ return dep && dep.status === 'completed';
469
+ });
470
+ });
471
+ }
472
+
473
+ async claimTask(
474
+ teamId: string,
475
+ taskId: string,
476
+ memberId: string
477
+ ): Promise<TeamTask | null> {
478
+ // Acquire lock first to prevent race conditions
479
+ const lockAcquired = await this.acquireTaskLock(teamId, taskId, memberId);
480
+ if (!lockAcquired) {
481
+ throw new Error(`Task ${taskId} is currently being claimed by another member. Please try again.`);
482
+ }
483
+
484
+ try {
485
+ // Re-read task after acquiring lock to ensure we have latest state
486
+ const task = await this.getTask(teamId, taskId);
487
+ if (!task) return null;
488
+
489
+ if (task.status !== 'pending') {
490
+ throw new Error(`Task ${taskId} is not available (status: ${task.status})`);
491
+ }
492
+
493
+ // Re-check dependencies after acquiring lock
494
+ const tasks = await this.getTasks(teamId);
495
+ const taskMap = new Map(tasks.map((t) => [t.taskId, t]));
496
+
497
+ const uncompletedDeps = task.dependencies.filter((depId) => {
498
+ const dep = taskMap.get(depId);
499
+ return dep && dep.status !== 'completed';
500
+ });
501
+
502
+ if (uncompletedDeps.length > 0) {
503
+ throw new Error(`Task has uncompleted dependencies: ${uncompletedDeps.join(', ')}`);
504
+ }
505
+
506
+ // Update task with version check
507
+ const result = await this.updateTask(teamId, taskId, {
508
+ status: 'in_progress',
509
+ assignee: memberId,
510
+ }, task.version);
511
+
512
+ if (!result) {
513
+ throw new Error(`Task ${taskId} was already claimed by another member`);
514
+ }
515
+
516
+ return result;
517
+ } finally {
518
+ // Always release the lock
519
+ await this.releaseTaskLock(teamId, taskId);
520
+ }
521
+ }
522
+
523
+ async getTeamStatus(teamId: string): Promise<{
524
+ team: Team | null;
525
+ memberCount: number;
526
+ activeTaskCount: number;
527
+ completedTaskCount: number;
528
+ }> {
529
+ const team = await this.getTeam(teamId);
530
+ if (!team) {
531
+ return { team: null, memberCount: 0, activeTaskCount: 0, completedTaskCount: 0 };
532
+ }
533
+
534
+ const tasks = await this.getTasks(teamId);
535
+ const activeTaskCount = tasks.filter((t) => t.status === 'in_progress').length;
536
+ const completedTaskCount = tasks.filter((t) => t.status === 'completed').length;
537
+
538
+ return {
539
+ team,
540
+ memberCount: team.members.length,
541
+ activeTaskCount,
542
+ completedTaskCount,
543
+ };
544
+ }
545
+
546
+ getPermissionsForRole(role: MemberRole): MemberPermissions {
547
+ return role === 'lead' ? LEAD_PERMISSIONS : TEAMMATE_PERMISSIONS;
548
+ }
549
+ }
550
+
551
+ let teamStoreInstance: TeamStore | null = null;
552
+
553
+ export function getTeamStore(): TeamStore {
554
+ if (!teamStoreInstance) {
555
+ teamStoreInstance = new TeamStore();
556
+ }
557
+ return teamStoreInstance;
558
+ }