@taazkareem/clickup-mcp-server 0.6.4 → 0.6.6

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.
@@ -23,6 +23,8 @@ export class TaskServiceCore extends BaseClickUpService {
23
23
  tasks: new Map(),
24
24
  lists: new Map()
25
25
  };
26
+ // Cache for task name to ID mapping
27
+ this.nameToIdCache = new Map();
26
28
  // Cache TTL in milliseconds (5 minutes)
27
29
  this.CACHE_TTL = 5 * 60 * 1000;
28
30
  if (workspaceService) {
@@ -155,24 +157,40 @@ export class TaskServiceCore extends BaseClickUpService {
155
157
  try {
156
158
  return await this.makeRequest(async () => {
157
159
  const response = await this.client.post(`/list/${listId}/task`, taskData);
158
- return response.data;
160
+ // Handle both JSON and text responses
161
+ const data = response.data;
162
+ if (typeof data === 'string') {
163
+ // If we got a text response, try to extract task ID from common patterns
164
+ const idMatch = data.match(/task.*?(\w{9})/i);
165
+ if (idMatch) {
166
+ // If we found an ID, fetch the full task details
167
+ return await this.getTask(idMatch[1]);
168
+ }
169
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
170
+ }
171
+ return data;
159
172
  });
160
173
  }
161
174
  catch (error) {
162
- throw this.handleError(error, `Failed to create task in list ${listId}`);
175
+ throw this.handleError(error, 'Failed to create task');
163
176
  }
164
177
  }
165
178
  /**
166
- * Get a specific task by ID
179
+ * Get a task by its ID
167
180
  * @param taskId The ID of the task to retrieve
168
- * @returns The task details
181
+ * @returns The task
169
182
  */
170
183
  async getTask(taskId) {
171
184
  this.logOperation('getTask', { taskId });
172
185
  try {
173
186
  return await this.makeRequest(async () => {
174
187
  const response = await this.client.get(`/task/${taskId}`);
175
- return response.data;
188
+ // Handle both JSON and text responses
189
+ const data = response.data;
190
+ if (typeof data === 'string') {
191
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
192
+ }
193
+ return data;
176
194
  });
177
195
  }
178
196
  catch (error) {
@@ -180,22 +198,27 @@ export class TaskServiceCore extends BaseClickUpService {
180
198
  }
181
199
  }
182
200
  /**
183
- * Get all tasks in a list with optional filtering
201
+ * Get all tasks in a list
184
202
  * @param listId The ID of the list to get tasks from
185
203
  * @param filters Optional filters to apply
186
- * @returns List of tasks matching the filters
204
+ * @returns Array of tasks
187
205
  */
188
206
  async getTasks(listId, filters = {}) {
189
207
  this.logOperation('getTasks', { listId, filters });
190
208
  try {
191
209
  return await this.makeRequest(async () => {
192
210
  const params = this.buildTaskFilterParams(filters);
193
- const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
194
- return response.data.tasks;
211
+ const response = await this.client.get(`/list/${listId}/task`, { params });
212
+ // Handle both JSON and text responses
213
+ const data = response.data;
214
+ if (typeof data === 'string') {
215
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
216
+ }
217
+ return Array.isArray(data) ? data : data.tasks || [];
195
218
  });
196
219
  }
197
220
  catch (error) {
198
- throw this.handleError(error, `Failed to get tasks from list ${listId}`);
221
+ throw this.handleError(error, `Failed to get tasks for list ${listId}`);
199
222
  }
200
223
  }
201
224
  /**
@@ -247,7 +270,18 @@ export class TaskServiceCore extends BaseClickUpService {
247
270
  try {
248
271
  return await this.makeRequest(async () => {
249
272
  const response = await this.client.put(`/task/${taskId}`, updateData);
250
- return response.data;
273
+ // Handle both JSON and text responses
274
+ const data = response.data;
275
+ if (typeof data === 'string') {
276
+ // If we got a text response, try to extract task ID from common patterns
277
+ const idMatch = data.match(/task.*?(\w{9})/i);
278
+ if (idMatch) {
279
+ // If we found an ID, fetch the full task details
280
+ return await this.getTask(idMatch[1]);
281
+ }
282
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
283
+ }
284
+ return data;
251
285
  });
252
286
  }
253
287
  catch (error) {
@@ -436,4 +470,35 @@ export class TaskServiceCore extends BaseClickUpService {
436
470
  throw error;
437
471
  }
438
472
  }
473
+ /**
474
+ * Try to get a task ID from the name cache
475
+ * @param taskName The name of the task
476
+ * @param listId Optional list ID for context
477
+ * @returns The cached task ID if found and not expired, otherwise null
478
+ */
479
+ getCachedTaskId(taskName, listId) {
480
+ const cached = this.nameToIdCache.get(taskName);
481
+ if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
482
+ // If listId is provided, ensure it matches the cached context
483
+ if (!listId || cached.listId === listId) {
484
+ this.logger.debug('Using cached task ID for name', { taskName, cachedId: cached.taskId });
485
+ return cached.taskId;
486
+ }
487
+ }
488
+ return null;
489
+ }
490
+ /**
491
+ * Cache a task name to ID mapping
492
+ * @param taskName The name of the task
493
+ * @param taskId The ID of the task
494
+ * @param listId Optional list ID for context
495
+ */
496
+ cacheTaskNameToId(taskName, taskId, listId) {
497
+ this.nameToIdCache.set(taskName, {
498
+ taskId,
499
+ validatedAt: Date.now(),
500
+ listId
501
+ });
502
+ this.logger.debug('Cached task name to ID mapping', { taskName, taskId, listId });
503
+ }
439
504
  }
@@ -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
  }