@taazkareem/clickup-mcp-server 0.8.4 → 0.9.0

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.
Files changed (36) hide show
  1. package/LICENSE +9 -17
  2. package/README.md +33 -38
  3. package/build/enhanced_server.js +262 -0
  4. package/build/index.js +9 -3
  5. package/build/license.js +172 -0
  6. package/build/middleware/auth.js +211 -0
  7. package/build/routes/auth.js +306 -0
  8. package/build/schemas/member.js +13 -0
  9. package/build/server.js +15 -1
  10. package/build/server.log +15 -0
  11. package/build/services/auth/oauth2.js +236 -0
  12. package/build/services/auth/session.js +337 -0
  13. package/build/services/clickup/adapter.js +281 -0
  14. package/build/services/clickup/factory.js +339 -0
  15. package/build/services/clickup/task/task-attachments.js +20 -12
  16. package/build/services/clickup/task/task-comments.js +19 -9
  17. package/build/services/clickup/task/task-core.js +68 -4
  18. package/build/services/clickup/task/task-custom-fields.js +23 -13
  19. package/build/services/clickup/task/task-search.js +79 -71
  20. package/build/services/clickup/task/task-service.js +88 -9
  21. package/build/services/clickup/task/task-tags.js +25 -13
  22. package/build/sse_server.js +4 -4
  23. package/build/tools/documents.js +11 -4
  24. package/build/tools/health.js +23 -0
  25. package/build/tools/member.js +2 -4
  26. package/build/tools/task/bulk-operations.js +5 -5
  27. package/build/tools/task/handlers.js +62 -12
  28. package/build/tools/task/single-operations.js +9 -9
  29. package/build/tools/task/time-tracking.js +61 -170
  30. package/build/tools/task/utilities.js +56 -22
  31. package/build/utils/date-utils.js +341 -141
  32. package/build/utils/schema-compatibility.js +222 -0
  33. package/build/utils/universal-schema-compatibility.js +171 -0
  34. package/build/virtual-sdk/generator.js +53 -0
  35. package/build/virtual-sdk/registry.js +45 -0
  36. package/package.json +2 -2
@@ -9,14 +9,22 @@
9
9
  * - Global workspace task lookup
10
10
  * - Task summaries and detailed task data
11
11
  */
12
- import { TaskServiceCore } from './task-core.js';
13
12
  import { isNameMatch } from '../../../utils/resolver-utils.js';
14
13
  import { findListIDByName } from '../../../tools/list.js';
15
14
  import { estimateTokensFromObject, wouldExceedTokenLimit } from '../../../utils/token-utils.js';
16
15
  /**
17
16
  * Search functionality for the TaskService
17
+ *
18
+ * This service handles all search and lookup operations for ClickUp tasks.
19
+ * It uses composition to access core functionality instead of inheritance.
20
+ *
21
+ * REFACTORED: Now uses composition instead of inheritance.
22
+ * Only depends on TaskServiceCore for base functionality.
18
23
  */
19
- export class TaskServiceSearch extends TaskServiceCore {
24
+ export class TaskServiceSearch {
25
+ constructor(core) {
26
+ this.core = core;
27
+ }
20
28
  /**
21
29
  * Find a task by name within a specific list
22
30
  * @param listId The ID of the list to search in
@@ -24,13 +32,13 @@ export class TaskServiceSearch extends TaskServiceCore {
24
32
  * @returns The task if found, otherwise null
25
33
  */
26
34
  async findTaskByName(listId, taskName) {
27
- this.logOperation('findTaskByName', { listId, taskName });
35
+ this.core.logOperation('findTaskByName', { listId, taskName });
28
36
  try {
29
- const tasks = await this.getTasks(listId);
37
+ const tasks = await this.core.getTasks(listId);
30
38
  return this.findTaskInArray(tasks, taskName);
31
39
  }
32
40
  catch (error) {
33
- throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
41
+ throw this.core.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
34
42
  }
35
43
  }
36
44
  /**
@@ -102,7 +110,7 @@ export class TaskServiceSearch extends TaskServiceCore {
102
110
  },
103
111
  due_date: task.due_date,
104
112
  url: task.url,
105
- priority: this.extractPriorityValue(task),
113
+ priority: this.core.extractPriorityValue(task),
106
114
  tags: task.tags.map(tag => ({
107
115
  name: tag.name,
108
116
  tag_bg: tag.tag_bg,
@@ -125,10 +133,10 @@ export class TaskServiceSearch extends TaskServiceCore {
125
133
  */
126
134
  async getWorkspaceTasks(filters = {}) {
127
135
  try {
128
- this.logOperation('getWorkspaceTasks', { filters });
129
- const params = this.buildTaskFilterParams(filters);
130
- const response = await this.makeRequest(async () => {
131
- return await this.client.get(`/team/${this.teamId}/task`, {
136
+ this.core.logOperation('getWorkspaceTasks', { filters });
137
+ const params = this.core.buildTaskFilterParams(filters);
138
+ const response = await this.core.makeRequest(async () => {
139
+ return await this.core.client.get(`/team/${this.core.teamId}/task`, {
132
140
  params
133
141
  });
134
142
  });
@@ -147,7 +155,7 @@ export class TaskServiceSearch extends TaskServiceCore {
147
155
  // First check with a sample task - if one task exceeds the limit, we definitely need summary
148
156
  const sampleTask = tasks[0];
149
157
  // Check if all tasks would exceed the token limit
150
- const estimatedTokensPerTask = this.estimateTaskTokens(sampleTask);
158
+ const estimatedTokensPerTask = this.core.estimateTaskTokens(sampleTask);
151
159
  const estimatedTotalTokens = estimatedTokensPerTask * tasks.length;
152
160
  // Add 10% overhead for the response wrapper
153
161
  tokensExceedLimit = estimatedTotalTokens * 1.1 > TOKEN_LIMIT;
@@ -159,15 +167,15 @@ export class TaskServiceSearch extends TaskServiceCore {
159
167
  }
160
168
  // Determine if we should return summary or detailed based on request and token limit
161
169
  const shouldUseSummary = filters.detail_level === 'summary' || tokensExceedLimit;
162
- this.logOperation('getWorkspaceTasks', {
170
+ this.core.logOperation('getWorkspaceTasks', {
163
171
  totalTasks: tasks.length,
164
- estimatedTokens: tasks.reduce((count, task) => count + this.estimateTaskTokens(task), 0),
172
+ estimatedTokens: tasks.reduce((count, task) => count + this.core.estimateTaskTokens(task), 0),
165
173
  usingDetailedFormat: !shouldUseSummary,
166
174
  requestedFormat: filters.detail_level || 'auto'
167
175
  });
168
176
  if (shouldUseSummary) {
169
177
  return {
170
- summaries: tasks.map(task => this.formatTaskSummary(task)),
178
+ summaries: tasks.map(task => this.core.formatTaskSummary(task)),
171
179
  total_count: totalCount,
172
180
  has_more: hasMore,
173
181
  next_page: nextPage
@@ -181,8 +189,8 @@ export class TaskServiceSearch extends TaskServiceCore {
181
189
  };
182
190
  }
183
191
  catch (error) {
184
- this.logOperation('getWorkspaceTasks', { error: error.message, status: error.response?.status });
185
- throw this.handleError(error, 'Failed to get workspace tasks');
192
+ this.core.logOperation('getWorkspaceTasks', { error: error.message, status: error.response?.status });
193
+ throw this.core.handleError(error, 'Failed to get workspace tasks');
186
194
  }
187
195
  }
188
196
  /**
@@ -200,13 +208,13 @@ export class TaskServiceSearch extends TaskServiceCore {
200
208
  */
201
209
  async getListViews(listId) {
202
210
  try {
203
- this.logOperation('getListViews', { listId });
204
- const response = await this.makeRequest(async () => {
205
- return await this.client.get(`/list/${listId}/view`);
211
+ this.core.logOperation('getListViews', { listId });
212
+ const response = await this.core.makeRequest(async () => {
213
+ return await this.core.client.get(`/list/${listId}/view`);
206
214
  });
207
215
  // First try to get the default list view from required_views.list
208
216
  if (response.data.required_views?.list?.id) {
209
- this.logOperation('getListViews', {
217
+ this.core.logOperation('getListViews', {
210
218
  listId,
211
219
  foundDefaultView: response.data.required_views.list.id,
212
220
  source: 'required_views.list'
@@ -217,7 +225,7 @@ export class TaskServiceSearch extends TaskServiceCore {
217
225
  const listView = response.data.views?.find(view => view.type?.toLowerCase() === 'list' ||
218
226
  view.name?.toLowerCase().includes('list'));
219
227
  if (listView?.id) {
220
- this.logOperation('getListViews', {
228
+ this.core.logOperation('getListViews', {
221
229
  listId,
222
230
  foundDefaultView: listView.id,
223
231
  source: 'views_array_fallback',
@@ -228,7 +236,7 @@ export class TaskServiceSearch extends TaskServiceCore {
228
236
  // If no specific list view found, use the first available view
229
237
  if (response.data.views?.length > 0) {
230
238
  const firstView = response.data.views[0];
231
- this.logOperation('getListViews', {
239
+ this.core.logOperation('getListViews', {
232
240
  listId,
233
241
  foundDefaultView: firstView.id,
234
242
  source: 'first_available_view',
@@ -237,7 +245,7 @@ export class TaskServiceSearch extends TaskServiceCore {
237
245
  });
238
246
  return firstView.id;
239
247
  }
240
- this.logOperation('getListViews', {
248
+ this.core.logOperation('getListViews', {
241
249
  listId,
242
250
  error: 'No views found for list',
243
251
  responseData: response.data
@@ -245,12 +253,12 @@ export class TaskServiceSearch extends TaskServiceCore {
245
253
  return null;
246
254
  }
247
255
  catch (error) {
248
- this.logOperation('getListViews', {
256
+ this.core.logOperation('getListViews', {
249
257
  listId,
250
258
  error: error.message,
251
259
  status: error.response?.status
252
260
  });
253
- throw this.handleError(error, `Failed to get views for list ${listId}`);
261
+ throw this.core.handleError(error, `Failed to get views for list ${listId}`);
254
262
  }
255
263
  }
256
264
  /**
@@ -261,7 +269,7 @@ export class TaskServiceSearch extends TaskServiceCore {
261
269
  */
262
270
  async getTasksFromView(viewId, filters = {}) {
263
271
  try {
264
- this.logOperation('getTasksFromView', { viewId, filters });
272
+ this.core.logOperation('getTasksFromView', { viewId, filters });
265
273
  // Build query parameters for supported filters
266
274
  const params = {};
267
275
  // Map supported filters to query parameters
@@ -309,8 +317,8 @@ export class TaskServiceSearch extends TaskServiceCore {
309
317
  let pageCount = 0;
310
318
  while (hasMore && pageCount < maxPages) {
311
319
  const pageParams = { ...params, page: currentPage };
312
- const response = await this.makeRequest(async () => {
313
- return await this.client.get(`/view/${viewId}/task`, {
320
+ const response = await this.core.makeRequest(async () => {
321
+ return await this.core.client.get(`/view/${viewId}/task`, {
314
322
  params: pageParams
315
323
  });
316
324
  });
@@ -320,7 +328,7 @@ export class TaskServiceSearch extends TaskServiceCore {
320
328
  hasMore = response.data.has_more === true && tasks.length > 0;
321
329
  currentPage++;
322
330
  pageCount++;
323
- this.logOperation('getTasksFromView', {
331
+ this.core.logOperation('getTasksFromView', {
324
332
  viewId,
325
333
  page: currentPage - 1,
326
334
  tasksInPage: tasks.length,
@@ -334,13 +342,13 @@ export class TaskServiceSearch extends TaskServiceCore {
334
342
  }
335
343
  }
336
344
  if (pageCount >= maxPages) {
337
- this.logOperation('getTasksFromView', {
345
+ this.core.logOperation('getTasksFromView', {
338
346
  viewId,
339
347
  warning: `Reached maximum page limit (${maxPages}) while fetching tasks`,
340
348
  totalTasks: allTasks.length
341
349
  });
342
350
  }
343
- this.logOperation('getTasksFromView', {
351
+ this.core.logOperation('getTasksFromView', {
344
352
  viewId,
345
353
  totalTasks: allTasks.length,
346
354
  totalPages: pageCount
@@ -348,12 +356,12 @@ export class TaskServiceSearch extends TaskServiceCore {
348
356
  return allTasks;
349
357
  }
350
358
  catch (error) {
351
- this.logOperation('getTasksFromView', {
359
+ this.core.logOperation('getTasksFromView', {
352
360
  viewId,
353
361
  error: error.message,
354
362
  status: error.response?.status
355
363
  });
356
- throw this.handleError(error, `Failed to get tasks from view ${viewId}`);
364
+ throw this.core.handleError(error, `Failed to get tasks from view ${viewId}`);
357
365
  }
358
366
  }
359
367
  /**
@@ -388,7 +396,7 @@ export class TaskServiceSearch extends TaskServiceCore {
388
396
  */
389
397
  async findTasks({ taskId, customTaskId, taskName, listId, listName, allowMultipleMatches = false, useSmartDisambiguation = true, includeFullDetails = true, includeListContext = false, requireExactMatch = false }) {
390
398
  try {
391
- this.logOperation('findTasks', {
399
+ this.core.logOperation('findTasks', {
392
400
  taskId,
393
401
  customTaskId,
394
402
  taskName,
@@ -403,15 +411,15 @@ export class TaskServiceSearch extends TaskServiceCore {
403
411
  // Resolve list ID if we have a list name
404
412
  let resolvedListId = listId;
405
413
  if (listName && !listId) {
406
- const listInfo = await findListIDByName(this.workspaceService, listName);
414
+ const listInfo = await findListIDByName(this.core.workspaceService, listName);
407
415
  if (listInfo) {
408
416
  resolvedListId = listInfo.id;
409
417
  }
410
418
  }
411
419
  // Try to get cached task ID
412
- const cachedTaskId = this.getCachedTaskId(taskName, resolvedListId);
420
+ const cachedTaskId = this.core.getCachedTaskId(taskName, resolvedListId);
413
421
  if (cachedTaskId) {
414
- this.logOperation('findTasks', {
422
+ this.core.logOperation('findTasks', {
415
423
  message: 'Using cached task ID for name lookup',
416
424
  taskName,
417
425
  cachedTaskId
@@ -423,7 +431,7 @@ export class TaskServiceSearch extends TaskServiceCore {
423
431
  if (taskId) {
424
432
  // Check if it looks like a custom ID
425
433
  if (taskId.includes('-') && /^[A-Z]+\-\d+$/.test(taskId)) {
426
- this.logOperation('findTasks', { detectedCustomId: taskId });
434
+ this.core.logOperation('findTasks', { detectedCustomId: taskId });
427
435
  try {
428
436
  // Try to get it as a custom ID first
429
437
  let resolvedListId;
@@ -431,25 +439,25 @@ export class TaskServiceSearch extends TaskServiceCore {
431
439
  resolvedListId = listId;
432
440
  }
433
441
  else if (listName) {
434
- const listInfo = await findListIDByName(this.workspaceService, listName);
442
+ const listInfo = await findListIDByName(this.core.workspaceService, listName);
435
443
  if (listInfo) {
436
444
  resolvedListId = listInfo.id;
437
445
  }
438
446
  }
439
- const foundTask = await this.getTaskByCustomId(taskId, resolvedListId);
447
+ const foundTask = await this.core.getTaskByCustomId(taskId, resolvedListId);
440
448
  return foundTask;
441
449
  }
442
450
  catch (error) {
443
451
  // If it fails as a custom ID, try as a regular ID
444
- this.logOperation('findTasks', {
452
+ this.core.logOperation('findTasks', {
445
453
  message: `Failed to find task with custom ID "${taskId}", falling back to regular ID`,
446
454
  error: error.message
447
455
  });
448
- return await this.getTask(taskId);
456
+ return await this.core.getTask(taskId);
449
457
  }
450
458
  }
451
459
  // Regular task ID
452
- return await this.getTask(taskId);
460
+ return await this.core.getTask(taskId);
453
461
  }
454
462
  // Case 2: Explicit custom task ID lookup
455
463
  if (customTaskId) {
@@ -458,12 +466,12 @@ export class TaskServiceSearch extends TaskServiceCore {
458
466
  resolvedListId = listId;
459
467
  }
460
468
  else if (listName) {
461
- const listInfo = await findListIDByName(this.workspaceService, listName);
469
+ const listInfo = await findListIDByName(this.core.workspaceService, listName);
462
470
  if (listInfo) {
463
471
  resolvedListId = listInfo.id;
464
472
  }
465
473
  }
466
- return await this.getTaskByCustomId(customTaskId, resolvedListId);
474
+ return await this.core.getTaskByCustomId(customTaskId, resolvedListId);
467
475
  }
468
476
  // Case 3: Task name lookup (requires either list context or global lookup)
469
477
  if (taskName) {
@@ -474,28 +482,28 @@ export class TaskServiceSearch extends TaskServiceCore {
474
482
  resolvedListId = listId;
475
483
  }
476
484
  else {
477
- const listInfo = await findListIDByName(this.workspaceService, listName);
485
+ const listInfo = await findListIDByName(this.core.workspaceService, listName);
478
486
  if (!listInfo) {
479
487
  throw new Error(`List "${listName}" not found`);
480
488
  }
481
489
  resolvedListId = listInfo.id;
482
490
  }
483
- const foundTask = this.findTaskInArray(await this.getTasks(resolvedListId), taskName, includeListContext);
491
+ const foundTask = this.core.findTaskInArray(await this.core.getTasks(resolvedListId), taskName, includeListContext);
484
492
  if (!foundTask) {
485
493
  throw new Error(`Task "${taskName}" not found in list`);
486
494
  }
487
495
  // Cache the task name to ID mapping with list context
488
- this.cacheTaskNameToId(taskName, foundTask.id, resolvedListId);
496
+ this.core.cacheTaskNameToId(taskName, foundTask.id, resolvedListId);
489
497
  // If includeFullDetails is true and we need context not already in the task,
490
498
  // get full details, otherwise return what we already have
491
499
  if (includeFullDetails && (!foundTask.list || !foundTask.list.name || !foundTask.status)) {
492
- return await this.getTask(foundTask.id);
500
+ return await this.core.getTask(foundTask.id);
493
501
  }
494
502
  return foundTask;
495
503
  }
496
504
  // Case 3b: Task name without list context - global lookup across workspace
497
505
  // Get lightweight task summaries for efficient first-pass filtering
498
- this.logOperation('findTasks', {
506
+ this.core.logOperation('findTasks', {
499
507
  message: `Starting global task search for "${taskName}"`,
500
508
  includeFullDetails,
501
509
  useSmartDisambiguation,
@@ -509,11 +517,11 @@ export class TaskServiceSearch extends TaskServiceCore {
509
517
  include_closed_lists: true,
510
518
  subtasks: true
511
519
  });
512
- if (!this.workspaceService) {
520
+ if (!this.core.workspaceService) {
513
521
  throw new Error("Workspace service required for global task lookup");
514
522
  }
515
523
  // Create an index to efficiently look up list context information
516
- const hierarchy = await this.workspaceService.getWorkspaceHierarchy();
524
+ const hierarchy = await this.core.workspaceService.getWorkspaceHierarchy();
517
525
  const listContextMap = new Map();
518
526
  // Function to recursively build list context map
519
527
  function buildListContextMap(nodes, spaceId, spaceName, folderId, folderName) {
@@ -551,7 +559,7 @@ export class TaskServiceSearch extends TaskServiceCore {
551
559
  let taskCount = 0;
552
560
  let matchesFound = 0;
553
561
  // Add additional logging to debug task matching
554
- this.logOperation('findTasks', {
562
+ this.core.logOperation('findTasks', {
555
563
  total_tasks_in_response: response.summaries.length,
556
564
  search_term: taskName,
557
565
  requireExactMatch
@@ -564,7 +572,7 @@ export class TaskServiceSearch extends TaskServiceCore {
564
572
  // For debugging, log every 20th task or any task with a similar name
565
573
  if (taskCount % 20 === 0 || taskSummary.name.toLowerCase().includes(taskName.toLowerCase()) ||
566
574
  taskName.toLowerCase().includes(taskSummary.name.toLowerCase())) {
567
- this.logOperation('findTasks:matching', {
575
+ this.core.logOperation('findTasks:matching', {
568
576
  task_name: taskSummary.name,
569
577
  search_term: taskName,
570
578
  list_name: taskSummary.list?.name || 'Unknown list',
@@ -589,7 +597,7 @@ export class TaskServiceSearch extends TaskServiceCore {
589
597
  }
590
598
  }
591
599
  }
592
- this.logOperation('findTasks', {
600
+ this.core.logOperation('findTasks', {
593
601
  globalSearch: true,
594
602
  searchTerm: taskName,
595
603
  tasksSearched: taskCount,
@@ -641,7 +649,7 @@ export class TaskServiceSearch extends TaskServiceCore {
641
649
  // If there's a single best match with score 80+, use it directly
642
650
  const exactMatches = initialMatches.filter(m => m.matchScore >= 80);
643
651
  if (exactMatches.length === 1 && !allowMultipleMatches) {
644
- this.logOperation('findTasks', {
652
+ this.core.logOperation('findTasks', {
645
653
  message: `Found single exact match with score ${exactMatches[0].matchScore}, prioritizing over other matches`,
646
654
  matchReason: exactMatches[0].matchReason
647
655
  });
@@ -668,7 +676,7 @@ export class TaskServiceSearch extends TaskServiceCore {
668
676
  return match.task;
669
677
  }
670
678
  // Otherwise, get the full details
671
- const fullTask = await this.getTask(exactMatches[0].id);
679
+ const fullTask = await this.core.getTask(exactMatches[0].id);
672
680
  if (includeListContext) {
673
681
  const match = exactMatches[0];
674
682
  // Enhance task with context information
@@ -696,7 +704,7 @@ export class TaskServiceSearch extends TaskServiceCore {
696
704
  try {
697
705
  // Process in sequence for better reliability
698
706
  for (const match of initialMatches) {
699
- const fullTask = await this.getTask(match.id);
707
+ const fullTask = await this.core.getTask(match.id);
700
708
  matchScoreMap.set(fullTask.id, match.matchScore);
701
709
  if (includeListContext) {
702
710
  // Enhance task with context information
@@ -734,7 +742,7 @@ export class TaskServiceSearch extends TaskServiceCore {
734
742
  }
735
743
  }
736
744
  catch (error) {
737
- this.logOperation('findTasks', {
745
+ this.core.logOperation('findTasks', {
738
746
  error: error.message,
739
747
  message: "Failed to get detailed task information"
740
748
  });
@@ -780,7 +788,7 @@ export class TaskServiceSearch extends TaskServiceCore {
780
788
  // After finding the task in global search, cache the mapping
781
789
  if (initialMatches.length === 1 || useSmartDisambiguation) {
782
790
  const bestMatch = fullMatches[0];
783
- this.cacheTaskNameToId(taskName, bestMatch.id, bestMatch.list?.id);
791
+ this.core.cacheTaskNameToId(taskName, bestMatch.id, bestMatch.list?.id);
784
792
  return bestMatch;
785
793
  }
786
794
  // Return results based on options
@@ -822,7 +830,7 @@ export class TaskServiceSearch extends TaskServiceCore {
822
830
  throw error;
823
831
  }
824
832
  // Unexpected errors
825
- throw this.handleError(error, `Error finding task: ${error.message}`);
833
+ throw this.core.handleError(error, `Error finding task: ${error.message}`);
826
834
  }
827
835
  }
828
836
  /**
@@ -833,16 +841,16 @@ export class TaskServiceSearch extends TaskServiceCore {
833
841
  * @returns The updated task
834
842
  */
835
843
  async updateTaskByName(listId, taskName, updateData) {
836
- this.logOperation('updateTaskByName', { listId, taskName, ...updateData });
844
+ this.core.logOperation('updateTaskByName', { listId, taskName, ...updateData });
837
845
  try {
838
846
  const task = await this.findTaskByName(listId, taskName);
839
847
  if (!task) {
840
848
  throw new Error(`Task "${taskName}" not found in list ${listId}`);
841
849
  }
842
- return await this.updateTask(task.id, updateData);
850
+ return await this.core.updateTask(task.id, updateData);
843
851
  }
844
852
  catch (error) {
845
- throw this.handleError(error, `Failed to update task by name: ${error instanceof Error ? error.message : String(error)}`);
853
+ throw this.core.handleError(error, `Failed to update task by name: ${error instanceof Error ? error.message : String(error)}`);
846
854
  }
847
855
  }
848
856
  /**
@@ -854,7 +862,7 @@ export class TaskServiceSearch extends TaskServiceCore {
854
862
  * @returns The best matching task or null if no match found
855
863
  */
856
864
  async findTaskByNameGlobally(taskName) {
857
- this.logOperation('findTaskByNameGlobally', { taskName });
865
+ this.core.logOperation('findTaskByNameGlobally', { taskName });
858
866
  // Use a static cache for task data to avoid redundant API calls
859
867
  // This dramatically reduces API usage across multiple task lookups
860
868
  if (!this.constructor.hasOwnProperty('_taskCache')) {
@@ -873,7 +881,7 @@ export class TaskServiceSearch extends TaskServiceCore {
873
881
  // Use cached tasks if available and not expired
874
882
  let tasks = [];
875
883
  if (cache.tasks.length > 0 && (now - cache.lastFetch) < cache.cacheTTL) {
876
- this.logOperation('findTaskByNameGlobally', {
884
+ this.core.logOperation('findTaskByNameGlobally', {
877
885
  usedCache: true,
878
886
  cacheAge: now - cache.lastFetch,
879
887
  taskCount: cache.tasks.length
@@ -890,7 +898,7 @@ export class TaskServiceSearch extends TaskServiceCore {
890
898
  // Update cache
891
899
  cache.tasks = tasks;
892
900
  cache.lastFetch = now;
893
- this.logOperation('findTaskByNameGlobally', {
901
+ this.core.logOperation('findTaskByNameGlobally', {
894
902
  usedCache: false,
895
903
  fetchedTaskCount: tasks.length
896
904
  });
@@ -904,7 +912,7 @@ export class TaskServiceSearch extends TaskServiceCore {
904
912
  updatedAt: task.date_updated ? parseInt(task.date_updated, 10) : 0
905
913
  };
906
914
  }).filter(result => result.matchResult.isMatch);
907
- this.logOperation('findTaskByNameGlobally', {
915
+ this.core.logOperation('findTaskByNameGlobally', {
908
916
  taskCount: tasks.length,
909
917
  matchCount: taskMatches.length,
910
918
  taskName
@@ -939,15 +947,15 @@ export class TaskServiceSearch extends TaskServiceCore {
939
947
  updatedAt: match.updatedAt,
940
948
  list: match.task.list?.name || 'Unknown list'
941
949
  }));
942
- this.logOperation('findTaskByNameGlobally', { topMatches });
950
+ this.core.logOperation('findTaskByNameGlobally', { topMatches });
943
951
  // Return the best match
944
952
  return bestMatches[0].task;
945
953
  }
946
954
  catch (error) {
947
- this.logOperation('findTaskByNameGlobally', { error: error.message });
955
+ this.core.logOperation('findTaskByNameGlobally', { error: error.message });
948
956
  // If there's an error (like rate limit), try to use cached data even if expired
949
957
  if (cache.tasks.length > 0) {
950
- this.logOperation('findTaskByNameGlobally', {
958
+ this.core.logOperation('findTaskByNameGlobally', {
951
959
  message: 'Using expired cache due to API error',
952
960
  cacheAge: now - cache.lastFetch
953
961
  });
@@ -4,22 +4,101 @@
4
4
  *
5
5
  * ClickUp Task Service
6
6
  *
7
- * Main entry point for the ClickUp Task Service.
8
- * Combines all task-related functionality through inheritance:
9
- * - Core operations (CRUD)
10
- * - Search capabilities
11
- * - File attachments
12
- * - Comments
13
- * - Tags
14
- * - Custom fields
7
+ * Complete task service combining all task-related functionality
8
+ *
9
+ * REFACTORED: Now uses composition instead of linear inheritance.
10
+ * Extends TaskServiceCore and composes other services as properties.
15
11
  */
12
+ import { TaskServiceCore } from './task-core.js';
13
+ import { TaskServiceSearch } from './task-search.js';
14
+ import { TaskServiceAttachments } from './task-attachments.js';
15
+ import { TaskServiceComments } from './task-comments.js';
16
+ import { TaskServiceTags } from './task-tags.js';
16
17
  import { TaskServiceCustomFields } from './task-custom-fields.js';
17
18
  /**
18
19
  * Complete TaskService combining all task-related functionality
20
+ *
21
+ * This service uses composition to provide access to all task operations
22
+ * while maintaining clean separation of concerns and eliminating artificial
23
+ * dependencies between service modules.
19
24
  */
20
- export class TaskService extends TaskServiceCustomFields {
25
+ export class TaskService extends TaskServiceCore {
21
26
  constructor(apiKey, teamId, baseUrl, workspaceService) {
22
27
  super(apiKey, teamId, baseUrl, workspaceService);
23
28
  this.logOperation('constructor', { initialized: true });
29
+ // Initialize composed services with core as dependency
30
+ this.search = new TaskServiceSearch(this);
31
+ this.attachments = new TaskServiceAttachments(this);
32
+ this.comments = new TaskServiceComments(this);
33
+ this.tags = new TaskServiceTags(this);
34
+ this.customFields = new TaskServiceCustomFields(this);
35
+ }
36
+ // ===== DELEGATED SEARCH METHODS =====
37
+ async findTaskByName(listId, taskName) {
38
+ return this.search.findTaskByName(listId, taskName);
39
+ }
40
+ async getWorkspaceTasks(filters = {}) {
41
+ return this.search.getWorkspaceTasks(filters);
42
+ }
43
+ async getTaskSummaries(filters = {}) {
44
+ return this.search.getTaskSummaries(filters);
45
+ }
46
+ async getListViews(listId) {
47
+ return this.search.getListViews(listId);
48
+ }
49
+ async getTasksFromView(viewId, filters = {}) {
50
+ return this.search.getTasksFromView(viewId, filters);
51
+ }
52
+ async getTaskDetails(filters = {}) {
53
+ return this.search.getTaskDetails(filters);
54
+ }
55
+ async updateTaskByName(listId, taskName, updateData) {
56
+ return this.search.updateTaskByName(listId, taskName, updateData);
57
+ }
58
+ async findTaskByNameGlobally(taskName) {
59
+ return this.search.findTaskByNameGlobally(taskName);
60
+ }
61
+ async findTasks(params) {
62
+ return this.search.findTasks(params);
63
+ }
64
+ // ===== DELEGATED ATTACHMENT METHODS =====
65
+ async uploadTaskAttachment(taskId, fileData, fileName) {
66
+ return this.attachments.uploadTaskAttachment(taskId, fileData, fileName);
67
+ }
68
+ async uploadTaskAttachmentFromUrl(taskId, fileUrl, fileName, authHeader) {
69
+ return this.attachments.uploadTaskAttachmentFromUrl(taskId, fileUrl, fileName, authHeader);
70
+ }
71
+ // ===== DELEGATED COMMENT METHODS =====
72
+ async getTaskComments(taskId, start, startId) {
73
+ return this.comments.getTaskComments(taskId, start, startId);
74
+ }
75
+ async createTaskComment(taskId, commentText, notifyAll, assignee) {
76
+ return this.comments.createTaskComment(taskId, commentText, notifyAll, assignee);
77
+ }
78
+ // ===== DELEGATED TAG METHODS =====
79
+ async addTagToTask(taskId, tagName) {
80
+ return this.tags.addTagToTask(taskId, tagName);
81
+ }
82
+ async removeTagFromTask(taskId, tagName) {
83
+ return this.tags.removeTagFromTask(taskId, tagName);
84
+ }
85
+ async getTaskTags(taskId) {
86
+ return this.tags.getTaskTags(taskId);
87
+ }
88
+ async updateTaskTags(taskId, tagNames) {
89
+ return this.tags.updateTaskTags(taskId, tagNames);
90
+ }
91
+ // ===== DELEGATED CUSTOM FIELD METHODS =====
92
+ async setCustomFieldValue(taskId, fieldId, value) {
93
+ return this.customFields.setCustomFieldValue(taskId, fieldId, value);
94
+ }
95
+ async setCustomFieldValues(taskId, customFields) {
96
+ return this.customFields.setCustomFieldValues(taskId, customFields);
97
+ }
98
+ async getCustomFieldValues(taskId) {
99
+ return this.customFields.getCustomFieldValues(taskId);
100
+ }
101
+ async getCustomFieldValue(taskId, fieldId) {
102
+ return this.customFields.getCustomFieldValue(taskId, fieldId);
24
103
  }
25
104
  }