@taazkareem/clickup-mcp-server 0.6.2 → 0.6.3

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 (41) hide show
  1. package/README.md +6 -6
  2. package/build/logger.js +26 -1
  3. package/build/server.js +1 -1
  4. package/build/services/clickup/base.js +22 -1
  5. package/build/services/clickup/bulk.js +76 -45
  6. package/build/services/clickup/index.js +2 -2
  7. package/build/services/clickup/task/index.js +32 -0
  8. package/build/services/clickup/task/task-attachments.js +97 -0
  9. package/build/services/clickup/task/task-comments.js +104 -0
  10. package/build/services/clickup/task/task-core.js +477 -0
  11. package/build/services/clickup/task/task-custom-fields.js +97 -0
  12. package/build/services/clickup/task/task-search.js +462 -0
  13. package/build/services/clickup/task/task-service.js +25 -0
  14. package/build/services/clickup/task/task-tags.js +101 -0
  15. package/build/services/clickup/workspace.js +81 -36
  16. package/build/tools/folder.js +1 -1
  17. package/build/tools/list.js +2 -4
  18. package/build/tools/task/attachments.js +18 -5
  19. package/build/tools/task/attachments.types.js +9 -0
  20. package/build/tools/task/bulk-operations.js +111 -15
  21. package/build/tools/task/handlers.js +169 -24
  22. package/build/tools/task/index.js +1 -1
  23. package/build/tools/task/main.js +36 -1
  24. package/build/tools/task/single-operations.js +51 -4
  25. package/build/tools/task/utilities.js +24 -71
  26. package/build/tools/utils.js +2 -2
  27. package/build/utils/date-utils.js +149 -30
  28. package/build/utils/resolver-utils.js +33 -40
  29. package/build/utils/sponsor-service.js +1 -1
  30. package/package.json +1 -1
  31. package/build/mcp-tools.js +0 -64
  32. package/build/server-state.js +0 -93
  33. package/build/server.log +0 -0
  34. package/build/services/clickup/task.js +0 -701
  35. package/build/tools/bulk-tasks.js +0 -36
  36. package/build/tools/debug.js +0 -76
  37. package/build/tools/logs.js +0 -55
  38. package/build/tools/task.js +0 -1554
  39. package/build/utils/params-utils.js +0 -39
  40. package/build/utils/sponsor-analytics.js +0 -100
  41. package/build/utils/sponsor-utils.js +0 -57
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A Model Context Protocol (MCP) server for integrating ClickUp tasks with AI applications. This server allows AI agents to interact with ClickUp tasks, spaces, lists, and folders through a standardized protocol.
8
8
 
9
- > 🚧 **Status Update:** Rolling out v0.6.1 which will add Complete Tag Support including natural language tag color commands, Subtasks Support, Custom ID Support, and Logging Fixes
9
+ > 🚧 **Status Update:** Rolling out v0.6.3 will add Global Task Lookup with smart disambiguation, Start Date Support for tasks with natural language expressions, Complete Tag Support including natural language tag color commands, Subtasks Support, Custom ID Support, and Logging Fixes
10
10
 
11
11
  ## Setup
12
12
 
@@ -56,21 +56,21 @@ Or use this npx command:
56
56
 
57
57
  | 📝 Task Management | 🏷️ Tag Management |
58
58
  |----------------------------|----------------------------|
59
- | • Create, update, and delete tasks<br>• Move and duplicate tasks anywhere<br>• Support for single and bulk operations<br>• Set due dates with natural language<br>• Create and manage subtasks<br>• Add comments and attachments | • Create, update, and delete space tags<br>• Add and remove tags from tasks<br>• Use natural language color commands<br>• Automatic contrasting foreground colors<br>• View all space tags<br>• Tag-based task organization across workspace |
59
+ | • Create, update, and delete tasks<br>• Move and duplicate tasks anywhere<br>• Support for single and bulk operations<br>• Set start/due dates with natural language<br>• Create and manage subtasks<br>• Add comments and attachments | • Create, update, and delete space tags<br>• Add and remove tags from tasks<br>• Use natural language color commands<br>• Automatic contrasting foreground colors<br>• View all space tags<br>• Tag-based task organization across workspace |
60
60
  | 🌳 **Workspace Organization** | ⚡ **Integration Features** |
61
- | • Navigate spaces, folders, and lists<br>• Create and manage folders<br>• Organize lists within spaces<br>• Create lists in folders<br>• View workspace hierarchy<br>• Efficient path navigation | • Name or ID-based lookups<br>• Case-insensitive matching<br>• Markdown formatting support<br>• Built-in rate limiting<br>• Error handling and validation<br>• Comprehensive API coverage |
61
+ | • Navigate spaces, folders, and lists<br>• Create and manage folders<br>• Organize lists within spaces<br>• Create lists in folders<br>• View workspace hierarchy<br>• Efficient path navigation | • Global name or ID-based lookups<br>• Case-insensitive matching<br>• Markdown formatting support<br>• Built-in rate limiting<br>• Error handling and validation<br>• Comprehensive API coverage |
62
62
 
63
63
  ## Available Tools
64
64
 
65
65
  | Tool | Description | Required Parameters |
66
66
  |------|-------------|-------------------|
67
67
  | [get_workspace_hierarchy](docs/api-reference.md#workspace-navigation) | Get workspace structure | None |
68
- | [create_task](docs/api-reference.md#task-management) | Create a task | `name`, (`listId`/`listName`), optional `parent` |
68
+ | [create_task](docs/api-reference.md#task-management) | Create a task | `name`, (`listId`/`listName`) |
69
69
  | [create_bulk_tasks](docs/api-reference.md#task-management) | Create multiple tasks | `tasks[]` |
70
70
  | [update_task](docs/api-reference.md#task-management) | Modify task | `taskId`/`taskName` |
71
71
  | [update_bulk_tasks](docs/api-reference.md#task-management) | Update multiple tasks | `tasks[]` with IDs or names |
72
- | [get_tasks](docs/api-reference.md#task-management) | Get tasks from list | `listId`/`listName`, optional `subtasks` |
73
- | [get_task](docs/api-reference.md#task-management) | Get single task details | `taskId`/`taskName`, optional `subtasks` |
72
+ | [get_tasks](docs/api-reference.md#task-management) | Get tasks from list | `listId`/`listName` |
73
+ | [get_task](docs/api-reference.md#task-management) | Get single task details | `taskId`/`taskName` (with smart disambiguation) |
74
74
  | [get_workspace_tasks](docs/api-reference.md#task-management) | Get tasks with filtering | At least one filter (tags, list_ids, space_ids, etc.) |
75
75
  | [get_task_comments](docs/api-reference.md#task-management) | Get comments on a task | `taskId`/`taskName` |
76
76
  | [create_task_comment](docs/api-reference.md#task-management) | Add a comment to a task | `commentText`, (`taskId`/(`taskName`+`listName`)) |
package/build/logger.js CHANGED
@@ -48,7 +48,32 @@ export function log(level, message, data) {
48
48
  return;
49
49
  }
50
50
  const timestamp = new Date().toISOString();
51
- const logMessage = `[${timestamp}] [PID:${pid}] ${level.toUpperCase()}: ${message}${data ? '\n' + JSON.stringify(data, null, 2) : ''}`;
51
+ // Format the log message differently based on the level and data
52
+ let logMessage = `[${timestamp}] [PID:${pid}] ${level.toUpperCase()}: ${message}`;
53
+ // Format data differently based on content and log level
54
+ if (data) {
55
+ // For debugging and trace levels, try to make the data more readable
56
+ if (level === 'debug' || level === 'trace') {
57
+ // If data is a simple object with few properties, format it inline
58
+ if (typeof data === 'object' && data !== null && !Array.isArray(data) &&
59
+ Object.keys(data).length <= 4 && Object.keys(data).every(k => typeof data[k] !== 'object' || data[k] === null)) {
60
+ const dataStr = Object.entries(data)
61
+ .map(([k, v]) => `${k}=${v === undefined ? 'undefined' :
62
+ (v === null ? 'null' :
63
+ (typeof v === 'string' ? `"${v}"` : v))}`)
64
+ .join(' ');
65
+ logMessage += ` (${dataStr})`;
66
+ }
67
+ else {
68
+ // For more complex data, keep the JSON format but on new lines
69
+ logMessage += '\n' + JSON.stringify(data, null, 2);
70
+ }
71
+ }
72
+ else {
73
+ // For other levels, keep the original JSON format
74
+ logMessage += '\n' + JSON.stringify(data, null, 2);
75
+ }
76
+ }
52
77
  // When using stdio transport, log to stderr which is captured by host application
53
78
  console.error(logMessage);
54
79
  // Write to file
package/build/server.js CHANGED
@@ -19,7 +19,7 @@ const logger = new Logger('Server');
19
19
  const { workspace } = clickUpServices;
20
20
  export const server = new Server({
21
21
  name: "clickup-mcp-server",
22
- version: "0.6.2",
22
+ version: "0.6.3",
23
23
  }, {
24
24
  capabilities: {
25
25
  tools: {},
@@ -230,13 +230,30 @@ export class BaseClickUpService {
230
230
  });
231
231
  });
232
232
  }
233
+ // Track request metadata
234
+ let requestMethod = 'unknown';
235
+ let requestPath = 'unknown';
236
+ let requestData = undefined;
237
+ // Set up interceptor to capture request details
238
+ const requestInterceptorId = this.client.interceptors.request.use((config) => {
239
+ // Capture request metadata
240
+ requestMethod = config.method?.toUpperCase() || 'unknown';
241
+ requestPath = config.url || 'unknown';
242
+ requestData = config.data;
243
+ return config;
244
+ });
233
245
  const startTime = Date.now();
234
246
  try {
235
247
  // Execute the request function
236
248
  const result = await fn();
237
249
  // Debug log for successful requests with timing information
238
250
  const duration = Date.now() - startTime;
239
- this.logger.debug(`Request completed successfully in ${duration}ms`);
251
+ this.logger.debug(`Request completed successfully in ${duration}ms`, {
252
+ method: requestMethod,
253
+ path: requestPath,
254
+ duration,
255
+ responseType: result ? typeof result : 'undefined'
256
+ });
240
257
  return result;
241
258
  }
242
259
  catch (error) {
@@ -268,6 +285,10 @@ export class BaseClickUpService {
268
285
  // For other errors, just throw
269
286
  throw error;
270
287
  }
288
+ finally {
289
+ // Always remove the interceptor
290
+ this.client.interceptors.request.eject(requestInterceptorId);
291
+ }
271
292
  }
272
293
  /**
273
294
  * Gets the ClickUp team ID associated with this service instance
@@ -55,66 +55,97 @@ export class BulkService {
55
55
  }
56
56
  }
57
57
  /**
58
- * Update multiple tasks efficiently
59
- *
60
- * @param tasks Array of task IDs and update data
61
- * @param options Batch processing options
62
- * @returns Results containing successful and failed task updates
58
+ * Update multiple tasks
59
+ * @param tasks Array of tasks to update with their new data
60
+ * @param options Optional batch processing settings
61
+ * @returns Array of updated tasks
63
62
  */
64
63
  async updateTasks(tasks, options) {
65
- logger.info(`Updating ${tasks.length} tasks`, {
66
- batchSize: options?.batchSize,
67
- concurrency: options?.concurrency
68
- });
64
+ logger.info('Starting bulk update operation', { taskCount: tasks.length });
69
65
  try {
70
- return await processBatch(tasks, ({ id, data }, index) => {
71
- logger.debug(`Updating task ${index + 1}/${tasks.length}`, {
72
- taskId: id
73
- });
74
- // Reuse the single-task update method
75
- return this.taskService.updateTask(id, data);
66
+ // Extract all task IDs that need validation
67
+ const taskIds = tasks
68
+ .map(task => task.taskId)
69
+ .filter((id) => !!id);
70
+ // Validate all tasks exist in parallel
71
+ if (taskIds.length > 0) {
72
+ await this.taskService.validateTasksExist(taskIds);
73
+ }
74
+ // Process updates in batches
75
+ return await processBatch(tasks, async (task) => {
76
+ const { taskId, taskName, listName, customTaskId, ...updateData } = task;
77
+ if (taskId) {
78
+ return await this.taskService.updateTask(taskId, updateData);
79
+ }
80
+ else if (customTaskId) {
81
+ const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
82
+ return await this.taskService.updateTask(resolvedTask.id, updateData);
83
+ }
84
+ else if (taskName && listName) {
85
+ // For tasks identified by name, we need to resolve the ID first
86
+ const taskList = await this.taskService.getTasks(listName);
87
+ const matchingTask = taskList.find(t => t.name === taskName);
88
+ if (!matchingTask) {
89
+ throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
90
+ }
91
+ return await this.taskService.updateTask(matchingTask.id, updateData);
92
+ }
93
+ else {
94
+ throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
95
+ }
76
96
  }, options);
77
97
  }
78
98
  catch (error) {
79
- logger.error(`Failed to update tasks in bulk`, {
80
- taskCount: tasks.length,
81
- error: error instanceof Error ? error.message : String(error)
82
- });
83
- throw new ClickUpServiceError(`Failed to update tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { taskCount: tasks.length });
99
+ logger.error('Bulk update operation failed', error);
100
+ throw error;
84
101
  }
85
102
  }
86
103
  /**
87
- * Move multiple tasks to a different list efficiently
88
- *
89
- * @param taskIds Array of task IDs to move
90
- * @param targetListId ID of the list to move tasks to
91
- * @param options Batch processing options
92
- * @returns Results containing successful and failed moves
104
+ * Move multiple tasks to a different list
105
+ * @param tasks Array of tasks to move (each with taskId or taskName + listName)
106
+ * @param targetListId ID of the destination list
107
+ * @param options Optional batch processing settings
108
+ * @returns Array of moved tasks
93
109
  */
94
- async moveTasks(taskIds, targetListId, options) {
95
- logger.info(`Moving ${taskIds.length} tasks to list ${targetListId}`, {
96
- batchSize: options?.batchSize,
97
- concurrency: options?.concurrency
98
- });
110
+ async moveTasks(tasks, targetListId, options) {
111
+ logger.info('Starting bulk move operation', { taskCount: tasks.length, targetListId });
99
112
  try {
100
- // First validate that the target list exists - do this once for all tasks
113
+ // First validate the destination list exists
101
114
  await this.taskService.validateListExists(targetListId);
102
- return await processBatch(taskIds, (taskId, index) => {
103
- logger.debug(`Moving task ${index + 1}/${taskIds.length}`, {
104
- taskId,
105
- targetListId
106
- });
107
- // Reuse the single-task move method
108
- return this.taskService.moveTask(taskId, targetListId);
115
+ // Extract all task IDs that need validation
116
+ const taskIds = tasks
117
+ .map(task => task.taskId)
118
+ .filter((id) => !!id);
119
+ // Validate all tasks exist in parallel
120
+ if (taskIds.length > 0) {
121
+ await this.taskService.validateTasksExist(taskIds);
122
+ }
123
+ // Process moves in batches
124
+ return await processBatch(tasks, async (task) => {
125
+ if (task.taskId) {
126
+ return await this.taskService.moveTask(task.taskId, targetListId);
127
+ }
128
+ else if (task.customTaskId) {
129
+ const resolvedTask = await this.taskService.getTaskByCustomId(task.customTaskId);
130
+ return await this.taskService.moveTask(resolvedTask.id, targetListId);
131
+ }
132
+ else if (task.taskName && task.listName) {
133
+ // For tasks identified by name, we need to resolve the ID first
134
+ const taskList = await this.taskService.getTasks(task.listName);
135
+ const matchingTask = taskList.find(t => t.name === task.taskName);
136
+ if (!matchingTask) {
137
+ throw new ClickUpServiceError(`Task "${task.taskName}" not found in list "${task.listName}"`, ErrorCode.NOT_FOUND);
138
+ }
139
+ return await this.taskService.moveTask(matchingTask.id, targetListId);
140
+ }
141
+ else {
142
+ throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
143
+ }
109
144
  }, options);
110
145
  }
111
146
  catch (error) {
112
- logger.error(`Failed to move tasks in bulk`, {
113
- targetListId,
114
- taskCount: taskIds.length,
115
- error: error instanceof Error ? error.message : String(error)
116
- });
117
- throw new ClickUpServiceError(`Failed to move tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { targetListId, taskCount: taskIds.length });
147
+ logger.error('Bulk move operation failed', error);
148
+ throw error;
118
149
  }
119
150
  }
120
151
  /**
@@ -13,13 +13,13 @@ export { BaseClickUpService, ClickUpServiceError, ErrorCode } from './base.js';
13
13
  export * from './types.js';
14
14
  // Export service modules
15
15
  export { WorkspaceService } from './workspace.js';
16
- export { TaskService } from './task.js';
16
+ export { TaskService } from './task/index.js';
17
17
  export { ListService } from './list.js';
18
18
  export { FolderService } from './folder.js';
19
19
  export { ClickUpTagService } from './tag.js';
20
20
  // Import service classes for the factory function
21
21
  import { WorkspaceService } from './workspace.js';
22
- import { TaskService } from './task.js';
22
+ import { TaskService } from './task/index.js';
23
23
  import { ListService } from './list.js';
24
24
  import { FolderService } from './folder.js';
25
25
  import { ClickUpTagService } from './tag.js';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Module Exports
6
+ *
7
+ * Exports all task-related functionality:
8
+ * - Core task operations (CRUD)
9
+ * - Task searching and filtering
10
+ * - Task comments
11
+ * - File attachments
12
+ * - Task tags
13
+ * - Custom fields
14
+ */
15
+ // Export the main TaskService class
16
+ export { TaskService } from './task-service.js';
17
+ // Export all component services
18
+ export { TaskServiceCore } from './task-core.js';
19
+ export { TaskServiceSearch } from './task-search.js';
20
+ export { TaskServiceAttachments } from './task-attachments.js';
21
+ export { TaskServiceComments } from './task-comments.js';
22
+ export { TaskServiceTags } from './task-tags.js';
23
+ export { TaskServiceCustomFields } from './task-custom-fields.js';
24
+ // Export types and interfaces from all modules
25
+ export * from './task-core.js';
26
+ export * from './task-search.js';
27
+ export * from './task-attachments.js';
28
+ export * from './task-comments.js';
29
+ export * from './task-tags.js';
30
+ export * from './task-custom-fields.js';
31
+ // Re-export TaskService as the default export
32
+ export { TaskService as default } from './task-service.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Attachments Module
6
+ *
7
+ * Handles file attachment operations for ClickUp tasks, supporting three methods:
8
+ * - Uploading file attachments from base64/buffer data
9
+ * - Uploading file attachments from a URL (web URLs like http/https)
10
+ * - Uploading file attachments from local file paths (absolute paths)
11
+ */
12
+ import { TaskServiceSearch } from './task-search.js';
13
+ /**
14
+ * Attachment functionality for the TaskService
15
+ */
16
+ export class TaskServiceAttachments extends TaskServiceSearch {
17
+ /**
18
+ * Upload a file attachment to a ClickUp task
19
+ * @param taskId The ID of the task to attach the file to
20
+ * @param fileData The file data as a Buffer
21
+ * @param fileName The name of the file
22
+ * @returns Promise resolving to the attachment response from ClickUp
23
+ */
24
+ async uploadTaskAttachment(taskId, fileData, fileName) {
25
+ this.logOperation('uploadTaskAttachment', { taskId, fileName, fileSize: fileData.length });
26
+ try {
27
+ return await this.makeRequest(async () => {
28
+ // Create FormData for multipart/form-data upload
29
+ const FormData = (await import('form-data')).default;
30
+ const formData = new FormData();
31
+ // Add the file to the form data
32
+ formData.append('attachment', fileData, {
33
+ filename: fileName,
34
+ contentType: 'application/octet-stream' // Let ClickUp determine the content type
35
+ });
36
+ // Use the raw axios client for this request since we need to handle FormData
37
+ const response = await this.client.post(`/task/${taskId}/attachment`, formData, {
38
+ headers: {
39
+ ...formData.getHeaders(),
40
+ 'Authorization': this.apiKey
41
+ }
42
+ });
43
+ return response.data;
44
+ });
45
+ }
46
+ catch (error) {
47
+ throw this.handleError(error, `Failed to upload attachment to task ${taskId}`);
48
+ }
49
+ }
50
+ /**
51
+ * Upload a file attachment to a ClickUp task from a URL
52
+ * @param taskId The ID of the task to attach the file to
53
+ * @param fileUrl The URL of the file to download and attach
54
+ * @param fileName Optional file name (if not provided, it will be extracted from the URL)
55
+ * @param authHeader Optional authorization header for the URL
56
+ * @returns Promise resolving to the attachment response from ClickUp
57
+ */
58
+ async uploadTaskAttachmentFromUrl(taskId, fileUrl, fileName, authHeader) {
59
+ this.logOperation('uploadTaskAttachmentFromUrl', { taskId, fileUrl, fileName });
60
+ try {
61
+ return await this.makeRequest(async () => {
62
+ // Import required modules
63
+ const axios = (await import('axios')).default;
64
+ const FormData = (await import('form-data')).default;
65
+ // Download the file from the URL
66
+ const headers = {};
67
+ if (authHeader) {
68
+ headers['Authorization'] = authHeader;
69
+ }
70
+ const response = await axios.get(fileUrl, {
71
+ responseType: 'arraybuffer',
72
+ headers
73
+ });
74
+ // Extract filename from URL if not provided
75
+ const actualFileName = fileName || fileUrl.split('/').pop() || 'downloaded-file';
76
+ // Create FormData for multipart/form-data upload
77
+ const formData = new FormData();
78
+ // Add the file to the form data
79
+ formData.append('attachment', Buffer.from(response.data), {
80
+ filename: actualFileName,
81
+ contentType: 'application/octet-stream'
82
+ });
83
+ // Upload the file to ClickUp
84
+ const uploadResponse = await this.client.post(`/task/${taskId}/attachment`, formData, {
85
+ headers: {
86
+ ...formData.getHeaders(),
87
+ 'Authorization': this.apiKey
88
+ }
89
+ });
90
+ return uploadResponse.data;
91
+ });
92
+ }
93
+ catch (error) {
94
+ throw this.handleError(error, `Failed to upload attachment from URL to task ${taskId}`);
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Comments Module
6
+ *
7
+ * Handles comment operations for ClickUp tasks, including:
8
+ * - Retrieving comments for a task
9
+ * - Creating comments on a task
10
+ */
11
+ import { TaskServiceAttachments } from './task-attachments.js';
12
+ /**
13
+ * Comments functionality for the TaskService
14
+ */
15
+ export class TaskServiceComments extends TaskServiceAttachments {
16
+ /**
17
+ * Get all comments for a task
18
+ *
19
+ * @param taskId ID of the task to get comments for
20
+ * @param start Optional pagination start
21
+ * @param startId Optional comment ID to start from
22
+ * @returns Array of task comments
23
+ */
24
+ async getTaskComments(taskId, start, startId) {
25
+ this.logOperation('getTaskComments', { taskId, start, startId });
26
+ try {
27
+ // Build query parameters for pagination
28
+ const queryParams = new URLSearchParams();
29
+ if (start !== undefined) {
30
+ queryParams.append('start', start.toString());
31
+ }
32
+ if (startId) {
33
+ queryParams.append('start_id', startId);
34
+ }
35
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
36
+ const response = await this.client.get(`/task/${taskId}/comment${queryString}`);
37
+ return response.data.comments || [];
38
+ }
39
+ catch (error) {
40
+ throw this.handleError(error, 'Failed to get task comments');
41
+ }
42
+ }
43
+ /**
44
+ * Create a comment on a task
45
+ *
46
+ * @param taskId ID of the task to comment on
47
+ * @param commentText Text content of the comment
48
+ * @param notifyAll Whether to notify all assignees
49
+ * @param assignee Optional user ID to assign the comment to
50
+ * @returns The created comment
51
+ */
52
+ async createTaskComment(taskId, commentText, notifyAll = false, assignee) {
53
+ this.logOperation('createTaskComment', { taskId, commentText, notifyAll, assignee });
54
+ try {
55
+ const payload = {
56
+ comment_text: commentText,
57
+ notify_all: notifyAll
58
+ };
59
+ if (assignee) {
60
+ payload.assignee = assignee;
61
+ }
62
+ // Make the request directly without using makeRequest for better error handling
63
+ const response = await this.client.post(`/task/${taskId}/comment`, payload);
64
+ // Handle different response formats from ClickUp API
65
+ if (response.data) {
66
+ if (response.data.comment) {
67
+ // Standard format: { comment: ClickUpComment }
68
+ return response.data.comment;
69
+ }
70
+ else if (response.data.id && (response.data.comment_text || response.data.comment)) {
71
+ // Direct format: the comment object itself
72
+ return response.data;
73
+ }
74
+ else {
75
+ // Fallback: construct a minimal valid comment object
76
+ return {
77
+ id: response.data.id || `custom-${Date.now()}`,
78
+ comment: response.data.comment || commentText,
79
+ comment_text: response.data.comment_text || commentText,
80
+ user: response.data.user || { id: 0, username: 'Unknown', email: '', color: '' },
81
+ date: response.data.date || new Date().toISOString(),
82
+ resolved: false
83
+ };
84
+ }
85
+ }
86
+ throw new Error('Invalid response from ClickUp API');
87
+ }
88
+ catch (error) {
89
+ // Check if comment might have been created despite error
90
+ if (error.response?.status === 200 || error.response?.status === 201) {
91
+ // Try to construct a comment object from what we know
92
+ return {
93
+ id: `fallback-${Date.now()}`,
94
+ comment: commentText,
95
+ comment_text: commentText,
96
+ user: { id: 0, username: 'Unknown', email: '', color: '' },
97
+ date: new Date().toISOString(),
98
+ resolved: false
99
+ };
100
+ }
101
+ throw this.handleError(error, 'Failed to create task comment');
102
+ }
103
+ }
104
+ }