@alanse/clickup-multi-mcp-server 1.0.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 (56) hide show
  1. package/Dockerfile +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +470 -0
  4. package/build/config.js +237 -0
  5. package/build/index.js +87 -0
  6. package/build/logger.js +163 -0
  7. package/build/middleware/security.js +231 -0
  8. package/build/server.js +288 -0
  9. package/build/services/clickup/base.js +432 -0
  10. package/build/services/clickup/bulk.js +180 -0
  11. package/build/services/clickup/document.js +159 -0
  12. package/build/services/clickup/folder.js +136 -0
  13. package/build/services/clickup/index.js +76 -0
  14. package/build/services/clickup/list.js +191 -0
  15. package/build/services/clickup/tag.js +239 -0
  16. package/build/services/clickup/task/index.js +32 -0
  17. package/build/services/clickup/task/task-attachments.js +105 -0
  18. package/build/services/clickup/task/task-comments.js +114 -0
  19. package/build/services/clickup/task/task-core.js +604 -0
  20. package/build/services/clickup/task/task-custom-fields.js +107 -0
  21. package/build/services/clickup/task/task-search.js +986 -0
  22. package/build/services/clickup/task/task-service.js +104 -0
  23. package/build/services/clickup/task/task-tags.js +113 -0
  24. package/build/services/clickup/time.js +244 -0
  25. package/build/services/clickup/types.js +33 -0
  26. package/build/services/clickup/workspace.js +397 -0
  27. package/build/services/shared.js +61 -0
  28. package/build/sse_server.js +277 -0
  29. package/build/tools/documents.js +489 -0
  30. package/build/tools/folder.js +331 -0
  31. package/build/tools/index.js +16 -0
  32. package/build/tools/list.js +428 -0
  33. package/build/tools/member.js +106 -0
  34. package/build/tools/tag.js +833 -0
  35. package/build/tools/task/attachments.js +357 -0
  36. package/build/tools/task/attachments.types.js +9 -0
  37. package/build/tools/task/bulk-operations.js +338 -0
  38. package/build/tools/task/handlers.js +919 -0
  39. package/build/tools/task/index.js +30 -0
  40. package/build/tools/task/main.js +233 -0
  41. package/build/tools/task/single-operations.js +469 -0
  42. package/build/tools/task/time-tracking.js +575 -0
  43. package/build/tools/task/utilities.js +310 -0
  44. package/build/tools/task/workspace-operations.js +258 -0
  45. package/build/tools/tool-enhancer.js +37 -0
  46. package/build/tools/utils.js +12 -0
  47. package/build/tools/workspace-helper.js +44 -0
  48. package/build/tools/workspace.js +73 -0
  49. package/build/utils/color-processor.js +183 -0
  50. package/build/utils/concurrency-utils.js +248 -0
  51. package/build/utils/date-utils.js +542 -0
  52. package/build/utils/resolver-utils.js +135 -0
  53. package/build/utils/sponsor-service.js +93 -0
  54. package/build/utils/token-utils.js +49 -0
  55. package/package.json +77 -0
  56. package/smithery.yaml +23 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Base ClickUp Service Class
6
+ *
7
+ * This class provides core functionality for all ClickUp service modules:
8
+ * - Axios client configuration
9
+ * - Rate limiting and request throttling
10
+ * - Error handling
11
+ * - Common request methods
12
+ */
13
+ import axios from 'axios';
14
+ import { Logger, LogLevel } from '../../logger.js';
15
+ /**
16
+ * Error types for better error handling
17
+ */
18
+ export var ErrorCode;
19
+ (function (ErrorCode) {
20
+ ErrorCode["RATE_LIMIT"] = "rate_limit_exceeded";
21
+ ErrorCode["NOT_FOUND"] = "resource_not_found";
22
+ ErrorCode["UNAUTHORIZED"] = "unauthorized";
23
+ ErrorCode["VALIDATION"] = "validation_error";
24
+ ErrorCode["SERVER_ERROR"] = "server_error";
25
+ ErrorCode["NETWORK_ERROR"] = "network_error";
26
+ ErrorCode["WORKSPACE_ERROR"] = "workspace_error";
27
+ ErrorCode["INVALID_PARAMETER"] = "invalid_parameter";
28
+ ErrorCode["UNKNOWN"] = "unknown_error";
29
+ })(ErrorCode || (ErrorCode = {}));
30
+ /**
31
+ * Custom error class for ClickUp API errors
32
+ */
33
+ export class ClickUpServiceError extends Error {
34
+ constructor(message, code = ErrorCode.UNKNOWN, data, status, context) {
35
+ super(message);
36
+ this.name = 'ClickUpServiceError';
37
+ this.code = code;
38
+ this.data = data;
39
+ this.status = status;
40
+ this.context = context;
41
+ }
42
+ }
43
+ /**
44
+ * Helper function to safely parse JSON
45
+ * @param data Data to parse
46
+ * @param fallback Optional fallback value if parsing fails
47
+ * @returns Parsed JSON or fallback value
48
+ */
49
+ function safeJsonParse(data, fallback = undefined) {
50
+ if (typeof data !== 'string') {
51
+ return data;
52
+ }
53
+ try {
54
+ return JSON.parse(data);
55
+ }
56
+ catch (error) {
57
+ return fallback;
58
+ }
59
+ }
60
+ /**
61
+ * Base ClickUp service class that handles common functionality
62
+ */
63
+ export class BaseClickUpService {
64
+ /**
65
+ * Creates an instance of BaseClickUpService.
66
+ * @param apiKey - ClickUp API key for authentication
67
+ * @param teamId - ClickUp team ID for targeting the correct workspace
68
+ * @param baseUrl - Optional custom base URL for the ClickUp API
69
+ */
70
+ constructor(apiKey, teamId, baseUrl = 'https://api.clickup.com/api/v2') {
71
+ this.defaultRequestSpacing = 600; // Default milliseconds between requests
72
+ this.rateLimit = 100; // Maximum requests per minute (Free Forever plan)
73
+ this.timeout = 65000; // 65 seconds (safely under the 1-minute window)
74
+ this.requestQueue = [];
75
+ this.processingQueue = false;
76
+ this.lastRateLimitReset = 0;
77
+ this.apiKey = apiKey;
78
+ this.teamId = teamId;
79
+ this.requestSpacing = this.defaultRequestSpacing;
80
+ // Create a logger with the actual class name for better context
81
+ const className = this.constructor.name;
82
+ this.logger = new Logger(`ClickUp:${className}`);
83
+ // Configure the Axios client with default settings
84
+ this.client = axios.create({
85
+ baseURL: baseUrl,
86
+ headers: {
87
+ 'Authorization': apiKey,
88
+ 'Content-Type': 'application/json'
89
+ },
90
+ timeout: this.timeout,
91
+ transformResponse: [
92
+ // Add custom response transformer to handle both JSON and text responses
93
+ (data) => {
94
+ if (!data)
95
+ return data;
96
+ // If it's already an object, return as is
97
+ if (typeof data !== 'string')
98
+ return data;
99
+ // Try to parse as JSON, fall back to raw text if parsing fails
100
+ const parsed = safeJsonParse(data, null);
101
+ return parsed !== null ? parsed : data;
102
+ }
103
+ ]
104
+ });
105
+ this.logger.debug(`Initialized ${className}`, { teamId, baseUrl });
106
+ // Add response interceptor for error handling
107
+ this.client.interceptors.response.use(response => response, error => this.handleAxiosError(error));
108
+ }
109
+ /**
110
+ * Handle errors from Axios requests
111
+ * @private
112
+ * @param error Error from Axios
113
+ * @returns Never - always throws an error
114
+ */
115
+ handleAxiosError(error) {
116
+ // Determine error details
117
+ const status = error.response?.status;
118
+ const responseData = error.response?.data;
119
+ const errorMsg = responseData?.err || responseData?.error || error.message || 'Unknown API error';
120
+ const path = error.config?.url || 'unknown path';
121
+ // Context object for providing more detailed log information
122
+ const errorContext = {
123
+ path,
124
+ status,
125
+ method: error.config?.method?.toUpperCase() || 'UNKNOWN',
126
+ requestData: error.config?.data ? safeJsonParse(error.config.data, error.config.data) : undefined
127
+ };
128
+ // Pick the appropriate error code based on status
129
+ let code;
130
+ let logMessage;
131
+ let errorMessage;
132
+ if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
133
+ code = ErrorCode.NETWORK_ERROR;
134
+ logMessage = `Request timeout for ${path}`;
135
+ errorMessage = 'Request timed out. Please try again.';
136
+ }
137
+ else if (!error.response) {
138
+ code = ErrorCode.NETWORK_ERROR;
139
+ logMessage = `Network error accessing ${path}: ${error.message}`;
140
+ errorMessage = 'Network error. Please check your connection and try again.';
141
+ }
142
+ else if (status === 429) {
143
+ code = ErrorCode.RATE_LIMIT;
144
+ this.handleRateLimitHeaders(error.response.headers);
145
+ // Calculate time until reset
146
+ const reset = error.response.headers['x-ratelimit-reset'];
147
+ const now = Date.now() / 1000; // Convert to seconds
148
+ const timeToReset = Math.max(0, reset - now);
149
+ const resetMinutes = Math.ceil(timeToReset / 60);
150
+ logMessage = `Rate limit exceeded for ${path}`;
151
+ errorMessage = `Rate limit exceeded. Please wait ${resetMinutes} minute${resetMinutes === 1 ? '' : 's'} before trying again.`;
152
+ // Add more context to the error
153
+ errorContext.rateLimitInfo = {
154
+ limit: error.response.headers['x-ratelimit-limit'],
155
+ remaining: error.response.headers['x-ratelimit-remaining'],
156
+ reset: reset,
157
+ timeToReset: timeToReset
158
+ };
159
+ }
160
+ else if (status === 401 || status === 403) {
161
+ code = ErrorCode.UNAUTHORIZED;
162
+ logMessage = `Authorization failed for ${path}`;
163
+ errorMessage = 'Authorization failed. Please check your API key.';
164
+ }
165
+ else if (status === 404) {
166
+ code = ErrorCode.NOT_FOUND;
167
+ logMessage = `Resource not found: ${path}`;
168
+ errorMessage = 'Resource not found.';
169
+ }
170
+ else if (status >= 400 && status < 500) {
171
+ code = ErrorCode.VALIDATION;
172
+ logMessage = `Validation error for ${path}: ${errorMsg}`;
173
+ errorMessage = errorMsg;
174
+ }
175
+ else if (status >= 500) {
176
+ code = ErrorCode.SERVER_ERROR;
177
+ logMessage = `ClickUp server error: ${errorMsg}`;
178
+ errorMessage = 'ClickUp server error. Please try again later.';
179
+ }
180
+ else {
181
+ code = ErrorCode.UNKNOWN;
182
+ logMessage = `Unknown API error: ${errorMsg}`;
183
+ errorMessage = 'An unexpected error occurred. Please try again.';
184
+ }
185
+ // Log the error with context
186
+ this.logger.error(logMessage, errorContext);
187
+ // Throw a formatted error with user-friendly message
188
+ throw new ClickUpServiceError(errorMessage, code, error);
189
+ }
190
+ /**
191
+ * Handle rate limit headers from ClickUp API
192
+ * @private
193
+ * @param headers Response headers from ClickUp
194
+ */
195
+ handleRateLimitHeaders(headers) {
196
+ try {
197
+ // Parse the rate limit headers
198
+ const limit = headers['x-ratelimit-limit'];
199
+ const remaining = headers['x-ratelimit-remaining'];
200
+ const reset = headers['x-ratelimit-reset'];
201
+ // Only log if we're getting close to the limit
202
+ if (remaining < limit * 0.2) {
203
+ this.logger.warn('Approaching rate limit', { remaining, limit, reset });
204
+ }
205
+ else {
206
+ this.logger.debug('Rate limit status', { remaining, limit, reset });
207
+ }
208
+ if (reset) {
209
+ this.lastRateLimitReset = reset;
210
+ // If reset is in the future, calculate a safe request spacing
211
+ const now = Date.now();
212
+ const resetTime = reset * 1000; // convert to milliseconds
213
+ const timeToReset = Math.max(0, resetTime - now);
214
+ // Proactively adjust spacing when remaining requests get low
215
+ // This helps avoid hitting rate limits in the first place
216
+ if (remaining < limit * 0.3) {
217
+ // More aggressive spacing when close to limit
218
+ let safeSpacing;
219
+ if (remaining <= 5) {
220
+ // Very aggressive spacing for last few requests
221
+ safeSpacing = Math.ceil((timeToReset / remaining) * 2);
222
+ // Start processing in queue mode preemptively
223
+ if (!this.processingQueue) {
224
+ this.logger.info('Preemptively switching to queue mode (low remaining requests)', {
225
+ remaining,
226
+ limit
227
+ });
228
+ this.processingQueue = true;
229
+ this.processQueue().catch(err => {
230
+ this.logger.error('Error processing request queue', err);
231
+ });
232
+ }
233
+ }
234
+ else if (remaining <= 20) {
235
+ // More aggressive spacing
236
+ safeSpacing = Math.ceil((timeToReset / remaining) * 1.5);
237
+ }
238
+ else {
239
+ // Standard safe spacing with buffer
240
+ safeSpacing = Math.ceil((timeToReset / remaining) * 1.1);
241
+ }
242
+ // Apply updated spacing, but with a reasonable maximum
243
+ const maxSpacing = 5000; // 5 seconds max spacing
244
+ const adjustedSpacing = Math.min(safeSpacing, maxSpacing);
245
+ // Only adjust if it's greater than our current spacing
246
+ if (adjustedSpacing > this.requestSpacing) {
247
+ this.logger.debug(`Adjusting request spacing: ${this.requestSpacing}ms → ${adjustedSpacing}ms`, {
248
+ remaining,
249
+ timeToReset
250
+ });
251
+ this.requestSpacing = adjustedSpacing;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ catch (error) {
257
+ this.logger.warn('Failed to parse rate limit headers', error);
258
+ }
259
+ }
260
+ /**
261
+ * Process the request queue, respecting rate limits by spacing out requests
262
+ * @private
263
+ */
264
+ async processQueue() {
265
+ if (this.requestQueue.length === 0) {
266
+ this.logger.debug('Queue empty, exiting queue processing mode');
267
+ this.processingQueue = false;
268
+ return;
269
+ }
270
+ const queueLength = this.requestQueue.length;
271
+ this.logger.debug(`Processing request queue (${queueLength} items)`);
272
+ const startTime = Date.now();
273
+ try {
274
+ // Take the first request from the queue
275
+ const request = this.requestQueue.shift();
276
+ if (request) {
277
+ // Adjust delay based on queue size
278
+ // Longer delays for bigger queues to prevent overwhelming the API
279
+ let delay = this.requestSpacing;
280
+ if (queueLength > 20) {
281
+ delay = this.requestSpacing * 2;
282
+ }
283
+ else if (queueLength > 10) {
284
+ delay = this.requestSpacing * 1.5;
285
+ }
286
+ // Wait for the calculated delay
287
+ await new Promise(resolve => setTimeout(resolve, delay));
288
+ // Run the request
289
+ await request();
290
+ }
291
+ }
292
+ catch (error) {
293
+ if (error instanceof ClickUpServiceError && error.code === ErrorCode.RATE_LIMIT) {
294
+ // If we still hit rate limits, increase the spacing
295
+ this.requestSpacing = Math.min(this.requestSpacing * 1.5, 10000); // Max 10s
296
+ this.logger.warn(`Rate limit hit during queue processing, increasing delay to ${this.requestSpacing}ms`);
297
+ }
298
+ else {
299
+ this.logger.error('Error executing queued request', error);
300
+ }
301
+ }
302
+ finally {
303
+ const duration = Date.now() - startTime;
304
+ this.logger.trace(`Queue item processed in ${duration}ms, ${this.requestQueue.length} items remaining`);
305
+ // Continue processing the queue after the calculated delay
306
+ setTimeout(() => this.processQueue(), this.requestSpacing);
307
+ }
308
+ }
309
+ /**
310
+ * Makes an API request with rate limiting.
311
+ * @protected
312
+ * @param fn - Function that executes the API request
313
+ * @returns Promise that resolves with the result of the API request
314
+ */
315
+ async makeRequest(fn) {
316
+ // If we're being rate limited, queue the request rather than executing immediately
317
+ if (this.processingQueue) {
318
+ const queuePosition = this.requestQueue.length + 1;
319
+ const estimatedWaitTime = Math.ceil((queuePosition * this.requestSpacing) / 1000);
320
+ this.logger.info('Request queued due to rate limiting', {
321
+ queuePosition,
322
+ estimatedWaitSeconds: estimatedWaitTime,
323
+ currentSpacing: this.requestSpacing
324
+ });
325
+ return new Promise((resolve, reject) => {
326
+ this.requestQueue.push(async () => {
327
+ try {
328
+ const result = await fn();
329
+ resolve(result);
330
+ }
331
+ catch (error) {
332
+ // Enhance error message with queue context if it's a rate limit error
333
+ if (error instanceof ClickUpServiceError && error.code === ErrorCode.RATE_LIMIT) {
334
+ const enhancedError = new ClickUpServiceError(`${error.message} (Request was queued at position ${queuePosition})`, error.code, error.data);
335
+ reject(enhancedError);
336
+ }
337
+ else {
338
+ reject(error);
339
+ }
340
+ }
341
+ });
342
+ });
343
+ }
344
+ // Track request metadata
345
+ let requestMethod = 'unknown';
346
+ let requestPath = 'unknown';
347
+ let requestData = undefined;
348
+ // Set up interceptor to capture request details
349
+ const requestInterceptorId = this.client.interceptors.request.use((config) => {
350
+ // Capture request metadata
351
+ requestMethod = config.method?.toUpperCase() || 'unknown';
352
+ requestPath = config.url || 'unknown';
353
+ requestData = config.data;
354
+ return config;
355
+ });
356
+ const startTime = Date.now();
357
+ try {
358
+ // Execute the request function
359
+ const result = await fn();
360
+ // Debug log for successful requests with timing information
361
+ const duration = Date.now() - startTime;
362
+ this.logger.debug(`Request completed successfully in ${duration}ms`, {
363
+ method: requestMethod,
364
+ path: requestPath,
365
+ duration,
366
+ responseType: result ? typeof result : 'undefined'
367
+ });
368
+ return result;
369
+ }
370
+ catch (error) {
371
+ // If we hit a rate limit, start processing the queue
372
+ if (error instanceof ClickUpServiceError && error.code === ErrorCode.RATE_LIMIT) {
373
+ this.logger.warn('Rate limit reached, switching to queue mode', {
374
+ reset: this.lastRateLimitReset,
375
+ queueLength: this.requestQueue.length
376
+ });
377
+ if (!this.processingQueue) {
378
+ this.processingQueue = true;
379
+ this.processQueue().catch(err => {
380
+ this.logger.error('Error processing request queue', err);
381
+ });
382
+ }
383
+ // Queue this failed request and return a promise that will resolve when it's retried
384
+ return new Promise((resolve, reject) => {
385
+ this.requestQueue.push(async () => {
386
+ try {
387
+ const result = await fn();
388
+ resolve(result);
389
+ }
390
+ catch (retryError) {
391
+ reject(retryError);
392
+ }
393
+ });
394
+ });
395
+ }
396
+ // For other errors, just throw
397
+ throw error;
398
+ }
399
+ finally {
400
+ // Always remove the interceptor
401
+ this.client.interceptors.request.eject(requestInterceptorId);
402
+ }
403
+ }
404
+ /**
405
+ * Gets the ClickUp team ID associated with this service instance
406
+ * @returns The team ID
407
+ */
408
+ getTeamId() {
409
+ return this.teamId;
410
+ }
411
+ /**
412
+ * Helper method to log API operations
413
+ * @protected
414
+ * @param operation - Name of the operation being performed
415
+ * @param details - Details about the operation
416
+ */
417
+ logOperation(operation, details) {
418
+ this.logger.info(`Operation: ${operation}`, details);
419
+ }
420
+ /**
421
+ * Log detailed information about a request (path and payload)
422
+ * For trace level logging only
423
+ */
424
+ traceRequest(method, url, data) {
425
+ if (this.logger.isLevelEnabled(LogLevel.TRACE)) {
426
+ this.logger.trace(`${method} ${url}`, {
427
+ payload: data,
428
+ teamId: this.teamId
429
+ });
430
+ }
431
+ }
432
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Bulk Service
6
+ *
7
+ * Enhanced implementation for bulk operations that leverages the existing single-operation methods.
8
+ * This approach reduces code duplication while offering powerful concurrency management.
9
+ */
10
+ import { Logger } from '../../logger.js';
11
+ import { processBatch } from '../../utils/concurrency-utils.js';
12
+ import { ClickUpServiceError, ErrorCode } from './base.js';
13
+ import { clickUpServices } from '../shared.js';
14
+ import { findListIDByName } from '../../tools/list.js';
15
+ // Create logger instance
16
+ const logger = new Logger('BulkService');
17
+ /**
18
+ * Service for performing bulk operations in ClickUp
19
+ */
20
+ export class BulkService {
21
+ /**
22
+ * Create a new bulk service
23
+ * @param taskService ClickUp Task Service instance
24
+ */
25
+ constructor(taskService) {
26
+ this.taskService = taskService;
27
+ logger.info('BulkService initialized');
28
+ }
29
+ /**
30
+ * Create multiple tasks in a list efficiently
31
+ *
32
+ * @param listId ID of the list to create tasks in
33
+ * @param tasks Array of task data
34
+ * @param options Batch processing options
35
+ * @returns Results containing successful and failed tasks
36
+ */
37
+ async createTasks(listId, tasks, options) {
38
+ logger.info(`Creating ${tasks.length} tasks in list ${listId}`, {
39
+ batchSize: options?.batchSize,
40
+ concurrency: options?.concurrency
41
+ });
42
+ try {
43
+ // First validate that the list exists - do this once for all tasks
44
+ await this.taskService.validateListExists(listId);
45
+ // Process the tasks in batches
46
+ return await processBatch(tasks, (task, index) => {
47
+ logger.debug(`Creating task ${index + 1}/${tasks.length}`, {
48
+ taskName: task.name
49
+ });
50
+ // Reuse the single-task creation method
51
+ return this.taskService.createTask(listId, task);
52
+ }, options);
53
+ }
54
+ catch (error) {
55
+ logger.error(`Failed to create tasks in bulk`, {
56
+ listId,
57
+ taskCount: tasks.length,
58
+ error: error instanceof Error ? error.message : String(error)
59
+ });
60
+ throw new ClickUpServiceError(`Failed to create tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { listId, taskCount: tasks.length });
61
+ }
62
+ }
63
+ /**
64
+ * Find task by name within a specific list
65
+ */
66
+ async findTaskInList(taskName, listName) {
67
+ try {
68
+ const result = await this.taskService.findTasks({
69
+ taskName,
70
+ listName,
71
+ allowMultipleMatches: false,
72
+ useSmartDisambiguation: true,
73
+ includeFullDetails: false
74
+ });
75
+ if (!result || Array.isArray(result)) {
76
+ throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
77
+ }
78
+ logger.info(`Task "${taskName}" found with ID: ${result.id}`);
79
+ return result.id;
80
+ }
81
+ catch (error) {
82
+ // Enhance the error message
83
+ if (error instanceof ClickUpServiceError) {
84
+ throw error;
85
+ }
86
+ throw new ClickUpServiceError(`Error finding task "${taskName}" in list "${listName}": ${error instanceof Error ? error.message : String(error)}`, ErrorCode.UNKNOWN);
87
+ }
88
+ }
89
+ /**
90
+ * Resolve task ID using provided identifiers
91
+ */
92
+ async resolveTaskId(task) {
93
+ const { taskId, taskName, listName, customTaskId } = task;
94
+ if (taskId) {
95
+ return taskId;
96
+ }
97
+ if (customTaskId) {
98
+ const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
99
+ return resolvedTask.id;
100
+ }
101
+ if (taskName && listName) {
102
+ return await this.findTaskInList(taskName, listName);
103
+ }
104
+ throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
105
+ }
106
+ /**
107
+ * Update multiple tasks
108
+ * @param tasks Array of tasks to update with their new data
109
+ * @param options Optional batch processing settings
110
+ * @returns Array of updated tasks
111
+ */
112
+ async updateTasks(tasks, options) {
113
+ logger.info('Starting bulk update operation', { taskCount: tasks.length });
114
+ try {
115
+ return await processBatch(tasks, async (task) => {
116
+ const { taskId, taskName, listName, customTaskId, ...updateData } = task;
117
+ const resolvedTaskId = await this.resolveTaskId({ taskId, taskName, listName, customTaskId });
118
+ return await this.taskService.updateTask(resolvedTaskId, updateData);
119
+ }, options);
120
+ }
121
+ catch (error) {
122
+ logger.error('Bulk update operation failed', error);
123
+ throw error;
124
+ }
125
+ }
126
+ /**
127
+ * Move multiple tasks to a different list
128
+ * @param tasks Array of tasks to move (each with taskId or taskName + listName)
129
+ * @param targetListId ID of the destination list or list name
130
+ * @param options Optional batch processing settings
131
+ * @returns Array of moved tasks
132
+ */
133
+ async moveTasks(tasks, targetListId, options) {
134
+ logger.info('Starting bulk move operation', { taskCount: tasks.length, targetListId });
135
+ try {
136
+ // Determine if targetListId is actually an ID or a name
137
+ let resolvedTargetListId = targetListId;
138
+ // If the targetListId doesn't match the pattern of a list ID (usually just numbers),
139
+ // assume it's a list name and try to resolve it
140
+ if (!/^\d+$/.test(targetListId)) {
141
+ logger.info(`Target list appears to be a name: "${targetListId}", attempting to resolve`);
142
+ const listInfo = await findListIDByName(clickUpServices.workspace, targetListId);
143
+ if (!listInfo) {
144
+ throw new ClickUpServiceError(`Target list "${targetListId}" not found`, ErrorCode.NOT_FOUND);
145
+ }
146
+ resolvedTargetListId = listInfo.id;
147
+ logger.info(`Resolved target list to ID: ${resolvedTargetListId}`);
148
+ }
149
+ // Validate the destination list exists
150
+ await this.taskService.validateListExists(resolvedTargetListId);
151
+ return await processBatch(tasks, async (task) => {
152
+ const resolvedTaskId = await this.resolveTaskId(task);
153
+ return await this.taskService.moveTask(resolvedTaskId, resolvedTargetListId);
154
+ }, options);
155
+ }
156
+ catch (error) {
157
+ logger.error('Bulk move operation failed', error);
158
+ throw error;
159
+ }
160
+ }
161
+ /**
162
+ * Delete multiple tasks
163
+ * @param tasks Array of tasks to delete (each with taskId or taskName + listName)
164
+ * @param options Batch processing options
165
+ * @returns Results containing successful and failed deletions
166
+ */
167
+ async deleteTasks(tasks, options) {
168
+ logger.info('Starting bulk delete operation', { taskCount: tasks.length });
169
+ try {
170
+ return await processBatch(tasks, async (task) => {
171
+ const resolvedTaskId = await this.resolveTaskId(task);
172
+ await this.taskService.deleteTask(resolvedTaskId);
173
+ }, options);
174
+ }
175
+ catch (error) {
176
+ logger.error('Bulk delete operation failed', error);
177
+ throw error;
178
+ }
179
+ }
180
+ }