@taazkareem/clickup-mcp-server 0.4.72 → 0.4.74

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,452 +0,0 @@
1
- /**
2
- * ClickUp MCP Cache Module
3
- *
4
- * This module provides caching for workspace data including:
5
- * - Workspace hierarchy (spaces, folders, lists)
6
- * - Task data (names, IDs, and metadata)
7
- *
8
- * The cache is initialized on server startup and updated when
9
- * tasks, lists, folders, or spaces are modified.
10
- */
11
- import { createClickUpServices } from '../services/clickup/index.js';
12
- import config from '../config.js';
13
- import { log } from '../logger.js';
14
- // Initialize ClickUp services
15
- const services = createClickUpServices({
16
- apiKey: config.clickupApiKey,
17
- teamId: config.clickupTeamId
18
- });
19
- const { workspace: workspaceService, task: taskService, list: listService, folder: folderService } = services;
20
- /**
21
- * Singleton cache instance
22
- */
23
- let workspaceCache = null;
24
- /**
25
- * Normalize a name for case-insensitive lookups
26
- */
27
- function normalizeName(name) {
28
- return name.toLowerCase().trim();
29
- }
30
- /**
31
- * Generate a unique key for task name lookup
32
- */
33
- function getTaskNameKey(listId, taskName) {
34
- return `${listId}:${normalizeName(taskName)}`;
35
- }
36
- /**
37
- * Initialize an empty cache structure
38
- */
39
- function createEmptyCache() {
40
- return {
41
- hierarchy: {
42
- root: {
43
- id: config.clickupTeamId,
44
- name: 'Workspace',
45
- children: []
46
- }
47
- },
48
- tasksById: new Map(),
49
- tasksByName: new Map(),
50
- listIdsByName: new Map(),
51
- folderIdsByName: new Map(),
52
- spaceIdsByName: new Map(),
53
- lastUpdated: new Date(),
54
- initialized: false
55
- };
56
- }
57
- /**
58
- * Process workspace hierarchy to populate ID lookup maps
59
- */
60
- function processHierarchy(hierarchy) {
61
- if (!workspaceCache) {
62
- workspaceCache = createEmptyCache();
63
- }
64
- // Clear existing maps
65
- workspaceCache.listIdsByName.clear();
66
- workspaceCache.folderIdsByName.clear();
67
- workspaceCache.spaceIdsByName.clear();
68
- // Recursively process all nodes
69
- const processNode = (node) => {
70
- const normalizedName = normalizeName(node.name);
71
- // Store in appropriate map based on node type
72
- switch (node.type) {
73
- case 'space':
74
- workspaceCache.spaceIdsByName.set(normalizedName, node.id);
75
- break;
76
- case 'folder':
77
- workspaceCache.folderIdsByName.set(normalizedName, node.id);
78
- break;
79
- case 'list':
80
- workspaceCache.listIdsByName.set(normalizedName, node.id);
81
- break;
82
- }
83
- // Process children recursively
84
- if (node.children && node.children.length > 0) {
85
- node.children.forEach(child => processNode(child));
86
- }
87
- };
88
- // Process all space nodes (children of the root)
89
- hierarchy.root.children.forEach(node => processNode(node));
90
- }
91
- /**
92
- * Cache a single task
93
- */
94
- function cacheTask(task) {
95
- if (!workspaceCache) {
96
- workspaceCache = createEmptyCache();
97
- }
98
- // Create cached task object
99
- const cachedTask = {
100
- id: task.id,
101
- name: task.name,
102
- listId: task.list.id,
103
- status: task.status?.status,
104
- dueDate: task.due_date,
105
- priority: task.priority?.priority ? Number(task.priority.priority) : null,
106
- createdDate: task.date_created,
107
- updatedDate: task.date_updated
108
- };
109
- // Store in maps
110
- workspaceCache.tasksById.set(task.id, cachedTask);
111
- workspaceCache.tasksByName.set(getTaskNameKey(task.list.id, task.name), task.id);
112
- }
113
- /**
114
- * Remove a task from the cache
115
- */
116
- function removeCachedTask(taskId) {
117
- if (!workspaceCache || !workspaceCache.tasksById.has(taskId)) {
118
- return;
119
- }
120
- // Get task details before removing
121
- const task = workspaceCache.tasksById.get(taskId);
122
- if (task) {
123
- // Remove from name lookup
124
- const nameKey = getTaskNameKey(task.listId, task.name);
125
- workspaceCache.tasksByName.delete(nameKey);
126
- // Remove from ID lookup
127
- workspaceCache.tasksById.delete(taskId);
128
- }
129
- }
130
- /**
131
- * Update a cached task with new data
132
- */
133
- function updateCachedTask(taskId, updates) {
134
- if (!workspaceCache || !workspaceCache.tasksById.has(taskId)) {
135
- return;
136
- }
137
- const task = workspaceCache.tasksById.get(taskId);
138
- if (!task)
139
- return;
140
- // Handle name change - requires updating the tasksByName map
141
- if (updates.name && updates.name !== task.name) {
142
- // Remove old name lookup
143
- const oldNameKey = getTaskNameKey(task.listId, task.name);
144
- workspaceCache.tasksByName.delete(oldNameKey);
145
- // Add new name lookup
146
- const newNameKey = getTaskNameKey(task.listId, updates.name);
147
- workspaceCache.tasksByName.set(newNameKey, taskId);
148
- }
149
- // Handle list change - requires updating the tasksByName map
150
- if (updates.listId && updates.listId !== task.listId) {
151
- // Remove old list-based lookup
152
- const oldNameKey = getTaskNameKey(task.listId, task.name);
153
- workspaceCache.tasksByName.delete(oldNameKey);
154
- // Add new list-based lookup
155
- const newNameKey = getTaskNameKey(updates.listId, task.name);
156
- workspaceCache.tasksByName.set(newNameKey, taskId);
157
- }
158
- // Update the task object with all provided updates
159
- Object.assign(task, updates);
160
- // Update the task in the cache
161
- workspaceCache.tasksById.set(taskId, task);
162
- // Update last updated timestamp
163
- workspaceCache.lastUpdated = new Date();
164
- }
165
- /**
166
- * Cache all tasks for a list
167
- */
168
- async function cacheTasksForList(listId) {
169
- try {
170
- log('debug', `Fetching tasks for list ${listId}...`);
171
- const tasks = await taskService.getTasks(listId);
172
- log('debug', `Caching ${tasks.length} tasks for list ${listId}`);
173
- tasks.forEach(task => {
174
- cacheTask(task);
175
- });
176
- // Update last updated timestamp
177
- if (workspaceCache) {
178
- workspaceCache.lastUpdated = new Date();
179
- }
180
- }
181
- catch (error) {
182
- log('error', `Error caching tasks for list ${listId}:`, error);
183
- }
184
- }
185
- /**
186
- * Initialize the workspace cache
187
- * This should be called on server startup
188
- */
189
- export async function initializeCache() {
190
- try {
191
- log('info', 'Initializing workspace cache...');
192
- // Create new cache
193
- workspaceCache = createEmptyCache();
194
- log('debug', 'Empty cache structure created');
195
- // Get workspace hierarchy
196
- log('debug', 'Fetching workspace hierarchy from ClickUp API...');
197
- const hierarchy = await workspaceService.getWorkspaceHierarchy(true);
198
- workspaceCache.hierarchy = hierarchy;
199
- // Count spaces, folders, and lists in the hierarchy
200
- const stats = countHierarchyItems(hierarchy);
201
- log('info', 'Workspace hierarchy fetched successfully', stats);
202
- // Process hierarchy to populate ID lookup maps
203
- log('debug', 'Processing hierarchy to build lookup maps...');
204
- processHierarchy(hierarchy);
205
- // Log the number of items in each lookup map
206
- log('info', 'Lookup maps populated', {
207
- spaces: workspaceCache.spaceIdsByName.size,
208
- folders: workspaceCache.folderIdsByName.size,
209
- lists: workspaceCache.listIdsByName.size
210
- });
211
- // Cache tasks for each list
212
- log('info', 'Beginning task caching process for all lists...');
213
- // Function to find all list IDs in the hierarchy
214
- const getAllListIds = (node) => {
215
- let listIds = [];
216
- if (node.type === 'list') {
217
- listIds.push(node.id);
218
- }
219
- if (node.children && node.children.length > 0) {
220
- node.children.forEach(child => {
221
- listIds = [...listIds, ...getAllListIds(child)];
222
- });
223
- }
224
- return listIds;
225
- };
226
- // Get all list IDs from all spaces
227
- const listIds = [];
228
- hierarchy.root.children.forEach(spaceNode => {
229
- listIds.push(...getAllListIds(spaceNode));
230
- });
231
- log('info', `Found ${listIds.length} lists to cache tasks from`);
232
- // Cache tasks for each list (sequentially to avoid rate limiting)
233
- let completedLists = 0;
234
- let totalTasksCached = 0;
235
- for (const listId of listIds) {
236
- // Get list name if available
237
- const listName = getListNameById(listId, hierarchy);
238
- log('debug', `Processing list ${completedLists + 1}/${listIds.length}: ${listName || listId}`);
239
- // Get task count before caching
240
- const taskCountBefore = workspaceCache.tasksById.size;
241
- await cacheTasksForList(listId);
242
- // Calculate how many tasks were added
243
- const taskCountAfter = workspaceCache.tasksById.size;
244
- const tasksAdded = taskCountAfter - taskCountBefore;
245
- totalTasksCached += tasksAdded;
246
- completedLists++;
247
- // Log progress every 5 lists or for the last list
248
- if (completedLists % 5 === 0 || completedLists === listIds.length) {
249
- log('info', `Cache progress: ${completedLists}/${listIds.length} lists processed (${Math.round(completedLists / listIds.length * 100)}%)`, {
250
- totalTasksCached,
251
- completedLists,
252
- remainingLists: listIds.length - completedLists
253
- });
254
- }
255
- // Small delay to avoid rate limiting
256
- await new Promise(resolve => setTimeout(resolve, 100));
257
- }
258
- // Mark cache as initialized
259
- workspaceCache.initialized = true;
260
- workspaceCache.lastUpdated = new Date();
261
- // Log final cache stats
262
- const cacheStats = getCacheStats();
263
- log('info', `Workspace cache initialized successfully`, cacheStats);
264
- }
265
- catch (error) {
266
- log('error', 'Error initializing workspace cache:', error);
267
- // Reset cache on error
268
- workspaceCache = null;
269
- // Re-throw error
270
- throw error;
271
- }
272
- }
273
- /**
274
- * Helper function to count items in the hierarchy
275
- */
276
- function countHierarchyItems(hierarchy) {
277
- let spaces = 0;
278
- let folders = 0;
279
- let lists = 0;
280
- const countNode = (node) => {
281
- switch (node.type) {
282
- case 'space':
283
- spaces++;
284
- break;
285
- case 'folder':
286
- folders++;
287
- break;
288
- case 'list':
289
- lists++;
290
- break;
291
- }
292
- if (node.children && node.children.length > 0) {
293
- node.children.forEach(countNode);
294
- }
295
- };
296
- // Count all spaces (children of root)
297
- hierarchy.root.children.forEach(countNode);
298
- return { spaces, folders, lists };
299
- }
300
- /**
301
- * Helper function to get list name by ID
302
- */
303
- function getListNameById(listId, hierarchy) {
304
- let result = null;
305
- const findListName = (node) => {
306
- if (node.type === 'list' && node.id === listId) {
307
- result = node.name;
308
- return true;
309
- }
310
- if (node.children) {
311
- for (const child of node.children) {
312
- if (findListName(child)) {
313
- return true;
314
- }
315
- }
316
- }
317
- return false;
318
- };
319
- hierarchy.root.children.forEach(findListName);
320
- return result;
321
- }
322
- /**
323
- * Check if the cache is initialized
324
- */
325
- export function isCacheInitialized() {
326
- return !!(workspaceCache && workspaceCache.initialized);
327
- }
328
- /**
329
- * Get the workspace hierarchy from cache
330
- */
331
- export function getCachedHierarchy() {
332
- return workspaceCache?.hierarchy || null;
333
- }
334
- /**
335
- * Find an entity ID by name from the cache
336
- */
337
- export function findIDByNameInCache(name, type) {
338
- if (!workspaceCache || !workspaceCache.initialized) {
339
- return null;
340
- }
341
- const normalizedName = normalizeName(name);
342
- switch (type) {
343
- case 'space':
344
- return workspaceCache.spaceIdsByName.get(normalizedName) || null;
345
- case 'folder':
346
- return workspaceCache.folderIdsByName.get(normalizedName) || null;
347
- case 'list':
348
- return workspaceCache.listIdsByName.get(normalizedName) || null;
349
- default:
350
- return null;
351
- }
352
- }
353
- /**
354
- * Find a task ID by name and list ID from the cache
355
- */
356
- export function findTaskIDByName(taskName, listId) {
357
- if (!workspaceCache || !workspaceCache.initialized) {
358
- return null;
359
- }
360
- const nameKey = getTaskNameKey(listId, taskName);
361
- return workspaceCache.tasksByName.get(nameKey) || null;
362
- }
363
- /**
364
- * Get a task by ID from the cache
365
- */
366
- export function getTaskFromCache(taskId) {
367
- if (!workspaceCache || !workspaceCache.initialized) {
368
- return null;
369
- }
370
- return workspaceCache.tasksById.get(taskId) || null;
371
- }
372
- /**
373
- * Add a new task to the cache
374
- */
375
- export function addTaskToCache(task) {
376
- if (!workspaceCache || !workspaceCache.initialized) {
377
- return;
378
- }
379
- cacheTask(task);
380
- workspaceCache.lastUpdated = new Date();
381
- }
382
- /**
383
- * Remove a task from the cache
384
- */
385
- export function removeTaskFromCache(taskId) {
386
- if (!workspaceCache || !workspaceCache.initialized) {
387
- return;
388
- }
389
- removeCachedTask(taskId);
390
- workspaceCache.lastUpdated = new Date();
391
- }
392
- /**
393
- * Update a task in the cache
394
- */
395
- export function updateTaskInCache(taskId, updates) {
396
- if (!workspaceCache || !workspaceCache.initialized) {
397
- return;
398
- }
399
- updateCachedTask(taskId, updates);
400
- }
401
- /**
402
- * Refresh the tasks for a specific list in the cache
403
- */
404
- export async function refreshListTasksInCache(listId) {
405
- if (!workspaceCache || !workspaceCache.initialized) {
406
- return;
407
- }
408
- // Remove existing tasks for this list
409
- const tasksToRemove = [];
410
- workspaceCache.tasksById.forEach((task, id) => {
411
- if (task.listId === listId) {
412
- tasksToRemove.push(id);
413
- }
414
- });
415
- tasksToRemove.forEach(taskId => {
416
- removeCachedTask(taskId);
417
- });
418
- // Cache tasks for the list
419
- await cacheTasksForList(listId);
420
- }
421
- /**
422
- * Refresh the entire workspace cache
423
- */
424
- export async function refreshCache() {
425
- // Reset cache
426
- workspaceCache = null;
427
- // Re-initialize
428
- await initializeCache();
429
- }
430
- /**
431
- * Get cache statistics for monitoring
432
- */
433
- export function getCacheStats() {
434
- if (!workspaceCache) {
435
- return {
436
- initialized: false,
437
- tasksCount: 0,
438
- listsCount: 0,
439
- foldersCount: 0,
440
- spacesCount: 0,
441
- lastUpdated: null
442
- };
443
- }
444
- return {
445
- initialized: workspaceCache.initialized,
446
- tasksCount: workspaceCache.tasksById.size,
447
- listsCount: workspaceCache.listIdsByName.size,
448
- foldersCount: workspaceCache.folderIdsByName.size,
449
- spacesCount: workspaceCache.spaceIdsByName.size,
450
- lastUpdated: workspaceCache.lastUpdated
451
- };
452
- }