@taazkareem/clickup-mcp-server 0.6.2 → 0.6.4

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 +111 -69
  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 +439 -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 +49 -20
  19. package/build/tools/task/attachments.types.js +9 -0
  20. package/build/tools/task/bulk-operations.js +102 -18
  21. package/build/tools/task/handlers.js +216 -53
  22. package/build/tools/task/index.js +1 -1
  23. package/build/tools/task/main.js +161 -32
  24. package/build/tools/task/single-operations.js +82 -17
  25. package/build/tools/task/utilities.js +47 -75
  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.4",
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
@@ -7,15 +7,21 @@
7
7
  * Enhanced implementation for bulk operations that leverages the existing single-operation methods.
8
8
  * This approach reduces code duplication while offering powerful concurrency management.
9
9
  */
10
- import { ClickUpServiceError, ErrorCode } from './base.js';
11
- import { processBatch } from '../../utils/concurrency-utils.js';
12
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';
13
15
  // Create logger instance
14
16
  const logger = new Logger('BulkService');
15
17
  /**
16
- * Service for handling bulk operations in ClickUp
18
+ * Service for performing bulk operations in ClickUp
17
19
  */
18
20
  export class BulkService {
21
+ /**
22
+ * Create a new bulk service
23
+ * @param taskService ClickUp Task Service instance
24
+ */
19
25
  constructor(taskService) {
20
26
  this.taskService = taskService;
21
27
  logger.info('BulkService initialized');
@@ -55,96 +61,132 @@ export class BulkService {
55
61
  }
56
62
  }
57
63
  /**
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
64
+ * Find task by name within a specific list
65
+ */
66
+ async findTaskInList(taskName, listName) {
67
+ try {
68
+ // First get the list ID using the global lookup utility
69
+ const listInfo = await findListIDByName(clickUpServices.workspace, listName);
70
+ if (!listInfo) {
71
+ throw new ClickUpServiceError(`List "${listName}" not found`, ErrorCode.NOT_FOUND);
72
+ }
73
+ logger.info(`List "${listName}" resolved to ID: ${listInfo.id}`);
74
+ // Get tasks from the list using the resolved ID
75
+ const taskList = await this.taskService.getTasks(listInfo.id);
76
+ // Find the task by name - first try exact match
77
+ let matchingTask = taskList.find(t => t.name === taskName);
78
+ // If no exact match, try case-insensitive match
79
+ if (!matchingTask) {
80
+ matchingTask = taskList.find(t => t.name.toLowerCase() === taskName.toLowerCase());
81
+ // If still no match, try substring match as a fallback
82
+ if (!matchingTask) {
83
+ matchingTask = taskList.find(t => t.name.toLowerCase().includes(taskName.toLowerCase()) ||
84
+ taskName.toLowerCase().includes(t.name.toLowerCase()));
85
+ }
86
+ }
87
+ if (!matchingTask) {
88
+ throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
89
+ }
90
+ logger.info(`Task "${taskName}" found with ID: ${matchingTask.id}`);
91
+ return matchingTask.id;
92
+ }
93
+ catch (error) {
94
+ // Enhance the error message
95
+ if (error instanceof ClickUpServiceError) {
96
+ throw error;
97
+ }
98
+ throw new ClickUpServiceError(`Error finding task "${taskName}" in list "${listName}": ${error instanceof Error ? error.message : String(error)}`, ErrorCode.UNKNOWN);
99
+ }
100
+ }
101
+ /**
102
+ * Resolve task ID using provided identifiers
103
+ */
104
+ async resolveTaskId(task) {
105
+ const { taskId, taskName, listName, customTaskId } = task;
106
+ if (taskId) {
107
+ return taskId;
108
+ }
109
+ if (customTaskId) {
110
+ const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
111
+ return resolvedTask.id;
112
+ }
113
+ if (taskName && listName) {
114
+ return await this.findTaskInList(taskName, listName);
115
+ }
116
+ throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
117
+ }
118
+ /**
119
+ * Update multiple tasks
120
+ * @param tasks Array of tasks to update with their new data
121
+ * @param options Optional batch processing settings
122
+ * @returns Array of updated tasks
63
123
  */
64
124
  async updateTasks(tasks, options) {
65
- logger.info(`Updating ${tasks.length} tasks`, {
66
- batchSize: options?.batchSize,
67
- concurrency: options?.concurrency
68
- });
125
+ logger.info('Starting bulk update operation', { taskCount: tasks.length });
69
126
  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);
127
+ return await processBatch(tasks, async (task) => {
128
+ const { taskId, taskName, listName, customTaskId, ...updateData } = task;
129
+ const resolvedTaskId = await this.resolveTaskId({ taskId, taskName, listName, customTaskId });
130
+ return await this.taskService.updateTask(resolvedTaskId, updateData);
76
131
  }, options);
77
132
  }
78
133
  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 });
134
+ logger.error('Bulk update operation failed', error);
135
+ throw error;
84
136
  }
85
137
  }
86
138
  /**
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
139
+ * Move multiple tasks to a different list
140
+ * @param tasks Array of tasks to move (each with taskId or taskName + listName)
141
+ * @param targetListId ID of the destination list or list name
142
+ * @param options Optional batch processing settings
143
+ * @returns Array of moved tasks
93
144
  */
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
- });
145
+ async moveTasks(tasks, targetListId, options) {
146
+ logger.info('Starting bulk move operation', { taskCount: tasks.length, targetListId });
99
147
  try {
100
- // First validate that the target list exists - do this once for all tasks
101
- 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);
148
+ // Determine if targetListId is actually an ID or a name
149
+ let resolvedTargetListId = targetListId;
150
+ // If the targetListId doesn't match the pattern of a list ID (usually just numbers),
151
+ // assume it's a list name and try to resolve it
152
+ if (!/^\d+$/.test(targetListId)) {
153
+ logger.info(`Target list appears to be a name: "${targetListId}", attempting to resolve`);
154
+ const listInfo = await findListIDByName(clickUpServices.workspace, targetListId);
155
+ if (!listInfo) {
156
+ throw new ClickUpServiceError(`Target list "${targetListId}" not found`, ErrorCode.NOT_FOUND);
157
+ }
158
+ resolvedTargetListId = listInfo.id;
159
+ logger.info(`Resolved target list to ID: ${resolvedTargetListId}`);
160
+ }
161
+ // Validate the destination list exists
162
+ await this.taskService.validateListExists(resolvedTargetListId);
163
+ return await processBatch(tasks, async (task) => {
164
+ const resolvedTaskId = await this.resolveTaskId(task);
165
+ return await this.taskService.moveTask(resolvedTaskId, resolvedTargetListId);
109
166
  }, options);
110
167
  }
111
168
  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 });
169
+ logger.error('Bulk move operation failed', error);
170
+ throw error;
118
171
  }
119
172
  }
120
173
  /**
121
- * Delete multiple tasks efficiently
122
- *
123
- * @param taskIds Array of task IDs to delete
174
+ * Delete multiple tasks
175
+ * @param tasks Array of tasks to delete (each with taskId or taskName + listName)
124
176
  * @param options Batch processing options
125
177
  * @returns Results containing successful and failed deletions
126
178
  */
127
- async deleteTasks(taskIds, options) {
128
- logger.info(`Deleting ${taskIds.length} tasks`, {
129
- batchSize: options?.batchSize,
130
- concurrency: options?.concurrency
131
- });
179
+ async deleteTasks(tasks, options) {
180
+ logger.info('Starting bulk delete operation', { taskCount: tasks.length });
132
181
  try {
133
- return await processBatch(taskIds, async (taskId, index) => {
134
- logger.debug(`Deleting task ${index + 1}/${taskIds.length}`, {
135
- taskId
136
- });
137
- // Reuse the single-task delete method
138
- await this.taskService.deleteTask(taskId);
139
- return taskId; // Return the ID for successful deletions
182
+ return await processBatch(tasks, async (task) => {
183
+ const resolvedTaskId = await this.resolveTaskId(task);
184
+ await this.taskService.deleteTask(resolvedTaskId);
140
185
  }, options);
141
186
  }
142
187
  catch (error) {
143
- logger.error(`Failed to delete tasks in bulk`, {
144
- taskCount: taskIds.length,
145
- error: error instanceof Error ? error.message : String(error)
146
- });
147
- throw new ClickUpServiceError(`Failed to delete tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { taskCount: taskIds.length });
188
+ logger.error('Bulk delete operation failed', error);
189
+ throw error;
148
190
  }
149
191
  }
150
192
  }
@@ -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
+ }