@taazkareem/clickup-mcp-server 0.4.60 → 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,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
- }