@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.
- package/LICENSE +9 -17
- package/README.md +33 -38
- package/build/enhanced_server.js +262 -0
- package/build/index.js +9 -3
- package/build/license.js +172 -0
- package/build/middleware/auth.js +211 -0
- package/build/routes/auth.js +306 -0
- package/build/schemas/member.js +13 -0
- package/build/server.js +15 -1
- package/build/server.log +15 -0
- package/build/services/auth/oauth2.js +236 -0
- package/build/services/auth/session.js +337 -0
- package/build/services/clickup/adapter.js +281 -0
- package/build/services/clickup/factory.js +339 -0
- package/build/services/clickup/task/task-attachments.js +20 -12
- package/build/services/clickup/task/task-comments.js +19 -9
- package/build/services/clickup/task/task-core.js +68 -4
- package/build/services/clickup/task/task-custom-fields.js +23 -13
- package/build/services/clickup/task/task-search.js +79 -71
- package/build/services/clickup/task/task-service.js +88 -9
- package/build/services/clickup/task/task-tags.js +25 -13
- package/build/sse_server.js +4 -4
- package/build/tools/documents.js +11 -4
- package/build/tools/health.js +23 -0
- package/build/tools/member.js +2 -4
- package/build/tools/task/bulk-operations.js +5 -5
- package/build/tools/task/handlers.js +62 -12
- package/build/tools/task/single-operations.js +9 -9
- package/build/tools/task/time-tracking.js +61 -170
- package/build/tools/task/utilities.js +56 -22
- package/build/utils/date-utils.js +341 -141
- package/build/utils/schema-compatibility.js +222 -0
- package/build/utils/universal-schema-compatibility.js +171 -0
- package/build/virtual-sdk/generator.js +53 -0
- package/build/virtual-sdk/registry.js +45 -0
- 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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
}
|