@taazkareem/clickup-mcp-server 0.4.60 → 0.4.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,492 @@
1
+ /**
2
+ * ClickUp Task Service
3
+ *
4
+ * Handles all operations related to tasks in ClickUp, including:
5
+ * - Creating tasks (single and bulk)
6
+ * - Retrieving tasks (single or multiple)
7
+ * - Updating tasks
8
+ * - Deleting tasks
9
+ * - Finding tasks by name
10
+ */
11
+ import { BaseClickUpService, ErrorCode, ClickUpServiceError } from './base.js';
12
+ import { BulkProcessor } from './bulk.js';
13
+ import { ListService } from './list.js';
14
+ export class TaskService extends BaseClickUpService {
15
+ constructor(apiKey, teamId, baseUrl, workspaceService) {
16
+ super(apiKey, teamId, baseUrl);
17
+ this.workspaceService = null;
18
+ this.bulkProcessor = new BulkProcessor();
19
+ this.listService = new ListService(apiKey, teamId);
20
+ this.workspaceService = workspaceService || null;
21
+ }
22
+ /**
23
+ * Helper method to handle errors consistently
24
+ * @param error The error that occurred
25
+ * @param message Optional custom error message
26
+ * @returns A ClickUpServiceError
27
+ */
28
+ handleError(error, message) {
29
+ if (error instanceof ClickUpServiceError) {
30
+ return error;
31
+ }
32
+ return new ClickUpServiceError(message || `Task service error: ${error.message}`, ErrorCode.UNKNOWN, error);
33
+ }
34
+ /**
35
+ * Create a new task in the specified list
36
+ * @param listId The ID of the list to create the task in
37
+ * @param taskData The data for the new task
38
+ * @returns The created task
39
+ */
40
+ async createTask(listId, taskData) {
41
+ this.logOperation('createTask', { listId, ...taskData });
42
+ try {
43
+ return await this.makeRequest(async () => {
44
+ const response = await this.client.post(`/list/${listId}/task`, taskData);
45
+ return response.data;
46
+ });
47
+ }
48
+ catch (error) {
49
+ throw this.handleError(error, `Failed to create task in list ${listId}`);
50
+ }
51
+ }
52
+ /**
53
+ * Get all tasks in a list with optional filtering
54
+ * @param listId The ID of the list to get tasks from
55
+ * @param filters Optional filters to apply
56
+ * @returns List of tasks matching the filters
57
+ */
58
+ async getTasks(listId, filters = {}) {
59
+ this.logOperation('getTasks', { listId, filters });
60
+ try {
61
+ return await this.makeRequest(async () => {
62
+ const params = new URLSearchParams();
63
+ // Add all filters to the query parameters
64
+ if (filters.include_closed)
65
+ params.append('include_closed', String(filters.include_closed));
66
+ if (filters.subtasks)
67
+ params.append('subtasks', String(filters.subtasks));
68
+ if (filters.page)
69
+ params.append('page', String(filters.page));
70
+ if (filters.order_by)
71
+ params.append('order_by', filters.order_by);
72
+ if (filters.reverse)
73
+ params.append('reverse', String(filters.reverse));
74
+ if (filters.statuses && filters.statuses.length > 0) {
75
+ filters.statuses.forEach(status => params.append('statuses[]', status));
76
+ }
77
+ if (filters.assignees && filters.assignees.length > 0) {
78
+ filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
79
+ }
80
+ if (filters.due_date_gt)
81
+ params.append('due_date_gt', String(filters.due_date_gt));
82
+ if (filters.due_date_lt)
83
+ params.append('due_date_lt', String(filters.due_date_lt));
84
+ if (filters.date_created_gt)
85
+ params.append('date_created_gt', String(filters.date_created_gt));
86
+ if (filters.date_created_lt)
87
+ params.append('date_created_lt', String(filters.date_created_lt));
88
+ if (filters.date_updated_gt)
89
+ params.append('date_updated_gt', String(filters.date_updated_gt));
90
+ if (filters.date_updated_lt)
91
+ params.append('date_updated_lt', String(filters.date_updated_lt));
92
+ // Handle custom fields if present
93
+ if (filters.custom_fields) {
94
+ Object.entries(filters.custom_fields).forEach(([key, value]) => {
95
+ params.append(`custom_fields[${key}]`, String(value));
96
+ });
97
+ }
98
+ const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
99
+ return response.data.tasks;
100
+ });
101
+ }
102
+ catch (error) {
103
+ throw this.handleError(error, `Failed to get tasks from list ${listId}`);
104
+ }
105
+ }
106
+ /**
107
+ * Get a specific task by ID
108
+ * @param taskId The ID of the task to retrieve
109
+ * @returns The task details
110
+ */
111
+ async getTask(taskId) {
112
+ this.logOperation('getTask', { taskId });
113
+ try {
114
+ return await this.makeRequest(async () => {
115
+ const response = await this.client.get(`/task/${taskId}`);
116
+ return response.data;
117
+ });
118
+ }
119
+ catch (error) {
120
+ throw this.handleError(error, `Failed to get task ${taskId}`);
121
+ }
122
+ }
123
+ /**
124
+ * Update an existing task
125
+ * @param taskId ID of the task to update
126
+ * @param updateData Data to update on the task
127
+ * @returns The updated task
128
+ */
129
+ async updateTask(taskId, updateData) {
130
+ this.logOperation('updateTask', { taskId, ...updateData });
131
+ try {
132
+ return await this.makeRequest(async () => {
133
+ const response = await this.client.put(`/task/${taskId}`, updateData);
134
+ return response.data;
135
+ });
136
+ }
137
+ catch (error) {
138
+ throw this.handleError(error, `Failed to update task ${taskId}`);
139
+ }
140
+ }
141
+ /**
142
+ * Delete a task
143
+ * @param taskId The ID of the task to delete
144
+ * @returns Success indicator
145
+ */
146
+ async deleteTask(taskId) {
147
+ this.logOperation('deleteTask', { taskId });
148
+ try {
149
+ await this.makeRequest(async () => {
150
+ await this.client.delete(`/task/${taskId}`);
151
+ });
152
+ return {
153
+ success: true
154
+ };
155
+ }
156
+ catch (error) {
157
+ throw this.handleError(error, `Failed to delete task ${taskId}`);
158
+ }
159
+ }
160
+ /**
161
+ * Find a task by its name in a specific list
162
+ * @param listId The list ID to search within
163
+ * @param taskName The name of the task to find
164
+ * @returns The task if found, otherwise null
165
+ */
166
+ async findTaskByName(listId, taskName) {
167
+ this.logOperation('findTaskByName', { listId, taskName });
168
+ try {
169
+ const tasks = await this.getTasks(listId);
170
+ const matchingTask = tasks.find(task => task.name.toLowerCase() === taskName.toLowerCase());
171
+ return matchingTask || null;
172
+ }
173
+ catch (error) {
174
+ throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
175
+ }
176
+ }
177
+ /**
178
+ * Move a task to a different list
179
+ * @param taskId The ID of the task to move
180
+ * @param destinationListId The ID of the list to move the task to
181
+ * @returns The updated task
182
+ */
183
+ async moveTask(taskId, destinationListId) {
184
+ this.logOperation('moveTask', { taskId, destinationListId });
185
+ try {
186
+ // First, get both the task and list info in parallel to save time
187
+ const [originalTask, destinationList] = await Promise.all([
188
+ this.getTask(taskId),
189
+ this.listService.getList(destinationListId)
190
+ ]);
191
+ const currentStatus = originalTask.status?.status;
192
+ const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
193
+ // Determine the appropriate status for the destination list
194
+ let newStatus = availableStatuses.includes(currentStatus || '')
195
+ ? currentStatus // Keep the same status if available in destination list
196
+ : destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
197
+ // Priority mapping: convert string priority to numeric value if needed
198
+ let priorityValue = null;
199
+ if (originalTask.priority) {
200
+ // If priority.id exists and is numeric, use that
201
+ if (originalTask.priority.id) {
202
+ priorityValue = parseInt(originalTask.priority.id);
203
+ // Ensure it's in the valid range 1-4
204
+ if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
205
+ priorityValue = null;
206
+ }
207
+ }
208
+ }
209
+ // Prepare the task data for the new list
210
+ const taskData = {
211
+ name: originalTask.name,
212
+ description: originalTask.description,
213
+ status: newStatus,
214
+ priority: priorityValue,
215
+ due_date: originalTask.due_date ? Number(originalTask.due_date) : undefined,
216
+ assignees: originalTask.assignees?.map(a => a.id) || [],
217
+ // Add any other relevant fields from the original task
218
+ };
219
+ // Create new task and delete old one in a single makeRequest call
220
+ return await this.makeRequest(async () => {
221
+ // First create the new task
222
+ const response = await this.client.post(`/list/${destinationListId}/task`, taskData);
223
+ // Then delete the original task
224
+ await this.client.delete(`/task/${taskId}`);
225
+ // Add a property to indicate the task was moved
226
+ const newTask = {
227
+ ...response.data,
228
+ moved: true,
229
+ originalId: taskId
230
+ };
231
+ return newTask;
232
+ });
233
+ }
234
+ catch (error) {
235
+ throw this.handleError(error, 'Failed to move task');
236
+ }
237
+ }
238
+ /**
239
+ * Create a duplicate of an existing task
240
+ * @param taskId The ID of the task to duplicate
241
+ * @param listId Optional destination list ID (defaults to the same list)
242
+ * @returns The newly created duplicate task
243
+ */
244
+ async duplicateTask(taskId, listId) {
245
+ this.logOperation('duplicateTask', { taskId, listId });
246
+ try {
247
+ // First, get the original task
248
+ const originalTask = await this.getTask(taskId);
249
+ // Priority mapping: convert string priority to numeric value if needed
250
+ let priorityValue = null;
251
+ if (originalTask.priority) {
252
+ // If priority.id exists and is numeric, use that
253
+ if (originalTask.priority.id) {
254
+ priorityValue = parseInt(originalTask.priority.id);
255
+ // Ensure it's in the valid range 1-4
256
+ if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
257
+ priorityValue = null;
258
+ }
259
+ }
260
+ }
261
+ // Prepare data for the new task
262
+ const newTaskData = {
263
+ name: `${originalTask.name} (Copy)`,
264
+ description: originalTask.description,
265
+ status: originalTask.status?.status,
266
+ priority: priorityValue,
267
+ due_date: originalTask.due_date ? new Date(originalTask.due_date).getTime() : undefined,
268
+ assignees: originalTask.assignees?.map(a => a.id) || []
269
+ };
270
+ // Create the new task in the specified list or original list
271
+ const targetListId = listId || originalTask.list.id;
272
+ return await this.createTask(targetListId, newTaskData);
273
+ }
274
+ catch (error) {
275
+ throw this.handleError(error, 'Failed to duplicate task');
276
+ }
277
+ }
278
+ /**
279
+ * Create multiple tasks in a list with advanced batching options
280
+ *
281
+ * @param listId The ID of the list to create tasks in
282
+ * @param data Object containing array of tasks to create
283
+ * @param options Configuration options for the bulk operation
284
+ * @param progressCallback Optional callback for tracking progress
285
+ * @returns Result containing both successful and failed operations
286
+ */
287
+ async createBulkTasks(listId, data, options, progressCallback) {
288
+ this.logOperation('createBulkTasks', {
289
+ listId,
290
+ taskCount: data.tasks.length,
291
+ batchSize: options?.batchSize,
292
+ concurrency: options?.concurrency
293
+ });
294
+ try {
295
+ // Validate list exists before proceeding
296
+ const list = await this.listService.getList(listId).catch(() => null);
297
+ if (!list) {
298
+ throw new ClickUpServiceError(`List not found with ID: ${listId}`, ErrorCode.NOT_FOUND);
299
+ }
300
+ // Set default options for better performance
301
+ const bulkOptions = {
302
+ batchSize: options?.batchSize ?? 5, // Smaller batch size for better rate limit handling
303
+ concurrency: options?.concurrency ?? 2, // Lower concurrency to avoid rate limits
304
+ continueOnError: options?.continueOnError ?? true, // Continue on individual task failures
305
+ retryCount: options?.retryCount ?? 3, // Retry failed operations
306
+ ...options
307
+ };
308
+ // Add list validation to progress tracking
309
+ const wrappedCallback = progressCallback ?
310
+ (progress) => {
311
+ progress.context = { listId, listName: list.name };
312
+ progressCallback(progress);
313
+ } : undefined;
314
+ return await this.bulkProcessor.processBulk(data.tasks, async (taskData) => {
315
+ try {
316
+ // Ensure task data is properly formatted
317
+ const sanitizedData = {
318
+ ...taskData,
319
+ // Remove any accidentally included list IDs in task data
320
+ list: undefined,
321
+ // Ensure name has emoji if missing
322
+ name: taskData.name.match(/^\p{Emoji}/u) ?
323
+ taskData.name :
324
+ '📋 ' + taskData.name
325
+ };
326
+ return await this.createTask(listId, sanitizedData);
327
+ }
328
+ catch (error) {
329
+ // Enhance error context for better debugging
330
+ if (error instanceof ClickUpServiceError) {
331
+ error.context = {
332
+ ...error.context,
333
+ taskName: taskData.name,
334
+ listId,
335
+ listName: list.name
336
+ };
337
+ }
338
+ throw error;
339
+ }
340
+ }, bulkOptions);
341
+ }
342
+ catch (error) {
343
+ const errorMessage = error instanceof ClickUpServiceError ?
344
+ error.message :
345
+ `Failed to create tasks in bulk: ${error.message}`;
346
+ throw new ClickUpServiceError(errorMessage, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, {
347
+ listId,
348
+ taskCount: data.tasks.length,
349
+ error: error instanceof Error ? error.message : String(error)
350
+ });
351
+ }
352
+ }
353
+ /**
354
+ * Update multiple tasks in bulk with advanced batching options
355
+ *
356
+ * @param tasks Array of task IDs and update data
357
+ * @param options Configuration options for the bulk operation
358
+ * @param progressCallback Optional callback for tracking progress
359
+ * @returns Result containing both successful and failed operations
360
+ */
361
+ async updateBulkTasks(tasks, options, progressCallback) {
362
+ this.logOperation('updateBulkTasks', {
363
+ taskCount: tasks.length,
364
+ batchSize: options?.batchSize,
365
+ concurrency: options?.concurrency
366
+ });
367
+ try {
368
+ return await this.bulkProcessor.processBulk(tasks, ({ id, data }) => this.updateTask(id, data), options);
369
+ }
370
+ catch (error) {
371
+ if (error instanceof ClickUpServiceError) {
372
+ throw error;
373
+ }
374
+ throw new ClickUpServiceError(`Failed to update tasks in bulk: ${error.message}`, ErrorCode.UNKNOWN, error);
375
+ }
376
+ }
377
+ /**
378
+ * Move multiple tasks to a different list in bulk
379
+ *
380
+ * @param tasks Array of task IDs to move
381
+ * @param targetListId ID of the list to move tasks to
382
+ * @param options Configuration options for the bulk operation
383
+ * @param progressCallback Optional callback for tracking progress
384
+ * @returns Result containing both successful and failed operations
385
+ */
386
+ async moveBulkTasks(tasks, targetListId, options, progressCallback) {
387
+ this.logOperation('moveBulkTasks', {
388
+ taskCount: tasks.length,
389
+ targetListId,
390
+ batchSize: options?.batchSize,
391
+ concurrency: options?.concurrency
392
+ });
393
+ try {
394
+ // First verify destination list exists
395
+ const destinationList = await this.listService.getList(targetListId);
396
+ if (!destinationList) {
397
+ throw new ClickUpServiceError(`Destination list not found with ID: ${targetListId}`, ErrorCode.NOT_FOUND);
398
+ }
399
+ // Set default options for better performance
400
+ const bulkOptions = {
401
+ batchSize: options?.batchSize ?? 5, // Smaller batch size for better rate limit handling
402
+ concurrency: options?.concurrency ?? 2, // Lower concurrency to avoid rate limits
403
+ continueOnError: options?.continueOnError ?? true, // Continue on individual task failures
404
+ retryCount: options?.retryCount ?? 3, // Retry failed operations
405
+ ...options
406
+ };
407
+ return await this.bulkProcessor.processBulk(tasks, async (taskId) => {
408
+ try {
409
+ // Get the original task
410
+ const originalTask = await this.getTask(taskId);
411
+ const currentStatus = originalTask.status?.status;
412
+ const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
413
+ // Determine the appropriate status for the destination list
414
+ let newStatus = availableStatuses.includes(currentStatus || '')
415
+ ? currentStatus // Keep the same status if available in destination list
416
+ : destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
417
+ // Prepare the task data for the new list
418
+ const taskData = {
419
+ name: originalTask.name,
420
+ description: originalTask.description,
421
+ status: newStatus,
422
+ priority: originalTask.priority?.priority,
423
+ due_date: originalTask.due_date ? Number(originalTask.due_date) : undefined,
424
+ assignees: originalTask.assignees?.map(a => a.id) || []
425
+ };
426
+ // Create new task and delete old one in a single makeRequest call
427
+ return await this.makeRequest(async () => {
428
+ // First create the new task
429
+ const response = await this.client.post(`/list/${targetListId}/task`, taskData);
430
+ // Then delete the original task
431
+ await this.client.delete(`/task/${taskId}`);
432
+ // Add a property to indicate the task was moved
433
+ const newTask = {
434
+ ...response.data,
435
+ moved: true,
436
+ originalId: taskId
437
+ };
438
+ return newTask;
439
+ });
440
+ }
441
+ catch (error) {
442
+ // Enhance error context for better debugging
443
+ if (error instanceof ClickUpServiceError) {
444
+ error.context = {
445
+ ...error.context,
446
+ taskId,
447
+ targetListId,
448
+ targetListName: destinationList.name
449
+ };
450
+ }
451
+ throw error;
452
+ }
453
+ }, bulkOptions);
454
+ }
455
+ catch (error) {
456
+ const errorMessage = error instanceof ClickUpServiceError ?
457
+ error.message :
458
+ `Failed to move tasks in bulk: ${error.message}`;
459
+ throw new ClickUpServiceError(errorMessage, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, {
460
+ targetListId,
461
+ taskCount: tasks.length,
462
+ error: error instanceof Error ? error.message : String(error)
463
+ });
464
+ }
465
+ }
466
+ /**
467
+ * Delete multiple tasks in bulk with advanced batching options
468
+ *
469
+ * @param taskIds Array of task IDs to delete
470
+ * @param options Configuration options for the bulk operation
471
+ * @returns Result containing both successful and failed operations
472
+ */
473
+ async deleteBulkTasks(taskIds, options) {
474
+ this.logOperation('deleteBulkTasks', {
475
+ taskCount: taskIds.length,
476
+ batchSize: options?.batchSize,
477
+ concurrency: options?.concurrency
478
+ });
479
+ try {
480
+ return await this.bulkProcessor.processBulk(taskIds, async (taskId) => {
481
+ await this.deleteTask(taskId);
482
+ return taskId;
483
+ }, options);
484
+ }
485
+ catch (error) {
486
+ if (error instanceof ClickUpServiceError) {
487
+ throw error;
488
+ }
489
+ throw new ClickUpServiceError(`Failed to delete tasks in bulk: ${error.message}`, ErrorCode.UNKNOWN, error);
490
+ }
491
+ }
492
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Common type definitions for ClickUp API entities
3
+ */
4
+ export {};