@taazkareem/clickup-mcp-server 0.6.1 → 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.
- package/README.md +6 -6
- package/build/index.js +1 -0
- package/build/logger.js +26 -1
- package/build/server.js +1 -1
- package/build/services/clickup/base.js +22 -1
- package/build/services/clickup/bulk.js +76 -45
- package/build/services/clickup/index.js +2 -2
- package/build/services/clickup/task/index.js +32 -0
- package/build/services/clickup/task/task-attachments.js +97 -0
- package/build/services/clickup/task/task-comments.js +104 -0
- package/build/services/clickup/task/task-core.js +477 -0
- package/build/services/clickup/task/task-custom-fields.js +97 -0
- package/build/services/clickup/task/task-search.js +462 -0
- package/build/services/clickup/task/task-service.js +25 -0
- package/build/services/clickup/task/task-tags.js +101 -0
- package/build/services/clickup/workspace.js +81 -36
- package/build/tools/folder.js +1 -1
- package/build/tools/list.js +2 -4
- package/build/tools/task/attachments.js +18 -5
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +111 -15
- package/build/tools/task/handlers.js +169 -24
- package/build/tools/task/index.js +1 -1
- package/build/tools/task/main.js +36 -1
- package/build/tools/task/single-operations.js +51 -4
- package/build/tools/task/utilities.js +24 -71
- package/build/tools/utils.js +2 -2
- package/build/utils/date-utils.js +149 -30
- package/build/utils/resolver-utils.js +33 -40
- package/build/utils/sponsor-service.js +1 -1
- package/package.json +1 -1
- package/build/services/clickup/task.js +0 -701
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.
|
|
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 | •
|
|
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`)
|
|
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
|
|
73
|
-
| [get_task](docs/api-reference.md#task-management) | Get single task details | `taskId`/`taskName
|
|
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/index.js
CHANGED
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
|
-
|
|
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
|
@@ -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
|
|
59
|
-
*
|
|
60
|
-
* @param
|
|
61
|
-
* @
|
|
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(
|
|
66
|
-
batchSize: options?.batchSize,
|
|
67
|
-
concurrency: options?.concurrency
|
|
68
|
-
});
|
|
64
|
+
logger.info('Starting bulk update operation', { taskCount: tasks.length });
|
|
69
65
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
80
|
-
|
|
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
|
|
88
|
-
*
|
|
89
|
-
* @param
|
|
90
|
-
* @param
|
|
91
|
-
* @
|
|
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(
|
|
95
|
-
logger.info(
|
|
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
|
|
113
|
+
// First validate the destination list exists
|
|
101
114
|
await this.taskService.validateListExists(targetListId);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
113
|
-
|
|
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
|
+
}
|