@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
|
@@ -1,765 +0,0 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
|
-
/**
|
|
3
|
-
* Service class for interacting with the ClickUp API.
|
|
4
|
-
* Handles all API requests and data transformations.
|
|
5
|
-
*/
|
|
6
|
-
export class ClickUpService {
|
|
7
|
-
client;
|
|
8
|
-
static instance;
|
|
9
|
-
clickupTeamId;
|
|
10
|
-
rateLimitRemaining = 100; // Default to lowest tier limit
|
|
11
|
-
rateLimitReset = 0;
|
|
12
|
-
constructor(apiKey, clickupTeamId) {
|
|
13
|
-
this.client = axios.create({
|
|
14
|
-
baseURL: 'https://api.clickup.com/api/v2',
|
|
15
|
-
headers: {
|
|
16
|
-
'Authorization': apiKey,
|
|
17
|
-
'Content-Type': 'application/json'
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
// Add response interceptor for rate limit handling
|
|
21
|
-
this.client.interceptors.response.use((response) => {
|
|
22
|
-
// Update rate limit info from headers
|
|
23
|
-
this.rateLimitRemaining = parseInt(response.headers['x-ratelimit-remaining'] || '100');
|
|
24
|
-
this.rateLimitReset = parseInt(response.headers['x-ratelimit-reset'] || '0');
|
|
25
|
-
return response;
|
|
26
|
-
}, async (error) => {
|
|
27
|
-
if (error.response?.status === 429) {
|
|
28
|
-
const resetTime = parseInt(error.response.headers['x-ratelimit-reset'] || '0');
|
|
29
|
-
const waitTime = Math.max(0, resetTime - Math.floor(Date.now() / 1000));
|
|
30
|
-
console.warn(`Rate limit exceeded. Waiting ${waitTime} seconds before retrying...`);
|
|
31
|
-
// Wait until rate limit resets
|
|
32
|
-
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
|
|
33
|
-
// Retry the request
|
|
34
|
-
return this.client.request(error.config);
|
|
35
|
-
}
|
|
36
|
-
throw error;
|
|
37
|
-
});
|
|
38
|
-
this.clickupTeamId = clickupTeamId;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Checks if we're close to hitting rate limits and waits if necessary.
|
|
42
|
-
* @private
|
|
43
|
-
*/
|
|
44
|
-
async checkRateLimit() {
|
|
45
|
-
if (this.rateLimitRemaining <= 5) { // Buffer of 5 requests
|
|
46
|
-
const now = Math.floor(Date.now() / 1000);
|
|
47
|
-
const waitTime = Math.max(0, this.rateLimitReset - now);
|
|
48
|
-
if (waitTime > 0) {
|
|
49
|
-
console.warn(`Approaching rate limit. Waiting ${waitTime} seconds...`);
|
|
50
|
-
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Makes an API request with rate limit handling.
|
|
56
|
-
* @private
|
|
57
|
-
* @param requestFn - Function that makes the actual API request
|
|
58
|
-
* @returns The API response
|
|
59
|
-
*/
|
|
60
|
-
async makeRequest(requestFn) {
|
|
61
|
-
await this.checkRateLimit();
|
|
62
|
-
return await requestFn();
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Initializes the ClickUpService singleton instance.
|
|
66
|
-
* @param apiKey - The ClickUp API key for authentication
|
|
67
|
-
* @param clickupTeamId - The team/workspace ID to operate on
|
|
68
|
-
* @returns The singleton instance of ClickUpService
|
|
69
|
-
* @throws Error if initialization fails
|
|
70
|
-
*/
|
|
71
|
-
static initialize(apiKey, clickupTeamId) {
|
|
72
|
-
if (!ClickUpService.instance) {
|
|
73
|
-
ClickUpService.instance = new ClickUpService(apiKey, clickupTeamId);
|
|
74
|
-
}
|
|
75
|
-
return ClickUpService.instance;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Gets the singleton instance of ClickUpService.
|
|
79
|
-
* @returns The singleton instance of ClickUpService
|
|
80
|
-
* @throws Error if service hasn't been initialized
|
|
81
|
-
*/
|
|
82
|
-
static getInstance() {
|
|
83
|
-
if (!ClickUpService.instance) {
|
|
84
|
-
throw new Error('ClickUpService not initialized. Call initialize() first.');
|
|
85
|
-
}
|
|
86
|
-
return ClickUpService.instance;
|
|
87
|
-
}
|
|
88
|
-
// Tasks
|
|
89
|
-
/**
|
|
90
|
-
* Retrieves tasks from a specific list with optional filtering.
|
|
91
|
-
* Handles rate limiting automatically.
|
|
92
|
-
* @param listId - The ID of the list to fetch tasks from
|
|
93
|
-
* @param filters - Optional filters to apply to the task query
|
|
94
|
-
* @param filters.archived - Include archived tasks
|
|
95
|
-
* @param filters.page - Page number for pagination
|
|
96
|
-
* @param filters.order_by - Field to order tasks by
|
|
97
|
-
* @param filters.reverse - Reverse the order of tasks
|
|
98
|
-
* @param filters.subtasks - Include subtasks
|
|
99
|
-
* @param filters.statuses - Filter by specific statuses
|
|
100
|
-
* @param filters.include_closed - Include closed tasks
|
|
101
|
-
* @param filters.assignees - Filter by assignee IDs
|
|
102
|
-
* @param filters.due_date_gt - Tasks due after this timestamp
|
|
103
|
-
* @param filters.due_date_lt - Tasks due before this timestamp
|
|
104
|
-
* @param filters.date_created_gt - Tasks created after this timestamp
|
|
105
|
-
* @param filters.date_created_lt - Tasks created before this timestamp
|
|
106
|
-
* @param filters.date_updated_gt - Tasks updated after this timestamp
|
|
107
|
-
* @param filters.date_updated_lt - Tasks updated before this timestamp
|
|
108
|
-
* @param filters.custom_fields - Filter by custom field values
|
|
109
|
-
* @returns Object containing tasks array and available statuses
|
|
110
|
-
* @throws Error if the API request fails
|
|
111
|
-
*/
|
|
112
|
-
async getTasks(listId, filters) {
|
|
113
|
-
return this.makeRequest(async () => {
|
|
114
|
-
const params = new URLSearchParams();
|
|
115
|
-
if (filters) {
|
|
116
|
-
if (filters.archived !== undefined)
|
|
117
|
-
params.append('archived', filters.archived.toString());
|
|
118
|
-
if (filters.page !== undefined)
|
|
119
|
-
params.append('page', filters.page.toString());
|
|
120
|
-
if (filters.order_by)
|
|
121
|
-
params.append('order_by', filters.order_by);
|
|
122
|
-
if (filters.reverse !== undefined)
|
|
123
|
-
params.append('reverse', filters.reverse.toString());
|
|
124
|
-
if (filters.subtasks !== undefined)
|
|
125
|
-
params.append('subtasks', filters.subtasks.toString());
|
|
126
|
-
if (filters.statuses)
|
|
127
|
-
params.append('statuses[]', filters.statuses.join(','));
|
|
128
|
-
if (filters.include_closed !== undefined)
|
|
129
|
-
params.append('include_closed', filters.include_closed.toString());
|
|
130
|
-
if (filters.assignees)
|
|
131
|
-
params.append('assignees[]', filters.assignees.join(','));
|
|
132
|
-
if (filters.due_date_gt)
|
|
133
|
-
params.append('due_date_gt', filters.due_date_gt.toString());
|
|
134
|
-
if (filters.due_date_lt)
|
|
135
|
-
params.append('due_date_lt', filters.due_date_lt.toString());
|
|
136
|
-
if (filters.date_created_gt)
|
|
137
|
-
params.append('date_created_gt', filters.date_created_gt.toString());
|
|
138
|
-
if (filters.date_created_lt)
|
|
139
|
-
params.append('date_created_lt', filters.date_created_lt.toString());
|
|
140
|
-
if (filters.date_updated_gt)
|
|
141
|
-
params.append('date_updated_gt', filters.date_updated_gt.toString());
|
|
142
|
-
if (filters.date_updated_lt)
|
|
143
|
-
params.append('date_updated_lt', filters.date_updated_lt.toString());
|
|
144
|
-
if (filters.custom_fields) {
|
|
145
|
-
Object.entries(filters.custom_fields).forEach(([key, value]) => {
|
|
146
|
-
params.append(`custom_fields[${key}]`, JSON.stringify(value));
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const queryString = params.toString();
|
|
151
|
-
const url = `/list/${listId}/task${queryString ? `?${queryString}` : ''}`;
|
|
152
|
-
const response = await this.client.get(url);
|
|
153
|
-
const tasks = response.data.tasks;
|
|
154
|
-
const statuses = [...new Set(tasks
|
|
155
|
-
.filter((task) => task.status !== undefined)
|
|
156
|
-
.map((task) => task.status.status))];
|
|
157
|
-
return { tasks, statuses };
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Retrieves detailed information about a specific task.
|
|
162
|
-
* Handles rate limiting automatically.
|
|
163
|
-
*/
|
|
164
|
-
async getTask(taskId) {
|
|
165
|
-
return this.makeRequest(async () => {
|
|
166
|
-
const response = await this.client.get(`/task/${taskId}`);
|
|
167
|
-
return response.data;
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Creates a new task in a specified list.
|
|
172
|
-
* Handles rate limiting automatically.
|
|
173
|
-
*/
|
|
174
|
-
async createTask(listId, data) {
|
|
175
|
-
return this.makeRequest(async () => {
|
|
176
|
-
const taskData = { ...data };
|
|
177
|
-
// If markdown_description is provided, it takes precedence
|
|
178
|
-
if (taskData.markdown_description) {
|
|
179
|
-
// Ensure we don't send both to avoid confusion
|
|
180
|
-
delete taskData.description;
|
|
181
|
-
}
|
|
182
|
-
else if (taskData.description) {
|
|
183
|
-
// Only use description as-is, don't auto-convert to markdown
|
|
184
|
-
taskData.description = taskData.description.trim();
|
|
185
|
-
}
|
|
186
|
-
const response = await this.client.post(`/list/${listId}/task`, taskData);
|
|
187
|
-
return response.data;
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Creates multiple tasks in a list sequentially to avoid rate limits.
|
|
192
|
-
* Automatically handles rate limiting and retries.
|
|
193
|
-
*/
|
|
194
|
-
async createBulkTasks(listId, data) {
|
|
195
|
-
const createdTasks = [];
|
|
196
|
-
for (const taskData of data.tasks) {
|
|
197
|
-
await this.makeRequest(async () => {
|
|
198
|
-
const processedTask = { ...taskData };
|
|
199
|
-
// If markdown_description is provided, it takes precedence
|
|
200
|
-
if (processedTask.markdown_description) {
|
|
201
|
-
// Ensure we don't send both to avoid confusion
|
|
202
|
-
delete processedTask.description;
|
|
203
|
-
}
|
|
204
|
-
else if (processedTask.description) {
|
|
205
|
-
// Only use description as-is, don't auto-convert to markdown
|
|
206
|
-
processedTask.description = processedTask.description.trim();
|
|
207
|
-
}
|
|
208
|
-
const response = await this.client.post(`/list/${listId}/task`, processedTask);
|
|
209
|
-
createdTasks.push(response.data);
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
return createdTasks;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Updates an existing task with new data.
|
|
216
|
-
* Handles rate limiting automatically.
|
|
217
|
-
*/
|
|
218
|
-
async updateTask(taskId, data) {
|
|
219
|
-
return this.makeRequest(async () => {
|
|
220
|
-
const updateData = { ...data };
|
|
221
|
-
// If markdown_description is provided, it takes precedence
|
|
222
|
-
if (updateData.markdown_description) {
|
|
223
|
-
// Ensure we don't send both to avoid confusion
|
|
224
|
-
delete updateData.description;
|
|
225
|
-
}
|
|
226
|
-
else if (updateData.description) {
|
|
227
|
-
// Only use description as-is, don't auto-convert to markdown
|
|
228
|
-
updateData.description = updateData.description.trim();
|
|
229
|
-
}
|
|
230
|
-
// Handle null priority explicitly
|
|
231
|
-
if (updateData.priority === null) {
|
|
232
|
-
updateData.priority = null;
|
|
233
|
-
}
|
|
234
|
-
const response = await this.client.put(`/task/${taskId}`, updateData);
|
|
235
|
-
return response.data;
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Deletes a task from the workspace.
|
|
240
|
-
* Can delete by taskId directly or find by taskName first.
|
|
241
|
-
* Handles rate limiting automatically.
|
|
242
|
-
* @param taskId - ID of the task to delete (optional if taskName provided)
|
|
243
|
-
* @param taskName - Name of the task to delete (optional if taskId provided)
|
|
244
|
-
* @param listName - Optional list name to narrow down task search
|
|
245
|
-
* @throws Error if neither taskId nor taskName is provided, or if task not found
|
|
246
|
-
*/
|
|
247
|
-
async deleteTask(taskId, taskName, listName) {
|
|
248
|
-
return this.makeRequest(async () => {
|
|
249
|
-
let finalTaskId = taskId;
|
|
250
|
-
// If no taskId but taskName provided, find the task first
|
|
251
|
-
if (!taskId && taskName) {
|
|
252
|
-
const taskInfo = await this.findTaskByName(taskName, undefined, listName);
|
|
253
|
-
if (!taskInfo) {
|
|
254
|
-
throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ''}`);
|
|
255
|
-
}
|
|
256
|
-
finalTaskId = taskInfo.id;
|
|
257
|
-
}
|
|
258
|
-
if (!finalTaskId) {
|
|
259
|
-
throw new Error('Either taskId or taskName must be provided');
|
|
260
|
-
}
|
|
261
|
-
await this.client.delete(`/task/${finalTaskId}`);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
// Lists
|
|
265
|
-
/**
|
|
266
|
-
* Gets all lists in a space.
|
|
267
|
-
* @param spaceId - ID of the space to get lists from
|
|
268
|
-
* @returns Promise resolving to array of ClickUpList objects
|
|
269
|
-
* @throws Error if the API request fails
|
|
270
|
-
*/
|
|
271
|
-
async getLists(spaceId) {
|
|
272
|
-
return this.makeRequest(async () => {
|
|
273
|
-
const response = await this.client.get(`/space/${spaceId}/list`);
|
|
274
|
-
return response.data.lists;
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Gets all lists in the workspace.
|
|
279
|
-
* @param clickupTeamId - ID of the team/workspace
|
|
280
|
-
* @returns Promise resolving to array of ClickUpList objects
|
|
281
|
-
* @throws Error if the API request fails
|
|
282
|
-
*/
|
|
283
|
-
async getAllLists(clickupTeamId) {
|
|
284
|
-
return this.makeRequest(async () => {
|
|
285
|
-
const response = await this.client.get(`/team/${clickupTeamId}/list`);
|
|
286
|
-
return response.data.lists;
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Gets a specific list by ID.
|
|
291
|
-
* @param listId - ID of the list to retrieve
|
|
292
|
-
* @returns Promise resolving to ClickUpList object
|
|
293
|
-
* @throws Error if the API request fails or list not found
|
|
294
|
-
*/
|
|
295
|
-
async getList(listId) {
|
|
296
|
-
return this.makeRequest(async () => {
|
|
297
|
-
const response = await this.client.get(`/list/${listId}`);
|
|
298
|
-
return response.data;
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
// Spaces
|
|
302
|
-
async getSpaces(clickupTeamId) {
|
|
303
|
-
return this.makeRequest(async () => {
|
|
304
|
-
const response = await this.client.get(`/team/${clickupTeamId}/space`);
|
|
305
|
-
return response.data.spaces;
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
async getSpace(spaceId) {
|
|
309
|
-
return this.makeRequest(async () => {
|
|
310
|
-
const response = await this.client.get(`/space/${spaceId}`);
|
|
311
|
-
return response.data;
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
async findSpaceByName(clickupTeamId, spaceName) {
|
|
315
|
-
const spaces = await this.getSpaces(clickupTeamId);
|
|
316
|
-
return spaces.find(space => space.name.toLowerCase() === spaceName.toLowerCase()) || null;
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Creates a new list in a space.
|
|
320
|
-
* @param spaceId - ID of the space to create the list in
|
|
321
|
-
* @param data - List creation data (name, content, due date, etc.)
|
|
322
|
-
* @returns Promise resolving to the created ClickUpList
|
|
323
|
-
* @throws Error if the API request fails
|
|
324
|
-
*/
|
|
325
|
-
async createList(spaceId, data) {
|
|
326
|
-
return this.makeRequest(async () => {
|
|
327
|
-
const response = await this.client.post(`/space/${spaceId}/list`, data);
|
|
328
|
-
return response.data;
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
// Folders
|
|
332
|
-
async getFolders(spaceId) {
|
|
333
|
-
return this.makeRequest(async () => {
|
|
334
|
-
const response = await this.client.get(`/space/${spaceId}/folder`);
|
|
335
|
-
return response.data.folders;
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
async getFolder(folderId) {
|
|
339
|
-
return this.makeRequest(async () => {
|
|
340
|
-
const response = await this.client.get(`/folder/${folderId}`);
|
|
341
|
-
return response.data;
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Updates an existing folder with new data.
|
|
346
|
-
* @param folderId - ID of the folder to update
|
|
347
|
-
* @param data - Data to update the folder with (name, override_statuses)
|
|
348
|
-
* @returns Promise resolving to the updated ClickUpFolder
|
|
349
|
-
* @throws Error if the API request fails
|
|
350
|
-
*/
|
|
351
|
-
async updateFolder(folderId, data) {
|
|
352
|
-
return this.makeRequest(async () => {
|
|
353
|
-
const response = await this.client.put(`/folder/${folderId}`, data);
|
|
354
|
-
return response.data;
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
async deleteFolder(folderId) {
|
|
358
|
-
return this.makeRequest(async () => {
|
|
359
|
-
await this.client.delete(`/folder/${folderId}`);
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Creates a new list in a folder.
|
|
364
|
-
* @param folderId - ID of the folder to create the list in
|
|
365
|
-
* @param data - List creation data (name, content, etc.)
|
|
366
|
-
* @returns Promise resolving to the created ClickUpList
|
|
367
|
-
* @throws Error if the API request fails
|
|
368
|
-
*/
|
|
369
|
-
async createListInFolder(folderId, data) {
|
|
370
|
-
return this.makeRequest(async () => {
|
|
371
|
-
const response = await this.client.post(`/folder/${folderId}/list`, data);
|
|
372
|
-
return response.data;
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
async findFolderByName(spaceId, folderName) {
|
|
376
|
-
const folders = await this.getFolders(spaceId);
|
|
377
|
-
return folders.find(folder => folder.name.toLowerCase() === folderName.toLowerCase()) || null;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Creates a new folder in a space.
|
|
381
|
-
* @param spaceId - ID of the space to create the folder in
|
|
382
|
-
* @param data - Folder creation data (name, override_statuses)
|
|
383
|
-
* @returns Promise resolving to the created ClickUpFolder
|
|
384
|
-
* @throws Error if the API request fails
|
|
385
|
-
*/
|
|
386
|
-
async createFolder(spaceId, data) {
|
|
387
|
-
return this.makeRequest(async () => {
|
|
388
|
-
const response = await this.client.post(`/space/${spaceId}/folder`, data);
|
|
389
|
-
return response.data;
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
// Additional helper methods
|
|
393
|
-
/**
|
|
394
|
-
* Moves a task to a different list.
|
|
395
|
-
* Since direct task moving is not supported by the ClickUp API,
|
|
396
|
-
* this creates a new task in the target list and deletes the original.
|
|
397
|
-
*
|
|
398
|
-
* @param taskId - ID of the task to move
|
|
399
|
-
* @param listId - ID of the destination list
|
|
400
|
-
* @returns Promise resolving to the new task in its new location
|
|
401
|
-
* @throws Error if the API request fails
|
|
402
|
-
*/
|
|
403
|
-
async moveTask(taskId, listId) {
|
|
404
|
-
return this.makeRequest(async () => {
|
|
405
|
-
// Get the current task to copy all its data
|
|
406
|
-
const currentTask = await this.getTask(taskId);
|
|
407
|
-
// Get available statuses in the target list
|
|
408
|
-
const { statuses: targetStatuses } = await this.getTasks(listId);
|
|
409
|
-
// Check if current status exists in target list
|
|
410
|
-
const currentStatus = currentTask.status?.status;
|
|
411
|
-
const statusExists = currentStatus && targetStatuses.includes(currentStatus);
|
|
412
|
-
// Prepare the task data for the new location
|
|
413
|
-
const moveData = {
|
|
414
|
-
name: currentTask.name,
|
|
415
|
-
description: currentTask.description,
|
|
416
|
-
markdown_description: currentTask.description, // In case it contains markdown
|
|
417
|
-
status: statusExists ? currentStatus : undefined, // Only set status if it exists in target list
|
|
418
|
-
priority: currentTask.priority ? parseInt(currentTask.priority.id) : undefined,
|
|
419
|
-
due_date: currentTask.due_date ? parseInt(currentTask.due_date) : undefined,
|
|
420
|
-
start_date: currentTask.start_date ? parseInt(currentTask.start_date) : undefined,
|
|
421
|
-
assignees: currentTask.assignees?.map(a => a.id)
|
|
422
|
-
};
|
|
423
|
-
// Create a new task in the target list with the same data
|
|
424
|
-
const newTask = await this.createTask(listId, moveData);
|
|
425
|
-
// Delete the original task
|
|
426
|
-
await this.deleteTask(taskId);
|
|
427
|
-
// Return the new task
|
|
428
|
-
return newTask;
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Duplicates a task to another list.
|
|
433
|
-
* Creates a new task with the same data in the target list.
|
|
434
|
-
*
|
|
435
|
-
* @param taskId - ID of the task to duplicate
|
|
436
|
-
* @param listId - ID of the destination list
|
|
437
|
-
* @returns Promise resolving to the new duplicate task
|
|
438
|
-
* @throws Error if the API request fails
|
|
439
|
-
*/
|
|
440
|
-
async duplicateTask(taskId, listId) {
|
|
441
|
-
return this.makeRequest(async () => {
|
|
442
|
-
// Get the current task to copy all its data
|
|
443
|
-
const currentTask = await this.getTask(taskId);
|
|
444
|
-
// Get available statuses in the target list
|
|
445
|
-
const { statuses: targetStatuses } = await this.getTasks(listId);
|
|
446
|
-
// Check if current status exists in target list
|
|
447
|
-
const currentStatus = currentTask.status?.status;
|
|
448
|
-
const statusExists = currentStatus && targetStatuses.includes(currentStatus);
|
|
449
|
-
// Prepare the task data for duplication
|
|
450
|
-
const taskData = {
|
|
451
|
-
name: currentTask.name,
|
|
452
|
-
description: currentTask.description,
|
|
453
|
-
markdown_description: currentTask.description, // In case it contains markdown
|
|
454
|
-
status: statusExists ? currentStatus : undefined, // Only set status if it exists in target list
|
|
455
|
-
priority: currentTask.priority ? parseInt(currentTask.priority.id) : undefined,
|
|
456
|
-
due_date: currentTask.due_date ? parseInt(currentTask.due_date) : undefined,
|
|
457
|
-
start_date: currentTask.start_date ? parseInt(currentTask.start_date) : undefined,
|
|
458
|
-
assignees: currentTask.assignees?.map(a => a.id)
|
|
459
|
-
};
|
|
460
|
-
// Create a new task in the target list with the same data
|
|
461
|
-
const newTask = await this.createTask(listId, taskData);
|
|
462
|
-
// Return the new task
|
|
463
|
-
return newTask;
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Deletes a list.
|
|
468
|
-
* @param listId - ID of the list to delete
|
|
469
|
-
* @returns Promise resolving when deletion is complete
|
|
470
|
-
* @throws Error if the API request fails
|
|
471
|
-
*/
|
|
472
|
-
async deleteList(listId) {
|
|
473
|
-
return this.makeRequest(async () => {
|
|
474
|
-
await this.client.delete(`/list/${listId}`);
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Updates an existing list.
|
|
479
|
-
* @param listId - ID of the list to update
|
|
480
|
-
* @param data - Partial list data to update
|
|
481
|
-
* @returns Promise resolving to the updated ClickUpList
|
|
482
|
-
* @throws Error if the API request fails
|
|
483
|
-
*/
|
|
484
|
-
async updateList(listId, data) {
|
|
485
|
-
return this.makeRequest(async () => {
|
|
486
|
-
const response = await this.client.put(`/list/${listId}`, data);
|
|
487
|
-
return response.data;
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Finds a list by name in a specific space.
|
|
492
|
-
* Performs case-insensitive matching.
|
|
493
|
-
* @param spaceId - ID of the space to search in
|
|
494
|
-
* @param listName - Name of the list to find
|
|
495
|
-
* @returns Promise resolving to ClickUpList object or null if not found
|
|
496
|
-
*/
|
|
497
|
-
async findListByName(spaceId, listName) {
|
|
498
|
-
const lists = await this.getLists(spaceId);
|
|
499
|
-
return lists.find(list => list.name.toLowerCase() === listName.toLowerCase()) || null;
|
|
500
|
-
}
|
|
501
|
-
async findListByNameGlobally(listName) {
|
|
502
|
-
// First try the direct lists
|
|
503
|
-
const lists = await this.getAllLists(this.clickupTeamId);
|
|
504
|
-
const directList = lists.find(list => list.name.toLowerCase() === listName.toLowerCase());
|
|
505
|
-
if (directList)
|
|
506
|
-
return directList;
|
|
507
|
-
// If not found, search through folders
|
|
508
|
-
const hierarchy = await this.getWorkspaceHierarchy();
|
|
509
|
-
return this.findListByNameInHierarchy(hierarchy, listName);
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Gets the complete workspace hierarchy as a tree structure.
|
|
513
|
-
* The tree consists of:
|
|
514
|
-
* - Root (Workspace)
|
|
515
|
-
* - Spaces
|
|
516
|
-
* - Lists (directly in space)
|
|
517
|
-
* - Folders
|
|
518
|
-
* - Lists (in folders)
|
|
519
|
-
*
|
|
520
|
-
* Each node in the tree contains:
|
|
521
|
-
* - id: Unique identifier
|
|
522
|
-
* - name: Display name
|
|
523
|
-
* - type: 'space' | 'folder' | 'list'
|
|
524
|
-
* - parent: Reference to parent node (except root)
|
|
525
|
-
* - children: Array of child nodes
|
|
526
|
-
* - data: Original ClickUp object data
|
|
527
|
-
*
|
|
528
|
-
* @returns Promise resolving to the complete workspace tree
|
|
529
|
-
* @throws Error if API requests fail
|
|
530
|
-
*/
|
|
531
|
-
async getWorkspaceHierarchy() {
|
|
532
|
-
const spaces = await this.getSpaces(this.clickupTeamId);
|
|
533
|
-
const root = {
|
|
534
|
-
id: this.clickupTeamId,
|
|
535
|
-
name: 'Workspace',
|
|
536
|
-
type: 'workspace',
|
|
537
|
-
children: []
|
|
538
|
-
};
|
|
539
|
-
// Build the tree
|
|
540
|
-
for (const space of spaces) {
|
|
541
|
-
const spaceNode = {
|
|
542
|
-
id: space.id,
|
|
543
|
-
name: space.name,
|
|
544
|
-
type: 'space',
|
|
545
|
-
children: [],
|
|
546
|
-
data: space
|
|
547
|
-
};
|
|
548
|
-
root.children.push(spaceNode);
|
|
549
|
-
// Add lists directly in the space
|
|
550
|
-
const spaceLists = await this.getLists(space.id);
|
|
551
|
-
for (const list of spaceLists) {
|
|
552
|
-
const listNode = {
|
|
553
|
-
id: list.id,
|
|
554
|
-
name: list.name,
|
|
555
|
-
type: 'list',
|
|
556
|
-
parent: spaceNode,
|
|
557
|
-
children: [],
|
|
558
|
-
data: list
|
|
559
|
-
};
|
|
560
|
-
spaceNode.children.push(listNode);
|
|
561
|
-
}
|
|
562
|
-
// Add folders and their lists
|
|
563
|
-
const folders = await this.getFolders(space.id);
|
|
564
|
-
for (const folder of folders) {
|
|
565
|
-
const folderNode = {
|
|
566
|
-
id: folder.id,
|
|
567
|
-
name: folder.name,
|
|
568
|
-
type: 'folder',
|
|
569
|
-
parent: spaceNode,
|
|
570
|
-
children: [],
|
|
571
|
-
data: folder
|
|
572
|
-
};
|
|
573
|
-
spaceNode.children.push(folderNode);
|
|
574
|
-
// Add lists in the folder
|
|
575
|
-
const folderLists = folder.lists || [];
|
|
576
|
-
for (const list of folderLists) {
|
|
577
|
-
const listNode = {
|
|
578
|
-
id: list.id,
|
|
579
|
-
name: list.name,
|
|
580
|
-
type: 'list',
|
|
581
|
-
parent: folderNode,
|
|
582
|
-
children: [],
|
|
583
|
-
data: list
|
|
584
|
-
};
|
|
585
|
-
folderNode.children.push(listNode);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
return { root };
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Helper method to find a node in the workspace tree by name and type.
|
|
593
|
-
* Performs a case-insensitive search through the tree structure.
|
|
594
|
-
*
|
|
595
|
-
* @private
|
|
596
|
-
* @param node - The root node to start searching from
|
|
597
|
-
* @param name - The name to search for (case-insensitive)
|
|
598
|
-
* @param type - The type of node to find ('space', 'folder', or 'list')
|
|
599
|
-
* @returns Object containing:
|
|
600
|
-
* - node: The found WorkspaceNode
|
|
601
|
-
* - path: Full path to the node (e.g., "Space > Folder > List")
|
|
602
|
-
* Or null if no matching node is found
|
|
603
|
-
*/
|
|
604
|
-
findNodeInTree(node, name, type) {
|
|
605
|
-
// Check current node if it's a WorkspaceNode
|
|
606
|
-
if ('type' in node && node.type === type && node.name.toLowerCase() === name.toLowerCase()) {
|
|
607
|
-
return {
|
|
608
|
-
node,
|
|
609
|
-
path: node.name
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
// Search children
|
|
613
|
-
for (const child of node.children) {
|
|
614
|
-
const result = this.findNodeInTree(child, name, type);
|
|
615
|
-
if (result) {
|
|
616
|
-
const path = node.type === 'workspace' ? result.path : `${node.name} > ${result.path}`;
|
|
617
|
-
return { node: result.node, path };
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return null;
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Finds a node by name and type in the workspace hierarchy.
|
|
624
|
-
* This is a high-level method that uses findNodeInTree internally.
|
|
625
|
-
*
|
|
626
|
-
* @param hierarchy - The workspace tree to search in
|
|
627
|
-
* @param name - Name of the space/folder/list to find (case-insensitive)
|
|
628
|
-
* @param type - Type of node to find ('space', 'folder', or 'list')
|
|
629
|
-
* @returns Object containing:
|
|
630
|
-
* - id: The ID of the found node
|
|
631
|
-
* - path: Full path to the node
|
|
632
|
-
* Or null if no matching node is found
|
|
633
|
-
*/
|
|
634
|
-
findIDByNameInHierarchy(hierarchy, name, type) {
|
|
635
|
-
const result = this.findNodeInTree(hierarchy.root, name, type);
|
|
636
|
-
if (!result)
|
|
637
|
-
return null;
|
|
638
|
-
return { id: result.node.id, path: result.path };
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Retrieves all tasks from the entire workspace using the tree structure.
|
|
642
|
-
* Traverses the workspace hierarchy tree and collects tasks from all lists.
|
|
643
|
-
* Uses recursive traversal to handle nested folders and lists efficiently.
|
|
644
|
-
*
|
|
645
|
-
* The process:
|
|
646
|
-
* 1. Gets the workspace hierarchy tree
|
|
647
|
-
* 2. Recursively processes each node:
|
|
648
|
-
* - If it's a list node, fetches and collects its tasks
|
|
649
|
-
* - If it has children, processes them recursively
|
|
650
|
-
* 3. Returns all collected tasks
|
|
651
|
-
*
|
|
652
|
-
* @returns Promise resolving to array of all tasks in the workspace
|
|
653
|
-
* @throws Error if API requests fail
|
|
654
|
-
*/
|
|
655
|
-
async getAllTasksInWorkspace() {
|
|
656
|
-
const hierarchy = await this.getWorkspaceHierarchy();
|
|
657
|
-
const allTasks = [];
|
|
658
|
-
// Helper function to process a node
|
|
659
|
-
const processNode = async (node) => {
|
|
660
|
-
if (node.type === 'list') {
|
|
661
|
-
const { tasks } = await this.getTasks(node.id);
|
|
662
|
-
allTasks.push(...tasks);
|
|
663
|
-
}
|
|
664
|
-
// Process children recursively
|
|
665
|
-
for (const child of node.children) {
|
|
666
|
-
await processNode(child);
|
|
667
|
-
}
|
|
668
|
-
};
|
|
669
|
-
// Process all spaces
|
|
670
|
-
for (const space of hierarchy.root.children) {
|
|
671
|
-
await processNode(space);
|
|
672
|
-
}
|
|
673
|
-
return allTasks;
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Finds a list by name in the workspace hierarchy.
|
|
677
|
-
* This is a specialized version of findNodeInTree for lists.
|
|
678
|
-
*
|
|
679
|
-
* @param hierarchy - The workspace tree to search in
|
|
680
|
-
* @param listName - Name of the list to find (case-insensitive)
|
|
681
|
-
* @returns The found ClickUpList object or null if not found
|
|
682
|
-
*/
|
|
683
|
-
findListByNameInHierarchy(hierarchy, listName) {
|
|
684
|
-
const result = this.findNodeInTree(hierarchy.root, listName, 'list');
|
|
685
|
-
if (!result)
|
|
686
|
-
return null;
|
|
687
|
-
return result.node.data;
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
|
-
* Helper method to find a space ID by name.
|
|
691
|
-
* Uses the tree structure for efficient lookup.
|
|
692
|
-
*
|
|
693
|
-
* @param spaceName - Name of the space to find (case-insensitive)
|
|
694
|
-
* @returns Promise resolving to the space ID or null if not found
|
|
695
|
-
*/
|
|
696
|
-
async findSpaceIDByName(spaceName) {
|
|
697
|
-
const hierarchy = await this.getWorkspaceHierarchy();
|
|
698
|
-
const result = this.findIDByNameInHierarchy(hierarchy, spaceName, 'space');
|
|
699
|
-
return result?.id || null;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Helper method to find a folder ID and its path by name.
|
|
703
|
-
* Uses the tree structure for efficient lookup.
|
|
704
|
-
*
|
|
705
|
-
* @param folderName - Name of the folder to find (case-insensitive)
|
|
706
|
-
* @returns Promise resolving to object containing:
|
|
707
|
-
* - id: The folder ID
|
|
708
|
-
* - spacePath: Full path including the parent space
|
|
709
|
-
* Or null if not found
|
|
710
|
-
*/
|
|
711
|
-
async findFolderIDByName(folderName) {
|
|
712
|
-
const hierarchy = await this.getWorkspaceHierarchy();
|
|
713
|
-
const result = this.findNodeInTree(hierarchy.root, folderName, 'folder');
|
|
714
|
-
return result ? { id: result.node.id, spacePath: result.path } : null;
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Helper method to find a list ID and its path by name.
|
|
718
|
-
* Uses the tree structure for efficient lookup.
|
|
719
|
-
*
|
|
720
|
-
* @param listName - Name of the list to find (case-insensitive)
|
|
721
|
-
* @returns Promise resolving to object containing:
|
|
722
|
-
* - id: The list ID
|
|
723
|
-
* - path: Full path including parent space and folder (if any)
|
|
724
|
-
* Or null if not found
|
|
725
|
-
*/
|
|
726
|
-
async findListIDByName(listName) {
|
|
727
|
-
const hierarchy = await this.getWorkspaceHierarchy();
|
|
728
|
-
const result = this.findNodeInTree(hierarchy.root, listName, 'list');
|
|
729
|
-
return result ? { id: result.node.id, path: result.path } : null;
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Helper method to find a task by name, optionally within a specific list.
|
|
733
|
-
* Uses case-insensitive matching and returns full path information.
|
|
734
|
-
*
|
|
735
|
-
* @param taskName - Name of the task to find (case-insensitive)
|
|
736
|
-
* @param listId - Optional: ID of the list to search in
|
|
737
|
-
* @param listName - Optional: Name of the list to search in (alternative to listId)
|
|
738
|
-
* @returns Promise resolving to object containing:
|
|
739
|
-
* - id: The task ID
|
|
740
|
-
* - path: Full path including space, folder (if any), list, and task name
|
|
741
|
-
* Or null if not found
|
|
742
|
-
*/
|
|
743
|
-
async findTaskByName(taskName, listId, listName) {
|
|
744
|
-
// If listName is provided, get the listId first
|
|
745
|
-
if (!listId && listName) {
|
|
746
|
-
const result = await this.findListIDByName(listName);
|
|
747
|
-
if (!result)
|
|
748
|
-
return null;
|
|
749
|
-
listId = result.id;
|
|
750
|
-
}
|
|
751
|
-
// Get tasks from specific list or all tasks
|
|
752
|
-
const tasks = listId
|
|
753
|
-
? (await this.getTasks(listId)).tasks
|
|
754
|
-
: await this.getAllTasksInWorkspace();
|
|
755
|
-
// Find matching task (case-insensitive)
|
|
756
|
-
const task = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
757
|
-
if (!task)
|
|
758
|
-
return null;
|
|
759
|
-
// Get the full path
|
|
760
|
-
const path = task.folder?.name
|
|
761
|
-
? `${task.space.name} > ${task.folder.name} > ${task.list.name} > ${task.name}`
|
|
762
|
-
: `${task.space.name} > ${task.list.name} > ${task.name}`;
|
|
763
|
-
return { id: task.id, path };
|
|
764
|
-
}
|
|
765
|
-
}
|