@taazkareem/clickup-mcp-server 0.6.5 → 0.6.7

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.
@@ -27,23 +27,65 @@ export class TaskServiceSearch extends TaskServiceCore {
27
27
  this.logOperation('findTaskByName', { listId, taskName });
28
28
  try {
29
29
  const tasks = await this.getTasks(listId);
30
- // Find task by exact match first, then case-insensitive, then substring
31
- // Exact match
32
- let match = tasks.find(task => task.name === taskName);
33
- if (match)
34
- return match;
35
- // Case-insensitive match
36
- match = tasks.find(task => task.name.toLowerCase() === taskName.toLowerCase());
37
- if (match)
38
- return match;
39
- // Substring match
40
- match = tasks.find(task => task.name.toLowerCase().includes(taskName.toLowerCase()));
41
- return match || null;
30
+ return this.findTaskInArray(tasks, taskName);
42
31
  }
43
32
  catch (error) {
44
33
  throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
45
34
  }
46
35
  }
36
+ /**
37
+ * Find a task by name from an array of tasks
38
+ * @param taskArray Array of tasks to search in
39
+ * @param name Name of the task to search for
40
+ * @param includeDetails Whether to add list context to task
41
+ * @returns The task that best matches the name, or null if no match
42
+ */
43
+ findTaskInArray(taskArray, name, includeDetails = false) {
44
+ if (!taskArray || !Array.isArray(taskArray) || taskArray.length === 0 || !name) {
45
+ return null;
46
+ }
47
+ // Get match scores for each task
48
+ const taskMatchScores = taskArray
49
+ .map(task => {
50
+ const matchResult = isNameMatch(task.name, name);
51
+ return {
52
+ task,
53
+ matchResult,
54
+ // Parse the date_updated field as a number for sorting
55
+ updatedAt: task.date_updated ? parseInt(task.date_updated, 10) : 0
56
+ };
57
+ })
58
+ .filter(result => result.matchResult.isMatch);
59
+ if (taskMatchScores.length === 0) {
60
+ return null;
61
+ }
62
+ // First, try to find exact matches
63
+ const exactMatches = taskMatchScores
64
+ .filter(result => result.matchResult.exactMatch)
65
+ .sort((a, b) => {
66
+ // For exact matches with the same score, sort by most recently updated
67
+ if (b.matchResult.score === a.matchResult.score) {
68
+ return b.updatedAt - a.updatedAt;
69
+ }
70
+ return b.matchResult.score - a.matchResult.score;
71
+ });
72
+ // Get the best matches based on whether we have exact matches or need to fall back to fuzzy matches
73
+ const bestMatches = exactMatches.length > 0 ? exactMatches : taskMatchScores.sort((a, b) => {
74
+ // First sort by match score (highest first)
75
+ if (b.matchResult.score !== a.matchResult.score) {
76
+ return b.matchResult.score - a.matchResult.score;
77
+ }
78
+ // Then sort by most recently updated
79
+ return b.updatedAt - a.updatedAt;
80
+ });
81
+ // Get the best match
82
+ const bestMatch = bestMatches[0].task;
83
+ // If we need to include more details
84
+ if (includeDetails) {
85
+ // Include any additional details needed
86
+ }
87
+ return bestMatch;
88
+ }
47
89
  /**
48
90
  * Formats a task into a lightweight summary format
49
91
  * @param task The task to format
@@ -177,10 +219,11 @@ export class TaskServiceSearch extends TaskServiceCore {
177
219
  * - useSmartDisambiguation: Whether to automatically select the most recently updated task
178
220
  * - includeFullDetails: Whether to include full task details (true) or just task summaries (false)
179
221
  * - includeListContext: Whether to include list/folder/space context with results
222
+ * - requireExactMatch: Whether to only consider exact name matches (true) or allow fuzzy matches (false)
180
223
  * @returns Either a single task or an array of tasks depending on options
181
224
  * @throws Error if task cannot be found or if multiple matches are found when not allowed
182
225
  */
183
- async findTasks({ taskId, customTaskId, taskName, listId, listName, allowMultipleMatches = false, useSmartDisambiguation = false, includeFullDetails = true, includeListContext = true }) {
226
+ async findTasks({ taskId, customTaskId, taskName, listId, listName, allowMultipleMatches = false, useSmartDisambiguation = true, includeFullDetails = true, includeListContext = false, requireExactMatch = false }) {
184
227
  try {
185
228
  this.logOperation('findTasks', {
186
229
  taskId,
@@ -189,8 +232,30 @@ export class TaskServiceSearch extends TaskServiceCore {
189
232
  listId,
190
233
  listName,
191
234
  allowMultipleMatches,
192
- useSmartDisambiguation
235
+ useSmartDisambiguation,
236
+ requireExactMatch
193
237
  });
238
+ // Check name-to-ID cache first if we have a task name
239
+ if (taskName && !taskId && !customTaskId) {
240
+ // Resolve list ID if we have a list name
241
+ let resolvedListId = listId;
242
+ if (listName && !listId) {
243
+ const listInfo = await findListIDByName(this.workspaceService, listName);
244
+ if (listInfo) {
245
+ resolvedListId = listInfo.id;
246
+ }
247
+ }
248
+ // Try to get cached task ID
249
+ const cachedTaskId = this.getCachedTaskId(taskName, resolvedListId);
250
+ if (cachedTaskId) {
251
+ this.logOperation('findTasks', {
252
+ message: 'Using cached task ID for name lookup',
253
+ taskName,
254
+ cachedTaskId
255
+ });
256
+ taskId = cachedTaskId;
257
+ }
258
+ }
194
259
  // Case 1: Direct task ID lookup (highest priority)
195
260
  if (taskId) {
196
261
  // Check if it looks like a custom ID
@@ -252,10 +317,12 @@ export class TaskServiceSearch extends TaskServiceCore {
252
317
  }
253
318
  resolvedListId = listInfo.id;
254
319
  }
255
- const foundTask = await this.findTaskByName(resolvedListId, taskName);
320
+ const foundTask = this.findTaskInArray(await this.getTasks(resolvedListId), taskName, includeListContext);
256
321
  if (!foundTask) {
257
322
  throw new Error(`Task "${taskName}" not found in list`);
258
323
  }
324
+ // Cache the task name to ID mapping with list context
325
+ this.cacheTaskNameToId(taskName, foundTask.id, resolvedListId);
259
326
  // If includeFullDetails is true and we need context not already in the task,
260
327
  // get full details, otherwise return what we already have
261
328
  if (includeFullDetails && (!foundTask.list || !foundTask.list.name || !foundTask.status)) {
@@ -265,7 +332,20 @@ export class TaskServiceSearch extends TaskServiceCore {
265
332
  }
266
333
  // Case 3b: Task name without list context - global lookup across workspace
267
334
  // Get lightweight task summaries for efficient first-pass filtering
268
- const response = await this.getTaskSummaries({});
335
+ this.logOperation('findTasks', {
336
+ message: `Starting global task search for "${taskName}"`,
337
+ includeFullDetails,
338
+ useSmartDisambiguation,
339
+ requireExactMatch
340
+ });
341
+ // Use statuses parameter to get both open and closed tasks
342
+ // Include additional filters to ensure we get as many tasks as possible
343
+ const response = await this.getTaskSummaries({
344
+ include_closed: true,
345
+ include_archived_lists: true,
346
+ include_closed_lists: true,
347
+ subtasks: true
348
+ });
269
349
  if (!this.workspaceService) {
270
350
  throw new Error("Workspace service required for global task lookup");
271
351
  }
@@ -302,27 +382,73 @@ export class TaskServiceSearch extends TaskServiceCore {
302
382
  }
303
383
  // Build the context map
304
384
  buildListContextMap(hierarchy.root.children);
305
- // Find tasks that match the provided name
385
+ // Find tasks that match the provided name with scored match results
306
386
  const initialMatches = [];
307
387
  // Process task summaries to find initial matches
388
+ let taskCount = 0;
389
+ let matchesFound = 0;
390
+ // Add additional logging to debug task matching
391
+ this.logOperation('findTasks', {
392
+ total_tasks_in_response: response.summaries.length,
393
+ search_term: taskName,
394
+ requireExactMatch
395
+ });
308
396
  for (const taskSummary of response.summaries) {
309
- if (isNameMatch(taskSummary.name, taskName)) {
397
+ taskCount++;
398
+ // Use isNameMatch for consistent matching behavior with scoring
399
+ const matchResult = isNameMatch(taskSummary.name, taskName);
400
+ const isMatch = matchResult.isMatch;
401
+ // For debugging, log every 20th task or any task with a similar name
402
+ if (taskCount % 20 === 0 || taskSummary.name.toLowerCase().includes(taskName.toLowerCase()) ||
403
+ taskName.toLowerCase().includes(taskSummary.name.toLowerCase())) {
404
+ this.logOperation('findTasks:matching', {
405
+ task_name: taskSummary.name,
406
+ search_term: taskName,
407
+ list_name: taskSummary.list?.name || 'Unknown list',
408
+ is_match: isMatch,
409
+ match_score: matchResult.score,
410
+ match_reason: matchResult.reason || 'no-match'
411
+ });
412
+ }
413
+ if (isMatch) {
414
+ matchesFound++;
310
415
  // Get list context information
311
416
  const listContext = listContextMap.get(taskSummary.list.id);
312
417
  if (listContext) {
313
- // Store task summary and context
418
+ // Store task summary and context with match score
314
419
  initialMatches.push({
315
420
  id: taskSummary.id,
316
421
  task: taskSummary,
317
- listContext
422
+ listContext,
423
+ matchScore: matchResult.score,
424
+ matchReason: matchResult.reason || 'unknown'
318
425
  });
319
426
  }
320
427
  }
321
428
  }
429
+ this.logOperation('findTasks', {
430
+ globalSearch: true,
431
+ searchTerm: taskName,
432
+ tasksSearched: taskCount,
433
+ matchesFound: matchesFound,
434
+ validMatchesWithContext: initialMatches.length
435
+ });
322
436
  // Handle the no matches case
323
437
  if (initialMatches.length === 0) {
324
438
  throw new Error(`Task "${taskName}" not found in any list across your workspace. Please check the task name and try again.`);
325
439
  }
440
+ // Sort matches by match score first (higher is better), then by update time
441
+ initialMatches.sort((a, b) => {
442
+ // First sort by match score (highest first)
443
+ if (b.matchScore !== a.matchScore) {
444
+ return b.matchScore - a.matchScore;
445
+ }
446
+ // Try to get the date_updated from the task
447
+ const aDate = a.task.date_updated ? parseInt(a.task.date_updated, 10) : 0;
448
+ const bDate = b.task.date_updated ? parseInt(b.task.date_updated, 10) : 0;
449
+ // For equal scores, sort by most recently updated
450
+ return bDate - aDate;
451
+ });
326
452
  // Handle the single match case - we can return early if we don't need full details
327
453
  if (initialMatches.length === 1 && !useSmartDisambiguation && !includeFullDetails) {
328
454
  const match = initialMatches[0];
@@ -345,12 +471,70 @@ export class TaskServiceSearch extends TaskServiceCore {
345
471
  }
346
472
  return match.task;
347
473
  }
474
+ // Handle the exact match case - if there's an exact or very good match, prefer it over others
475
+ // This is our key improvement to prefer exact matches over update time
476
+ const bestMatchScore = initialMatches[0].matchScore;
477
+ if (bestMatchScore >= 80) { // 80+ is an exact match or case-insensitive exact match
478
+ // If there's a single best match with score 80+, use it directly
479
+ const exactMatches = initialMatches.filter(m => m.matchScore >= 80);
480
+ if (exactMatches.length === 1 && !allowMultipleMatches) {
481
+ this.logOperation('findTasks', {
482
+ message: `Found single exact match with score ${exactMatches[0].matchScore}, prioritizing over other matches`,
483
+ matchReason: exactMatches[0].matchReason
484
+ });
485
+ // If we don't need details, return early
486
+ if (!includeFullDetails) {
487
+ const match = exactMatches[0];
488
+ if (includeListContext) {
489
+ return {
490
+ ...match.task,
491
+ list: {
492
+ id: match.listContext.listId,
493
+ name: match.listContext.listName
494
+ },
495
+ folder: match.listContext.folderId ? {
496
+ id: match.listContext.folderId,
497
+ name: match.listContext.folderName
498
+ } : undefined,
499
+ space: {
500
+ id: match.listContext.spaceId,
501
+ name: match.listContext.spaceName
502
+ }
503
+ };
504
+ }
505
+ return match.task;
506
+ }
507
+ // Otherwise, get the full details
508
+ const fullTask = await this.getTask(exactMatches[0].id);
509
+ if (includeListContext) {
510
+ const match = exactMatches[0];
511
+ // Enhance task with context information
512
+ fullTask.list = {
513
+ ...fullTask.list,
514
+ name: match.listContext.listName
515
+ };
516
+ if (match.listContext.folderId) {
517
+ fullTask.folder = {
518
+ id: match.listContext.folderId,
519
+ name: match.listContext.folderName
520
+ };
521
+ }
522
+ fullTask.space = {
523
+ id: match.listContext.spaceId,
524
+ name: match.listContext.spaceName
525
+ };
526
+ }
527
+ return fullTask;
528
+ }
529
+ }
348
530
  // For multiple matches or when we need details, fetch full task info
349
531
  const fullMatches = [];
532
+ const matchScoreMap = new Map(); // To preserve match scores
350
533
  try {
351
534
  // Process in sequence for better reliability
352
535
  for (const match of initialMatches) {
353
536
  const fullTask = await this.getTask(match.id);
537
+ matchScoreMap.set(fullTask.id, match.matchScore);
354
538
  if (includeListContext) {
355
539
  // Enhance task with context information
356
540
  fullTask.list = {
@@ -370,9 +554,16 @@ export class TaskServiceSearch extends TaskServiceCore {
370
554
  }
371
555
  fullMatches.push(fullTask);
372
556
  }
373
- // Sort by update time for disambiguation
557
+ // Sort matches - first by match score, then by update time
374
558
  if (fullMatches.length > 1) {
375
559
  fullMatches.sort((a, b) => {
560
+ // First sort by match score (highest first)
561
+ const aScore = matchScoreMap.get(a.id) || 0;
562
+ const bScore = matchScoreMap.get(b.id) || 0;
563
+ if (aScore !== bScore) {
564
+ return bScore - aScore;
565
+ }
566
+ // For equal scores, sort by update time
376
567
  const aDate = parseInt(a.date_updated || '0', 10);
377
568
  const bDate = parseInt(b.date_updated || '0', 10);
378
569
  return bDate - aDate; // Most recent first
@@ -404,7 +595,7 @@ export class TaskServiceSearch extends TaskServiceCore {
404
595
  }));
405
596
  }
406
597
  else {
407
- // For single result, return the first match
598
+ // For single result, return the first match (best match score)
408
599
  const match = initialMatches[0];
409
600
  return {
410
601
  ...match.task,
@@ -423,9 +614,15 @@ export class TaskServiceSearch extends TaskServiceCore {
423
614
  };
424
615
  }
425
616
  }
617
+ // After finding the task in global search, cache the mapping
618
+ if (initialMatches.length === 1 || useSmartDisambiguation) {
619
+ const bestMatch = fullMatches[0];
620
+ this.cacheTaskNameToId(taskName, bestMatch.id, bestMatch.list?.id);
621
+ return bestMatch;
622
+ }
426
623
  // Return results based on options
427
624
  if (fullMatches.length === 1 || useSmartDisambiguation) {
428
- return fullMatches[0]; // Return most recently updated if multiple and smart disambiguation enabled
625
+ return fullMatches[0]; // Return best match (sorted by score then update time)
429
626
  }
430
627
  else if (allowMultipleMatches) {
431
628
  return fullMatches; // Return all matches
@@ -439,10 +636,16 @@ export class TaskServiceSearch extends TaskServiceCore {
439
636
  const updateTime = task.date_updated
440
637
  ? new Date(parseInt(task.date_updated, 10)).toLocaleString()
441
638
  : "Unknown date";
639
+ const matchScore = matchScoreMap.get(task.id) || 0;
640
+ const matchQuality = matchScore >= 100 ? "Exact match" :
641
+ matchScore >= 80 ? "Case-insensitive exact match" :
642
+ matchScore >= 70 ? "Text match ignoring emojis" :
643
+ matchScore >= 50 ? "Contains search term" :
644
+ "Partial match";
442
645
  const location = `list "${listName}"${folderName ? ` (folder: "${folderName}")` : ''} (space: "${spaceName}")`;
443
- return `- "${task.name}" in ${location} - Updated ${updateTime}`;
646
+ return `- "${task.name}" in ${location} - Updated ${updateTime} - Match quality: ${matchQuality} (${matchScore}/100)`;
444
647
  }).join('\n');
445
- throw new Error(`Multiple tasks found with name "${taskName}":\n${matchesInfo}\n\nPlease provide list context to disambiguate or set allowMultipleMatches to true.`);
648
+ throw new Error(`Multiple tasks found with name "${taskName}":\n${matchesInfo}\n\nPlease provide list context to disambiguate, use the exact task name with requireExactMatch=true, or set allowMultipleMatches to true.`);
446
649
  }
447
650
  }
448
651
  // No valid lookup parameters provided
@@ -459,4 +662,154 @@ export class TaskServiceSearch extends TaskServiceCore {
459
662
  throw this.handleError(error, `Error finding task: ${error.message}`);
460
663
  }
461
664
  }
665
+ /**
666
+ * Update a task by name within a specific list
667
+ * @param listId The ID of the list containing the task
668
+ * @param taskName The name of the task to update
669
+ * @param updateData The data to update the task with
670
+ * @returns The updated task
671
+ */
672
+ async updateTaskByName(listId, taskName, updateData) {
673
+ this.logOperation('updateTaskByName', { listId, taskName, ...updateData });
674
+ try {
675
+ const task = await this.findTaskByName(listId, taskName);
676
+ if (!task) {
677
+ throw new Error(`Task "${taskName}" not found in list ${listId}`);
678
+ }
679
+ return await this.updateTask(task.id, updateData);
680
+ }
681
+ catch (error) {
682
+ throw this.handleError(error, `Failed to update task by name: ${error instanceof Error ? error.message : String(error)}`);
683
+ }
684
+ }
685
+ /**
686
+ * Global task search by name across all lists
687
+ * This is a specialized method that uses getWorkspaceTasks to search all lists at once
688
+ * which is more efficient than searching list by list
689
+ *
690
+ * @param taskName The name to search for
691
+ * @returns The best matching task or null if no match found
692
+ */
693
+ async findTaskByNameGlobally(taskName) {
694
+ this.logOperation('findTaskByNameGlobally', { taskName });
695
+ // Use a static cache for task data to avoid redundant API calls
696
+ // This dramatically reduces API usage across multiple task lookups
697
+ if (!this.constructor.hasOwnProperty('_taskCache')) {
698
+ Object.defineProperty(this.constructor, '_taskCache', {
699
+ value: {
700
+ tasks: [],
701
+ lastFetch: 0,
702
+ cacheTTL: 60000, // 1 minute cache TTL
703
+ },
704
+ writable: true
705
+ });
706
+ }
707
+ const cache = this.constructor._taskCache;
708
+ const now = Date.now();
709
+ try {
710
+ // Use cached tasks if available and not expired
711
+ let tasks = [];
712
+ if (cache.tasks.length > 0 && (now - cache.lastFetch) < cache.cacheTTL) {
713
+ this.logOperation('findTaskByNameGlobally', {
714
+ usedCache: true,
715
+ cacheAge: now - cache.lastFetch,
716
+ taskCount: cache.tasks.length
717
+ });
718
+ tasks = cache.tasks;
719
+ }
720
+ else {
721
+ // Get tasks using a single efficient workspace-wide API call
722
+ const response = await this.getWorkspaceTasks({
723
+ include_closed: true,
724
+ detail_level: 'detailed'
725
+ });
726
+ tasks = 'tasks' in response ? response.tasks : [];
727
+ // Update cache
728
+ cache.tasks = tasks;
729
+ cache.lastFetch = now;
730
+ this.logOperation('findTaskByNameGlobally', {
731
+ usedCache: false,
732
+ fetchedTaskCount: tasks.length
733
+ });
734
+ }
735
+ // Map tasks to include match scores and updated time for sorting
736
+ const taskMatches = tasks.map(task => {
737
+ const matchResult = isNameMatch(task.name, taskName);
738
+ return {
739
+ task,
740
+ matchResult,
741
+ updatedAt: task.date_updated ? parseInt(task.date_updated, 10) : 0
742
+ };
743
+ }).filter(result => result.matchResult.isMatch);
744
+ this.logOperation('findTaskByNameGlobally', {
745
+ taskCount: tasks.length,
746
+ matchCount: taskMatches.length,
747
+ taskName
748
+ });
749
+ if (taskMatches.length === 0) {
750
+ return null;
751
+ }
752
+ // First try exact matches
753
+ const exactMatches = taskMatches
754
+ .filter(result => result.matchResult.exactMatch)
755
+ .sort((a, b) => {
756
+ // For exact matches with the same score, sort by most recently updated
757
+ if (b.matchResult.score === a.matchResult.score) {
758
+ return b.updatedAt - a.updatedAt;
759
+ }
760
+ return b.matchResult.score - a.matchResult.score;
761
+ });
762
+ // Get the best matches based on whether we have exact matches or need to fall back to fuzzy matches
763
+ const bestMatches = exactMatches.length > 0 ? exactMatches : taskMatches.sort((a, b) => {
764
+ // First sort by match score (highest first)
765
+ if (b.matchResult.score !== a.matchResult.score) {
766
+ return b.matchResult.score - a.matchResult.score;
767
+ }
768
+ // Then sort by most recently updated
769
+ return b.updatedAt - a.updatedAt;
770
+ });
771
+ // Log the top matches for debugging
772
+ const topMatches = bestMatches.slice(0, 3).map(match => ({
773
+ taskName: match.task.name,
774
+ score: match.matchResult.score,
775
+ reason: match.matchResult.reason,
776
+ updatedAt: match.updatedAt,
777
+ list: match.task.list?.name || 'Unknown list'
778
+ }));
779
+ this.logOperation('findTaskByNameGlobally', { topMatches });
780
+ // Return the best match
781
+ return bestMatches[0].task;
782
+ }
783
+ catch (error) {
784
+ this.logOperation('findTaskByNameGlobally', { error: error.message });
785
+ // If there's an error (like rate limit), try to use cached data even if expired
786
+ if (cache.tasks.length > 0) {
787
+ this.logOperation('findTaskByNameGlobally', {
788
+ message: 'Using expired cache due to API error',
789
+ cacheAge: now - cache.lastFetch
790
+ });
791
+ // Perform the same matching logic with cached data
792
+ const taskMatches = cache.tasks
793
+ .map(task => {
794
+ const matchResult = isNameMatch(task.name, taskName);
795
+ return {
796
+ task,
797
+ matchResult,
798
+ updatedAt: task.date_updated ? parseInt(task.date_updated, 10) : 0
799
+ };
800
+ })
801
+ .filter(result => result.matchResult.isMatch)
802
+ .sort((a, b) => {
803
+ if (b.matchResult.score !== a.matchResult.score) {
804
+ return b.matchResult.score - a.matchResult.score;
805
+ }
806
+ return b.updatedAt - a.updatedAt;
807
+ });
808
+ if (taskMatches.length > 0) {
809
+ return taskMatches[0].task;
810
+ }
811
+ }
812
+ return null;
813
+ }
814
+ }
462
815
  }
@@ -7,6 +7,9 @@
7
7
  * Handles workspace hierarchy and space-related operations
8
8
  */
9
9
  import { BaseClickUpService, ClickUpServiceError, ErrorCode } from './base.js';
10
+ import { Logger } from '../../logger.js';
11
+ // Create a logger instance for workspace service
12
+ const logger = new Logger('WorkspaceService');
10
13
  /**
11
14
  * Service for workspace-related operations
12
15
  */
@@ -29,8 +32,7 @@ export class WorkspaceService extends BaseClickUpService {
29
32
  * @returns - A standardized ClickUpServiceError
30
33
  */
31
34
  handleError(error, message) {
32
- // Log the error for debugging
33
- console.error('WorkspaceService error:', error);
35
+ logger.error('WorkspaceService error:', error);
34
36
  // If the error is already a ClickUpServiceError, return it
35
37
  if (error instanceof ClickUpServiceError) {
36
38
  return error;
@@ -104,11 +106,11 @@ export class WorkspaceService extends BaseClickUpService {
104
106
  try {
105
107
  // If we have the hierarchy in memory and not forcing refresh, return it
106
108
  if (this.workspaceHierarchy && !forceRefresh) {
107
- this.logger.debug('Returning cached workspace hierarchy');
109
+ logger.debug('Returning cached workspace hierarchy');
108
110
  return this.workspaceHierarchy;
109
111
  }
110
112
  const startTime = Date.now();
111
- this.logger.info('Starting workspace hierarchy fetch');
113
+ logger.info('Starting workspace hierarchy fetch');
112
114
  // Start building the workspace tree
113
115
  const workspaceTree = {
114
116
  root: {
@@ -121,7 +123,7 @@ export class WorkspaceService extends BaseClickUpService {
121
123
  const spacesStartTime = Date.now();
122
124
  const spaces = await this.getSpaces();
123
125
  const spacesTime = Date.now() - spacesStartTime;
124
- this.logger.info(`Fetched ${spaces.length} spaces in ${spacesTime}ms`);
126
+ logger.info(`Fetched ${spaces.length} spaces in ${spacesTime}ms`);
125
127
  // Process spaces in batches to respect rate limits
126
128
  const batchSize = 3; // Process 3 spaces at a time
127
129
  const spaceNodes = [];
@@ -130,7 +132,7 @@ export class WorkspaceService extends BaseClickUpService {
130
132
  for (let i = 0; i < spaces.length; i += batchSize) {
131
133
  const batchStartTime = Date.now();
132
134
  const spaceBatch = spaces.slice(i, i + batchSize);
133
- this.logger.debug(`Processing space batch ${i / batchSize + 1} of ${Math.ceil(spaces.length / batchSize)} (${spaceBatch.length} spaces)`);
135
+ logger.debug(`Processing space batch ${i / batchSize + 1} of ${Math.ceil(spaces.length / batchSize)} (${spaceBatch.length} spaces)`);
134
136
  const batchNodes = await Promise.all(spaceBatch.map(async (space) => {
135
137
  const spaceStartTime = Date.now();
136
138
  const spaceNode = {
@@ -173,12 +175,12 @@ export class WorkspaceService extends BaseClickUpService {
173
175
  }));
174
176
  folderNodes.push(...batchFolderNodes);
175
177
  const folderBatchTime = Date.now() - folderBatchStartTime;
176
- this.logger.debug(`Processed folder batch in space ${space.name} in ${folderBatchTime}ms (${folderBatch.length} folders)`);
178
+ logger.debug(`Processed folder batch in space ${space.name} in ${folderBatchTime}ms (${folderBatch.length} folders)`);
177
179
  }
178
180
  // Add folder nodes to space
179
181
  spaceNode.children?.push(...folderNodes);
180
182
  // Add folderless lists to space
181
- this.logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name}`);
183
+ logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name}`);
182
184
  const listNodes = listsInSpace.map(list => ({
183
185
  id: list.id,
184
186
  name: list.name,
@@ -187,17 +189,17 @@ export class WorkspaceService extends BaseClickUpService {
187
189
  }));
188
190
  spaceNode.children?.push(...listNodes);
189
191
  const spaceTime = Date.now() - spaceStartTime;
190
- this.logger.info(`Processed space ${space.name} in ${spaceTime}ms (${folders.length} folders, ${listsInSpace.length} lists)`);
192
+ logger.info(`Processed space ${space.name} in ${spaceTime}ms (${folders.length} folders, ${listsInSpace.length} lists)`);
191
193
  return spaceNode;
192
194
  }));
193
195
  spaceNodes.push(...batchNodes);
194
196
  const batchTime = Date.now() - batchStartTime;
195
- this.logger.info(`Processed space batch in ${batchTime}ms (${spaceBatch.length} spaces)`);
197
+ logger.info(`Processed space batch in ${batchTime}ms (${spaceBatch.length} spaces)`);
196
198
  }
197
199
  // Add all space nodes to the workspace tree
198
200
  workspaceTree.root.children.push(...spaceNodes);
199
201
  const totalTime = Date.now() - startTime;
200
- this.logger.info('Workspace hierarchy fetch completed', {
202
+ logger.info('Workspace hierarchy fetch completed', {
201
203
  duration: totalTime,
202
204
  spaces: spaces.length,
203
205
  folders: totalFolders,
@@ -293,7 +295,7 @@ export class WorkspaceService extends BaseClickUpService {
293
295
  try {
294
296
  // The /space/{space_id}/list endpoint already returns folderless lists only
295
297
  const lists = await this.getFolderlessLists(spaceId);
296
- this.logger.debug(`Found ${lists.length} folderless lists in space ${spaceId}`);
298
+ logger.debug(`Found ${lists.length} folderless lists in space ${spaceId}`);
297
299
  // Return all lists without filtering since the API already returns folderless lists
298
300
  return lists;
299
301
  }