@taazkareem/clickup-mcp-server 0.4.60 → 0.4.63
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 +10 -2
- package/build/config.js +10 -0
- package/build/index.js +9 -1356
- package/build/server.js +120 -0
- package/build/services/clickup/base.js +253 -0
- package/build/services/clickup/bulk.js +116 -0
- package/build/services/clickup/folder.js +133 -0
- package/build/services/clickup/index.js +43 -0
- package/build/services/clickup/initialization.js +28 -0
- package/build/services/clickup/list.js +188 -0
- package/build/services/clickup/task.js +492 -0
- package/build/services/clickup/types.js +4 -0
- package/build/services/clickup/workspace.js +314 -0
- package/build/services/shared.js +15 -0
- package/build/tools/folder.js +356 -0
- package/build/tools/index.js +11 -0
- package/build/tools/list.js +452 -0
- package/build/tools/task.js +1519 -0
- package/build/tools/utils.js +150 -0
- package/build/tools/workspace.js +132 -0
- package/package.json +3 -1
- package/build/services/clickup.js +0 -765
- package/build/types/clickup.js +0 -1
package/build/server.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { createClickUpServices } from "./services/clickup/index.js";
|
|
4
|
+
import config from "./config.js";
|
|
5
|
+
import { workspaceHierarchyTool, handleGetWorkspaceHierarchy } from "./tools/workspace.js";
|
|
6
|
+
import { createTaskTool, handleCreateTask, updateTaskTool, handleUpdateTask, moveTaskTool, handleMoveTask, duplicateTaskTool, handleDuplicateTask, getTaskTool, handleGetTask, getTasksTool, handleGetTasks, deleteTaskTool, handleDeleteTask, createBulkTasksTool, handleCreateBulkTasks, updateBulkTasksTool, handleUpdateBulkTasks, moveBulkTasksTool, handleMoveBulkTasks, deleteBulkTasksTool, handleDeleteBulkTasks } from "./tools/task.js";
|
|
7
|
+
import { createListTool, handleCreateList, createListInFolderTool, handleCreateListInFolder, getListTool, handleGetList, updateListTool, handleUpdateList, deleteListTool, handleDeleteList } from "./tools/list.js";
|
|
8
|
+
import { createFolderTool, handleCreateFolder, getFolderTool, handleGetFolder, updateFolderTool, handleUpdateFolder, deleteFolderTool, handleDeleteFolder } from "./tools/folder.js";
|
|
9
|
+
// Initialize ClickUp services
|
|
10
|
+
const services = createClickUpServices({
|
|
11
|
+
apiKey: config.clickupApiKey,
|
|
12
|
+
teamId: config.clickupTeamId
|
|
13
|
+
});
|
|
14
|
+
// Extract the workspace service for use in this module
|
|
15
|
+
const { workspace } = services;
|
|
16
|
+
/**
|
|
17
|
+
* MCP Server for ClickUp integration
|
|
18
|
+
*/
|
|
19
|
+
export const server = new Server({
|
|
20
|
+
name: "clickup-mcp-server",
|
|
21
|
+
version: "0.4.61",
|
|
22
|
+
}, {
|
|
23
|
+
capabilities: {
|
|
24
|
+
tools: {},
|
|
25
|
+
prompts: {},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Configure the server routes and handlers
|
|
30
|
+
*/
|
|
31
|
+
export function configureServer() {
|
|
32
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
33
|
+
return {
|
|
34
|
+
tools: [
|
|
35
|
+
workspaceHierarchyTool,
|
|
36
|
+
createTaskTool,
|
|
37
|
+
getTaskTool,
|
|
38
|
+
getTasksTool,
|
|
39
|
+
updateTaskTool,
|
|
40
|
+
moveTaskTool,
|
|
41
|
+
duplicateTaskTool,
|
|
42
|
+
deleteTaskTool,
|
|
43
|
+
createBulkTasksTool,
|
|
44
|
+
updateBulkTasksTool,
|
|
45
|
+
moveBulkTasksTool,
|
|
46
|
+
deleteBulkTasksTool,
|
|
47
|
+
createListTool,
|
|
48
|
+
createListInFolderTool,
|
|
49
|
+
getListTool,
|
|
50
|
+
updateListTool,
|
|
51
|
+
deleteListTool,
|
|
52
|
+
createFolderTool,
|
|
53
|
+
getFolderTool,
|
|
54
|
+
updateFolderTool,
|
|
55
|
+
deleteFolderTool
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
60
|
+
const { name, arguments: params } = req.params;
|
|
61
|
+
// Handle tool calls by routing to the appropriate handler
|
|
62
|
+
switch (name) {
|
|
63
|
+
case "get_workspace_hierarchy":
|
|
64
|
+
return handleGetWorkspaceHierarchy();
|
|
65
|
+
case "create_task":
|
|
66
|
+
return handleCreateTask(params);
|
|
67
|
+
case "update_task":
|
|
68
|
+
return handleUpdateTask(params);
|
|
69
|
+
case "move_task":
|
|
70
|
+
return handleMoveTask(params);
|
|
71
|
+
case "duplicate_task":
|
|
72
|
+
return handleDuplicateTask(params);
|
|
73
|
+
case "get_task":
|
|
74
|
+
return handleGetTask(params);
|
|
75
|
+
case "get_tasks":
|
|
76
|
+
return handleGetTasks(params);
|
|
77
|
+
case "delete_task":
|
|
78
|
+
return handleDeleteTask(params);
|
|
79
|
+
case "create_bulk_tasks":
|
|
80
|
+
return handleCreateBulkTasks(params);
|
|
81
|
+
case "update_bulk_tasks":
|
|
82
|
+
return handleUpdateBulkTasks(params);
|
|
83
|
+
case "move_bulk_tasks":
|
|
84
|
+
return handleMoveBulkTasks(params);
|
|
85
|
+
case "delete_bulk_tasks":
|
|
86
|
+
return handleDeleteBulkTasks(params);
|
|
87
|
+
case "create_list":
|
|
88
|
+
return handleCreateList(params);
|
|
89
|
+
case "create_list_in_folder":
|
|
90
|
+
return handleCreateListInFolder(params);
|
|
91
|
+
case "get_list":
|
|
92
|
+
return handleGetList(params);
|
|
93
|
+
case "update_list":
|
|
94
|
+
return handleUpdateList(params);
|
|
95
|
+
case "delete_list":
|
|
96
|
+
return handleDeleteList(params);
|
|
97
|
+
case "create_folder":
|
|
98
|
+
return handleCreateFolder(params);
|
|
99
|
+
case "get_folder":
|
|
100
|
+
return handleGetFolder(params);
|
|
101
|
+
case "update_folder":
|
|
102
|
+
return handleUpdateFolder(params);
|
|
103
|
+
case "delete_folder":
|
|
104
|
+
return handleDeleteFolder(params);
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
110
|
+
return { prompts: [] };
|
|
111
|
+
});
|
|
112
|
+
server.setRequestHandler(GetPromptRequestSchema, async () => {
|
|
113
|
+
throw new Error("Prompt not found");
|
|
114
|
+
});
|
|
115
|
+
return server;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Export the clickup service for use in tool handlers
|
|
119
|
+
*/
|
|
120
|
+
export { workspace };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base ClickUp Service Class
|
|
3
|
+
*
|
|
4
|
+
* This class provides core functionality for all ClickUp service modules:
|
|
5
|
+
* - Axios client configuration
|
|
6
|
+
* - Rate limiting and request throttling
|
|
7
|
+
* - Error handling
|
|
8
|
+
* - Common request methods
|
|
9
|
+
*/
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
/**
|
|
12
|
+
* Error types for better error handling
|
|
13
|
+
*/
|
|
14
|
+
export var ErrorCode;
|
|
15
|
+
(function (ErrorCode) {
|
|
16
|
+
ErrorCode["RATE_LIMIT"] = "rate_limit_exceeded";
|
|
17
|
+
ErrorCode["NOT_FOUND"] = "resource_not_found";
|
|
18
|
+
ErrorCode["UNAUTHORIZED"] = "unauthorized";
|
|
19
|
+
ErrorCode["VALIDATION"] = "validation_error";
|
|
20
|
+
ErrorCode["SERVER_ERROR"] = "server_error";
|
|
21
|
+
ErrorCode["NETWORK_ERROR"] = "network_error";
|
|
22
|
+
ErrorCode["WORKSPACE_ERROR"] = "workspace_error";
|
|
23
|
+
ErrorCode["INVALID_PARAMETER"] = "invalid_parameter";
|
|
24
|
+
ErrorCode["UNKNOWN"] = "unknown_error";
|
|
25
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
26
|
+
/**
|
|
27
|
+
* Custom error class for ClickUp API errors
|
|
28
|
+
*/
|
|
29
|
+
export class ClickUpServiceError extends Error {
|
|
30
|
+
constructor(message, code = ErrorCode.UNKNOWN, data, status, context) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'ClickUpServiceError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.data = data;
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.context = context;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Base ClickUp service class that handles common functionality
|
|
41
|
+
*/
|
|
42
|
+
export class BaseClickUpService {
|
|
43
|
+
/**
|
|
44
|
+
* Creates an instance of BaseClickUpService.
|
|
45
|
+
* @param apiKey - ClickUp API key for authentication
|
|
46
|
+
* @param teamId - ClickUp team ID for targeting the correct workspace
|
|
47
|
+
* @param baseUrl - Optional custom base URL for the ClickUp API
|
|
48
|
+
*/
|
|
49
|
+
constructor(apiKey, teamId, baseUrl = 'https://api.clickup.com/api/v2') {
|
|
50
|
+
this.defaultRequestSpacing = 600; // Default milliseconds between requests
|
|
51
|
+
this.rateLimit = 100; // Maximum requests per minute (Free Forever plan)
|
|
52
|
+
this.timeout = 65000; // 65 seconds (safely under the 1-minute window)
|
|
53
|
+
this.requestQueue = [];
|
|
54
|
+
this.processingQueue = false;
|
|
55
|
+
this.lastRateLimitReset = 0;
|
|
56
|
+
this.apiKey = apiKey;
|
|
57
|
+
this.teamId = teamId;
|
|
58
|
+
this.requestSpacing = this.defaultRequestSpacing;
|
|
59
|
+
// Configure the Axios client with default settings
|
|
60
|
+
this.client = axios.create({
|
|
61
|
+
baseURL: baseUrl,
|
|
62
|
+
headers: {
|
|
63
|
+
'Authorization': apiKey,
|
|
64
|
+
'Content-Type': 'application/json'
|
|
65
|
+
},
|
|
66
|
+
timeout: this.timeout
|
|
67
|
+
});
|
|
68
|
+
// Add response interceptor for error handling
|
|
69
|
+
this.client.interceptors.response.use(response => response, error => this.handleAxiosError(error));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Handle errors from Axios requests
|
|
73
|
+
* @private
|
|
74
|
+
* @param error Error from Axios
|
|
75
|
+
* @returns Never - always throws an error
|
|
76
|
+
*/
|
|
77
|
+
handleAxiosError(error) {
|
|
78
|
+
let message = 'Unknown error occurred';
|
|
79
|
+
let code = ErrorCode.UNKNOWN;
|
|
80
|
+
let details = null;
|
|
81
|
+
let status = undefined;
|
|
82
|
+
if (error.response) {
|
|
83
|
+
// Server responded with an error status code
|
|
84
|
+
status = error.response.status;
|
|
85
|
+
details = error.response.data;
|
|
86
|
+
switch (status) {
|
|
87
|
+
case 401:
|
|
88
|
+
message = 'Unauthorized: Invalid API key';
|
|
89
|
+
code = ErrorCode.UNAUTHORIZED;
|
|
90
|
+
break;
|
|
91
|
+
case 403:
|
|
92
|
+
message = 'Forbidden: Insufficient permissions';
|
|
93
|
+
code = ErrorCode.UNAUTHORIZED;
|
|
94
|
+
break;
|
|
95
|
+
case 404:
|
|
96
|
+
message = 'Resource not found';
|
|
97
|
+
code = ErrorCode.NOT_FOUND;
|
|
98
|
+
break;
|
|
99
|
+
case 429:
|
|
100
|
+
message = 'Rate limit exceeded';
|
|
101
|
+
code = ErrorCode.RATE_LIMIT;
|
|
102
|
+
break;
|
|
103
|
+
case 400:
|
|
104
|
+
message = 'Invalid request: ' + (error.response.data?.err || 'Validation error');
|
|
105
|
+
code = ErrorCode.VALIDATION;
|
|
106
|
+
break;
|
|
107
|
+
case 500:
|
|
108
|
+
case 502:
|
|
109
|
+
case 503:
|
|
110
|
+
case 504:
|
|
111
|
+
message = 'ClickUp server error';
|
|
112
|
+
code = ErrorCode.SERVER_ERROR;
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
message = `ClickUp API error (${status}): ${error.response.data?.err || 'Unknown error'}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (error.request) {
|
|
119
|
+
// Request was made but no response received
|
|
120
|
+
message = 'Network error: No response received from ClickUp';
|
|
121
|
+
code = ErrorCode.NETWORK_ERROR;
|
|
122
|
+
details = { request: error.request };
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Error setting up the request
|
|
126
|
+
message = `Request setup error: ${error.message}`;
|
|
127
|
+
details = { message: error.message };
|
|
128
|
+
}
|
|
129
|
+
throw new ClickUpServiceError(message, code, details, status);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Process the request queue, respecting rate limits by spacing out requests
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
async processQueue() {
|
|
136
|
+
if (this.processingQueue || this.requestQueue.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.processingQueue = true;
|
|
140
|
+
try {
|
|
141
|
+
while (this.requestQueue.length > 0) {
|
|
142
|
+
const request = this.requestQueue.shift();
|
|
143
|
+
if (request) {
|
|
144
|
+
try {
|
|
145
|
+
await request();
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error('Request failed:', error);
|
|
149
|
+
// Continue processing queue even if one request fails
|
|
150
|
+
}
|
|
151
|
+
// Space out requests to stay within rate limit
|
|
152
|
+
if (this.requestQueue.length > 0) {
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, this.requestSpacing));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
this.processingQueue = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Handle rate limit headers from ClickUp API
|
|
164
|
+
* @private
|
|
165
|
+
* @param headers Response headers from ClickUp
|
|
166
|
+
*/
|
|
167
|
+
handleRateLimitHeaders(headers) {
|
|
168
|
+
const limit = parseInt(headers['x-ratelimit-limit'], 10);
|
|
169
|
+
const remaining = parseInt(headers['x-ratelimit-remaining'], 10);
|
|
170
|
+
const reset = parseInt(headers['x-ratelimit-reset'], 10);
|
|
171
|
+
if (!isNaN(reset)) {
|
|
172
|
+
this.lastRateLimitReset = reset;
|
|
173
|
+
}
|
|
174
|
+
// If we're running low on remaining requests, increase spacing
|
|
175
|
+
if (!isNaN(remaining) && remaining < 10) {
|
|
176
|
+
const timeUntilReset = (this.lastRateLimitReset * 1000) - Date.now();
|
|
177
|
+
if (timeUntilReset > 0) {
|
|
178
|
+
this.requestSpacing = Math.max(this.defaultRequestSpacing, Math.floor(timeUntilReset / remaining));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.requestSpacing = this.defaultRequestSpacing; // Reset to default spacing
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Makes an API request with rate limiting.
|
|
187
|
+
* @protected
|
|
188
|
+
* @param fn - Function that executes the API request
|
|
189
|
+
* @returns Promise that resolves with the result of the API request
|
|
190
|
+
*/
|
|
191
|
+
async makeRequest(fn) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
this.requestQueue.push(async () => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await fn();
|
|
196
|
+
// Handle rate limit headers if present
|
|
197
|
+
if (result && typeof result === 'object' && 'headers' in result) {
|
|
198
|
+
this.handleRateLimitHeaders(result.headers);
|
|
199
|
+
}
|
|
200
|
+
resolve(result);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (axios.isAxiosError(error) && error.response?.status === 429) {
|
|
204
|
+
const retryAfter = parseInt(error.response.headers['retry-after'] || '60', 10);
|
|
205
|
+
const resetTime = parseInt(error.response.headers['x-ratelimit-reset'] || '0', 10);
|
|
206
|
+
// Use the more precise reset time if available
|
|
207
|
+
const waitTime = resetTime > 0 ?
|
|
208
|
+
(resetTime * 1000) - Date.now() :
|
|
209
|
+
retryAfter * 1000;
|
|
210
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
211
|
+
try {
|
|
212
|
+
// Retry the request once after waiting
|
|
213
|
+
const result = await fn();
|
|
214
|
+
resolve(result);
|
|
215
|
+
}
|
|
216
|
+
catch (retryError) {
|
|
217
|
+
reject(retryError);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
reject(error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
this.processQueue().catch(reject);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Gets the ClickUp team ID associated with this service instance
|
|
230
|
+
* @returns The team ID
|
|
231
|
+
*/
|
|
232
|
+
getTeamId() {
|
|
233
|
+
return this.teamId;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Helper method to log API operations
|
|
237
|
+
* @protected
|
|
238
|
+
* @param operation - Name of the operation being performed
|
|
239
|
+
* @param details - Details about the operation
|
|
240
|
+
*/
|
|
241
|
+
logOperation(operation, details) {
|
|
242
|
+
// This could be enhanced to use a proper logging framework
|
|
243
|
+
console.log(`[ClickUpService] ${operation}:`, details);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Protected helper method to check if cache is available
|
|
247
|
+
* @param cacheService Optional cache service instance
|
|
248
|
+
* @returns True if caching is available and enabled
|
|
249
|
+
*/
|
|
250
|
+
isCacheEnabled(cacheService) {
|
|
251
|
+
return !!cacheService && typeof cacheService.isEnabled === 'function' && cacheService.isEnabled();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export class BulkProcessor {
|
|
2
|
+
async processBulk(items, processor, options) {
|
|
3
|
+
const opts = {
|
|
4
|
+
batchSize: options?.batchSize ?? 10,
|
|
5
|
+
concurrency: options?.concurrency ?? 3,
|
|
6
|
+
continueOnError: options?.continueOnError ?? false,
|
|
7
|
+
retryCount: options?.retryCount ?? 3,
|
|
8
|
+
retryDelay: options?.retryDelay ?? 1000,
|
|
9
|
+
exponentialBackoff: options?.exponentialBackoff ?? true,
|
|
10
|
+
onProgress: options?.onProgress ?? (() => { })
|
|
11
|
+
};
|
|
12
|
+
const result = {
|
|
13
|
+
success: true,
|
|
14
|
+
successfulItems: [],
|
|
15
|
+
failedItems: [],
|
|
16
|
+
totalItems: items.length,
|
|
17
|
+
successCount: 0,
|
|
18
|
+
failureCount: 0
|
|
19
|
+
};
|
|
20
|
+
if (items.length === 0)
|
|
21
|
+
return result;
|
|
22
|
+
try {
|
|
23
|
+
const totalBatches = Math.ceil(items.length / opts.batchSize);
|
|
24
|
+
let processedItems = 0;
|
|
25
|
+
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
|
|
26
|
+
const startIdx = batchIndex * opts.batchSize;
|
|
27
|
+
const endIdx = Math.min(startIdx + opts.batchSize, items.length);
|
|
28
|
+
const batch = items.slice(startIdx, endIdx);
|
|
29
|
+
const batchResults = await this.processBatch(batch, processor, startIdx, opts);
|
|
30
|
+
result.successfulItems.push(...batchResults.successfulItems);
|
|
31
|
+
result.failedItems.push(...batchResults.failedItems);
|
|
32
|
+
result.successCount += batchResults.successCount;
|
|
33
|
+
result.failureCount += batchResults.failureCount;
|
|
34
|
+
if (batchResults.failureCount > 0 && !opts.continueOnError) {
|
|
35
|
+
result.success = false;
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
processedItems += batch.length;
|
|
39
|
+
opts.onProgress(processedItems, items.length, result.successCount, result.failureCount);
|
|
40
|
+
}
|
|
41
|
+
result.success = result.failedItems.length === 0;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const err = error;
|
|
46
|
+
console.error('Failed to process bulk operation:', err.message || String(error));
|
|
47
|
+
result.success = false;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async processBatch(batch, processor, startIndex, opts) {
|
|
52
|
+
const result = {
|
|
53
|
+
success: true,
|
|
54
|
+
successfulItems: [],
|
|
55
|
+
failedItems: [],
|
|
56
|
+
totalItems: batch.length,
|
|
57
|
+
successCount: 0,
|
|
58
|
+
failureCount: 0
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
for (let i = 0; i < batch.length; i += opts.concurrency) {
|
|
62
|
+
const concurrentBatch = batch.slice(i, Math.min(i + opts.concurrency, batch.length));
|
|
63
|
+
const promises = concurrentBatch.map((item, idx) => {
|
|
64
|
+
const index = startIndex + i + idx;
|
|
65
|
+
return this.processWithRetry(() => processor(item, index), index, item, opts);
|
|
66
|
+
});
|
|
67
|
+
const results = await Promise.allSettled(promises);
|
|
68
|
+
results.forEach((promiseResult, idx) => {
|
|
69
|
+
const index = startIndex + i + idx;
|
|
70
|
+
if (promiseResult.status === 'fulfilled') {
|
|
71
|
+
result.successfulItems.push(promiseResult.value);
|
|
72
|
+
result.successCount++;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const error = promiseResult.reason;
|
|
76
|
+
result.failedItems.push({ item: batch[i + idx], index, error });
|
|
77
|
+
result.failureCount++;
|
|
78
|
+
if (!opts.continueOnError) {
|
|
79
|
+
result.success = false;
|
|
80
|
+
throw new Error(`Bulk operation failed at index ${index}: ${error.message || String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const err = error;
|
|
89
|
+
console.error(`Bulk operation failed: ${err.message || String(error)}`, error);
|
|
90
|
+
result.success = false;
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async processWithRetry(operation, index, item, options) {
|
|
95
|
+
let attempts = 1;
|
|
96
|
+
let lastError = new Error('Unknown error');
|
|
97
|
+
while (attempts <= options.retryCount) {
|
|
98
|
+
try {
|
|
99
|
+
return await operation();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const err = error;
|
|
103
|
+
console.warn(`Operation failed for item at index ${index}, attempt ${attempts}/${options.retryCount}: ${err.message || String(error)}`);
|
|
104
|
+
lastError = err;
|
|
105
|
+
if (attempts >= options.retryCount)
|
|
106
|
+
break;
|
|
107
|
+
const delay = options.exponentialBackoff
|
|
108
|
+
? options.retryDelay * Math.pow(2, attempts) + Math.random() * 1000
|
|
109
|
+
: options.retryDelay * Math.pow(1.5, attempts - 1);
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
111
|
+
attempts++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Operation failed after ${attempts} attempts for item at index ${index}: ${lastError?.message || 'Unknown error'}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickUp Folder Service
|
|
3
|
+
*
|
|
4
|
+
* Handles all operations related to folders in ClickUp, including:
|
|
5
|
+
* - Creating folders
|
|
6
|
+
* - Retrieving folders
|
|
7
|
+
* - Updating folders
|
|
8
|
+
* - Deleting folders
|
|
9
|
+
* - Finding folders by name
|
|
10
|
+
*/
|
|
11
|
+
import { BaseClickUpService, ErrorCode, ClickUpServiceError } from './base.js';
|
|
12
|
+
export class FolderService extends BaseClickUpService {
|
|
13
|
+
/**
|
|
14
|
+
* Creates an instance of FolderService
|
|
15
|
+
* @param apiKey - ClickUp API key
|
|
16
|
+
* @param teamId - ClickUp team ID
|
|
17
|
+
* @param baseUrl - Optional custom API URL
|
|
18
|
+
* @param workspaceService - Optional workspace service for lookups
|
|
19
|
+
*/
|
|
20
|
+
constructor(apiKey, teamId, baseUrl, workspaceService) {
|
|
21
|
+
super(apiKey, teamId, baseUrl);
|
|
22
|
+
this.workspaceService = null;
|
|
23
|
+
this.workspaceService = workspaceService || null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Helper method to handle errors consistently
|
|
27
|
+
* @param error The error that occurred
|
|
28
|
+
* @param message Optional custom error message
|
|
29
|
+
* @returns A ClickUpServiceError
|
|
30
|
+
*/
|
|
31
|
+
handleError(error, message) {
|
|
32
|
+
if (error instanceof ClickUpServiceError) {
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
return new ClickUpServiceError(message || `Folder service error: ${error.message}`, ErrorCode.UNKNOWN, error);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a new folder in a space
|
|
39
|
+
* @param spaceId The ID of the space to create the folder in
|
|
40
|
+
* @param folderData The data for the new folder
|
|
41
|
+
* @returns The created folder
|
|
42
|
+
*/
|
|
43
|
+
async createFolder(spaceId, folderData) {
|
|
44
|
+
try {
|
|
45
|
+
this.logOperation('createFolder', { spaceId, ...folderData });
|
|
46
|
+
const response = await this.client.post(`/space/${spaceId}/folder`, folderData);
|
|
47
|
+
return response.data;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw this.handleError(error, `Failed to create folder in space ${spaceId}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get a folder by its ID
|
|
55
|
+
* @param folderId The ID of the folder to retrieve
|
|
56
|
+
* @returns The folder details
|
|
57
|
+
*/
|
|
58
|
+
async getFolder(folderId) {
|
|
59
|
+
try {
|
|
60
|
+
this.logOperation('getFolder', { folderId });
|
|
61
|
+
const response = await this.client.get(`/folder/${folderId}`);
|
|
62
|
+
return response.data;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw this.handleError(error, `Failed to get folder ${folderId}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Update an existing folder
|
|
70
|
+
* @param folderId The ID of the folder to update
|
|
71
|
+
* @param updateData The data to update on the folder
|
|
72
|
+
* @returns The updated folder
|
|
73
|
+
*/
|
|
74
|
+
async updateFolder(folderId, updateData) {
|
|
75
|
+
try {
|
|
76
|
+
this.logOperation('updateFolder', { folderId, ...updateData });
|
|
77
|
+
const response = await this.client.put(`/folder/${folderId}`, updateData);
|
|
78
|
+
return response.data;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw this.handleError(error, `Failed to update folder ${folderId}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Delete a folder
|
|
86
|
+
* @param folderId The ID of the folder to delete
|
|
87
|
+
* @returns Success indicator
|
|
88
|
+
*/
|
|
89
|
+
async deleteFolder(folderId) {
|
|
90
|
+
try {
|
|
91
|
+
this.logOperation('deleteFolder', { folderId });
|
|
92
|
+
await this.client.delete(`/folder/${folderId}`);
|
|
93
|
+
return {
|
|
94
|
+
success: true
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw this.handleError(error, `Failed to delete folder ${folderId}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get all folders in a space
|
|
103
|
+
* @param spaceId The ID of the space to get folders from
|
|
104
|
+
* @returns Array of folders in the space
|
|
105
|
+
*/
|
|
106
|
+
async getFoldersInSpace(spaceId) {
|
|
107
|
+
this.logOperation('getFoldersInSpace', { spaceId });
|
|
108
|
+
try {
|
|
109
|
+
const response = await this.client.get(`/space/${spaceId}/folder`);
|
|
110
|
+
return response.data.folders;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw this.handleError(error, `Failed to get folders in space ${spaceId}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Find a folder by its name in a space
|
|
118
|
+
* @param spaceId The ID of the space to search in
|
|
119
|
+
* @param folderName The name of the folder to find
|
|
120
|
+
* @returns The folder if found, otherwise null
|
|
121
|
+
*/
|
|
122
|
+
async findFolderByName(spaceId, folderName) {
|
|
123
|
+
this.logOperation('findFolderByName', { spaceId, folderName });
|
|
124
|
+
try {
|
|
125
|
+
const folders = await this.getFoldersInSpace(spaceId);
|
|
126
|
+
const matchingFolder = folders.find(folder => folder.name.toLowerCase() === folderName.toLowerCase());
|
|
127
|
+
return matchingFolder || null;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw this.handleError(error, `Failed to find folder by name in space ${spaceId}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickUp Service Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This file re-exports all service modules for the ClickUp API integration.
|
|
5
|
+
* It also provides a convenient factory method to create instances of all services.
|
|
6
|
+
*/
|
|
7
|
+
// Export base service components
|
|
8
|
+
export { BaseClickUpService, ClickUpServiceError, ErrorCode } from './base.js';
|
|
9
|
+
// Export type definitions
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
// Export service modules
|
|
12
|
+
export { WorkspaceService } from './workspace.js';
|
|
13
|
+
export { TaskService } from './task.js';
|
|
14
|
+
export { ListService } from './list.js';
|
|
15
|
+
export { FolderService } from './folder.js';
|
|
16
|
+
export { InitializationService } from './initialization.js';
|
|
17
|
+
// Import service classes for the factory function
|
|
18
|
+
import { WorkspaceService } from './workspace.js';
|
|
19
|
+
import { TaskService } from './task.js';
|
|
20
|
+
import { ListService } from './list.js';
|
|
21
|
+
import { FolderService } from './folder.js';
|
|
22
|
+
import { InitializationService } from './initialization.js';
|
|
23
|
+
/**
|
|
24
|
+
* Factory function to create instances of all ClickUp services
|
|
25
|
+
* @param config Configuration for the services
|
|
26
|
+
* @returns Object containing all service instances
|
|
27
|
+
*/
|
|
28
|
+
export function createClickUpServices(config) {
|
|
29
|
+
const { apiKey, teamId, baseUrl } = config;
|
|
30
|
+
// Create the workspace service
|
|
31
|
+
const workspaceService = new WorkspaceService(apiKey, teamId, baseUrl);
|
|
32
|
+
return {
|
|
33
|
+
workspace: workspaceService,
|
|
34
|
+
task: new TaskService(apiKey, teamId, baseUrl, workspaceService),
|
|
35
|
+
list: new ListService(apiKey, teamId, baseUrl, workspaceService),
|
|
36
|
+
folder: new FolderService(apiKey, teamId, baseUrl, workspaceService),
|
|
37
|
+
initialization: new InitializationService({
|
|
38
|
+
apiKey,
|
|
39
|
+
teamId,
|
|
40
|
+
baseUrl
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
}
|