@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.
- package/README.md +6 -6
- 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 +111 -69
- 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 +439 -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 +49 -20
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +102 -18
- package/build/tools/task/handlers.js +216 -53
- package/build/tools/task/index.js +1 -1
- package/build/tools/task/main.js +161 -32
- package/build/tools/task/single-operations.js +82 -17
- package/build/tools/task/utilities.js +47 -75
- 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/mcp-tools.js +0 -64
- package/build/server-state.js +0 -93
- package/build/server.log +0 -0
- package/build/services/clickup/task.js +0 -701
- package/build/tools/bulk-tasks.js +0 -36
- package/build/tools/debug.js +0 -76
- package/build/tools/logs.js +0 -55
- package/build/tools/task.js +0 -1554
- package/build/utils/params-utils.js +0 -39
- package/build/utils/sponsor-analytics.js +0 -100
- 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.
|
|
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/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
|
|
@@ -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
|
|
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
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
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, (
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
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 });
|
|
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
|
|
88
|
-
*
|
|
89
|
-
* @param
|
|
90
|
-
* @param
|
|
91
|
-
* @
|
|
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(
|
|
95
|
-
logger.info(
|
|
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
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
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 });
|
|
169
|
+
logger.error('Bulk move operation failed', error);
|
|
170
|
+
throw error;
|
|
118
171
|
}
|
|
119
172
|
}
|
|
120
173
|
/**
|
|
121
|
-
* Delete multiple tasks
|
|
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(
|
|
128
|
-
logger.info(
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
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(
|
|
144
|
-
|
|
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
|
+
}
|