@taazkareem/clickup-mcp-server 0.4.58 → 0.4.62

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