@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.
@@ -12,10 +12,40 @@ import { clickUpServices } from '../../services/shared.js';
12
12
  import { BulkService } from '../../services/clickup/bulk.js';
13
13
  import { parseDueDate } from '../utils.js';
14
14
  import { validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveListIdWithValidation } from './utilities.js';
15
+ import { workspaceService } from '../../services/shared.js';
16
+ import { isNameMatch } from '../../utils/resolver-utils.js';
17
+ import { Logger } from '../../logger.js';
15
18
  // Use shared services instance
16
19
  const { task: taskService, list: listService } = clickUpServices;
17
20
  // Create a bulk service instance that uses the task service
18
21
  const bulkService = new BulkService(taskService);
22
+ // Create a logger instance for task handlers
23
+ const logger = new Logger('TaskHandlers');
24
+ // Cache for task context between sequential operations
25
+ const taskContextCache = new Map();
26
+ const TASK_CONTEXT_TTL = 5 * 60 * 1000; // 5 minutes
27
+ /**
28
+ * Store task context for sequential operations
29
+ */
30
+ function storeTaskContext(taskName, taskId) {
31
+ taskContextCache.set(taskName, {
32
+ id: taskId,
33
+ timestamp: Date.now()
34
+ });
35
+ }
36
+ /**
37
+ * Get cached task context if valid
38
+ */
39
+ function getCachedTaskContext(taskName) {
40
+ const context = taskContextCache.get(taskName);
41
+ if (!context)
42
+ return null;
43
+ if (Date.now() - context.timestamp > TASK_CONTEXT_TTL) {
44
+ taskContextCache.delete(taskName);
45
+ return null;
46
+ }
47
+ return context.id;
48
+ }
19
49
  //=============================================================================
20
50
  // SHARED UTILITY FUNCTIONS
21
51
  //=============================================================================
@@ -50,31 +80,212 @@ function buildUpdateData(params) {
50
80
  return updateData;
51
81
  }
52
82
  /**
53
- * Resolves a task ID from various input formats
54
- * Smart disambiguation is used for task name lookups
55
- *
56
- * @param taskId Direct task ID
57
- * @param taskName Task name to search for
58
- * @param listName List name for context
59
- * @param customTaskId Custom task ID (prefixed format)
60
- * @returns Resolved task ID
83
+ * Core function to find a task by ID or name
84
+ * This consolidates all task lookup logic in one place for consistency
85
+ */
86
+ async function findTask(params) {
87
+ const { taskId, taskName, listName, customTaskId, requireId = false, includeSubtasks = false } = params;
88
+ // Validate that we have enough information to identify a task
89
+ const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { requireTaskId: requireId, useGlobalLookup: true });
90
+ if (!validationResult.isValid) {
91
+ throw new Error(validationResult.errorMessage);
92
+ }
93
+ try {
94
+ // Direct path for taskId - most efficient
95
+ if (taskId) {
96
+ const task = await taskService.getTask(taskId);
97
+ // Add subtasks if requested
98
+ if (includeSubtasks) {
99
+ const subtasks = await taskService.getSubtasks(task.id);
100
+ return { task, subtasks };
101
+ }
102
+ return { task };
103
+ }
104
+ // Direct path for customTaskId - also efficient
105
+ if (customTaskId) {
106
+ const task = await taskService.getTaskByCustomId(customTaskId);
107
+ // Add subtasks if requested
108
+ if (includeSubtasks) {
109
+ const subtasks = await taskService.getSubtasks(task.id);
110
+ return { task, subtasks };
111
+ }
112
+ return { task };
113
+ }
114
+ // Special optimized path for taskName + listName combination
115
+ if (taskName && listName) {
116
+ const listId = await resolveListIdWithValidation(null, listName);
117
+ // Get all tasks in the list
118
+ const allTasks = await taskService.getTasks(listId);
119
+ // Find the task that matches the name
120
+ const matchingTask = findTaskByName(allTasks, taskName);
121
+ if (!matchingTask) {
122
+ throw new Error(`Task "${taskName}" not found in list "${listName}"`);
123
+ }
124
+ // Add subtasks if requested
125
+ if (includeSubtasks) {
126
+ const subtasks = await taskService.getSubtasks(matchingTask.id);
127
+ return { task: matchingTask, subtasks };
128
+ }
129
+ return { task: matchingTask };
130
+ }
131
+ // Fallback to searching all lists for taskName-only case
132
+ if (taskName) {
133
+ logger.debug(`Searching all lists for task: "${taskName}"`);
134
+ // Get workspace hierarchy which contains all lists
135
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
136
+ // Extract all list IDs from the hierarchy
137
+ const listIds = [];
138
+ const extractListIds = (node) => {
139
+ if (node.type === 'list') {
140
+ listIds.push(node.id);
141
+ }
142
+ if (node.children) {
143
+ node.children.forEach(extractListIds);
144
+ }
145
+ };
146
+ // Start from the root's children
147
+ hierarchy.root.children.forEach(extractListIds);
148
+ // Search through each list
149
+ const searchPromises = listIds.map(async (listId) => {
150
+ try {
151
+ const tasks = await taskService.getTasks(listId);
152
+ const matchingTask = findTaskByName(tasks, taskName);
153
+ if (matchingTask) {
154
+ logger.debug(`Found task "${matchingTask.name}" (ID: ${matchingTask.id}) in list with ID "${listId}"`);
155
+ return matchingTask;
156
+ }
157
+ return null;
158
+ }
159
+ catch (error) {
160
+ logger.warn(`Error searching list ${listId}: ${error.message}`);
161
+ return null;
162
+ }
163
+ });
164
+ // Wait for all searches to complete
165
+ const results = await Promise.all(searchPromises);
166
+ // Filter out null results and sort by match quality and recency
167
+ const matchingTasks = results
168
+ .filter(task => task !== null)
169
+ .sort((a, b) => {
170
+ const aMatch = isNameMatch(a.name, taskName);
171
+ const bMatch = isNameMatch(b.name, taskName);
172
+ // First sort by match quality
173
+ if (bMatch.score !== aMatch.score) {
174
+ return bMatch.score - aMatch.score;
175
+ }
176
+ // Then sort by recency
177
+ return parseInt(b.date_updated) - parseInt(a.date_updated);
178
+ });
179
+ if (matchingTasks.length === 0) {
180
+ throw new Error(`Task "${taskName}" not found in any list across your workspace. Please check the task name and try again.`);
181
+ }
182
+ const bestMatch = matchingTasks[0];
183
+ // Add subtasks if requested
184
+ if (includeSubtasks) {
185
+ const subtasks = await taskService.getSubtasks(bestMatch.id);
186
+ return { task: bestMatch, subtasks };
187
+ }
188
+ return { task: bestMatch };
189
+ }
190
+ // We shouldn't reach here if validation is working correctly
191
+ throw new Error("No valid task identification provided");
192
+ }
193
+ catch (error) {
194
+ // Enhance error message for non-existent tasks
195
+ if (taskName && error.message.includes('not found')) {
196
+ throw new Error(`Task "${taskName}" not found. Please check the task name and try again.`);
197
+ }
198
+ // Pass along other formatted errors
199
+ throw error;
200
+ }
201
+ }
202
+ /**
203
+ * Helper function to find a task by name in an array of tasks
204
+ */
205
+ function findTaskByName(tasks, name) {
206
+ if (!tasks || !Array.isArray(tasks) || !name)
207
+ return null;
208
+ const normalizedSearchName = name.toLowerCase().trim();
209
+ // Get match scores for all tasks
210
+ const taskMatchScores = tasks.map(task => {
211
+ const matchResult = isNameMatch(task.name, name);
212
+ return {
213
+ task,
214
+ matchResult,
215
+ // Parse the date_updated field as a number for sorting
216
+ updatedAt: task.date_updated ? parseInt(task.date_updated, 10) : 0
217
+ };
218
+ }).filter(result => result.matchResult.isMatch);
219
+ if (taskMatchScores.length === 0) {
220
+ return null;
221
+ }
222
+ // First, try to find exact matches
223
+ const exactMatches = taskMatchScores
224
+ .filter(result => result.matchResult.exactMatch)
225
+ .sort((a, b) => {
226
+ // For exact matches with the same score, sort by most recently updated
227
+ if (b.matchResult.score === a.matchResult.score) {
228
+ return b.updatedAt - a.updatedAt;
229
+ }
230
+ return b.matchResult.score - a.matchResult.score;
231
+ });
232
+ // Get the best matches based on whether we have exact matches or need to fall back to fuzzy matches
233
+ const bestMatches = exactMatches.length > 0 ? exactMatches : taskMatchScores.sort((a, b) => {
234
+ // First sort by match score (highest first)
235
+ if (b.matchResult.score !== a.matchResult.score) {
236
+ return b.matchResult.score - a.matchResult.score;
237
+ }
238
+ // Then sort by most recently updated
239
+ return b.updatedAt - a.updatedAt;
240
+ });
241
+ // Get the best match
242
+ return bestMatches[0].task;
243
+ }
244
+ /**
245
+ * Handler for getting a task - uses the consolidated findTask function
246
+ */
247
+ export async function getTaskHandler(params) {
248
+ try {
249
+ const result = await findTask({
250
+ taskId: params.taskId,
251
+ taskName: params.taskName,
252
+ listName: params.listName,
253
+ customTaskId: params.customTaskId,
254
+ includeSubtasks: params.subtasks
255
+ });
256
+ if (result.subtasks) {
257
+ return { ...result.task, subtasks: result.subtasks };
258
+ }
259
+ return result.task;
260
+ }
261
+ catch (error) {
262
+ throw error;
263
+ }
264
+ }
265
+ /**
266
+ * Get task ID from various identifiers - uses the consolidated findTask function
61
267
  */
62
- export async function getTaskId(taskId, taskName, listName, customTaskId) {
63
- validateTaskIdentification(taskId, taskName, listName, customTaskId, true);
64
- const result = await taskService.findTasks({
268
+ export async function getTaskId(taskId, taskName, listName, customTaskId, requireId, includeSubtasks) {
269
+ // Check task context cache first if we have a task name
270
+ if (taskName && !taskId && !customTaskId) {
271
+ const cachedId = getCachedTaskContext(taskName);
272
+ if (cachedId) {
273
+ return cachedId;
274
+ }
275
+ }
276
+ const result = await findTask({
65
277
  taskId,
66
- customTaskId,
67
278
  taskName,
68
279
  listName,
69
- allowMultipleMatches: false,
70
- useSmartDisambiguation: true,
71
- includeFullDetails: false,
72
- includeListContext: false
280
+ customTaskId,
281
+ requireId,
282
+ includeSubtasks
73
283
  });
74
- if (result && !Array.isArray(result)) {
75
- return result.id;
284
+ // Store task context for future operations
285
+ if (taskName && result.task.id) {
286
+ storeTaskContext(taskName, result.task.id);
76
287
  }
77
- throw new Error("Task not found");
288
+ return result.task.id;
78
289
  }
79
290
  /**
80
291
  * Process a list identification validation, returning the list ID
@@ -107,7 +318,10 @@ function buildTaskFilters(params) {
107
318
  */
108
319
  async function mapTaskIds(tasks) {
109
320
  return Promise.all(tasks.map(async (task) => {
110
- validateTaskIdentification(task.taskId, task.taskName, task.listName, task.customTaskId);
321
+ const validationResult = validateTaskIdentification({ taskId: task.taskId, taskName: task.taskName, listName: task.listName, customTaskId: task.customTaskId }, { useGlobalLookup: true });
322
+ if (!validationResult.isValid) {
323
+ throw new Error(validationResult.errorMessage);
324
+ }
111
325
  return await getTaskId(task.taskId, task.taskName, task.listName, task.customTaskId);
112
326
  }));
113
327
  }
@@ -150,20 +364,29 @@ export async function createTaskHandler(params) {
150
364
  /**
151
365
  * Handler for updating a task
152
366
  */
153
- export async function updateTaskHandler(params) {
154
- console.log('Update Task Handler - Raw params:', JSON.stringify(params));
155
- console.log('Update Task Handler - Priority type:', typeof params.priority, 'Value:', params.priority);
156
- validateTaskUpdateData(params);
157
- const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
158
- const updateData = buildUpdateData(params);
159
- console.log('Update Task Handler - Update data:', JSON.stringify(updateData));
160
- return await taskService.updateTask(taskId, updateData);
367
+ export async function updateTaskHandler(taskService, params) {
368
+ const { taskId, taskName, listName, customTaskId, ...updateData } = params;
369
+ // Validate task identification with global lookup enabled
370
+ const validationResult = validateTaskIdentification(params, { useGlobalLookup: true });
371
+ if (!validationResult.isValid) {
372
+ throw new Error(validationResult.errorMessage);
373
+ }
374
+ // Validate update data
375
+ validateTaskUpdateData(updateData);
376
+ try {
377
+ // Get the task ID using global lookup
378
+ const id = await getTaskId(taskId, taskName, listName, customTaskId);
379
+ return await taskService.updateTask(id, updateData);
380
+ }
381
+ catch (error) {
382
+ throw new Error(`Failed to update task: ${error instanceof Error ? error.message : String(error)}`);
383
+ }
161
384
  }
162
385
  /**
163
386
  * Handler for moving a task
164
387
  */
165
388
  export async function moveTaskHandler(params) {
166
- const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
389
+ const taskId = await getTaskId(params.taskId, params.taskName, undefined, params.customTaskId, false);
167
390
  const listId = await getListId(params.listId, params.listName);
168
391
  return await taskService.moveTask(taskId, listId);
169
392
  }
@@ -171,119 +394,13 @@ export async function moveTaskHandler(params) {
171
394
  * Handler for duplicating a task
172
395
  */
173
396
  export async function duplicateTaskHandler(params) {
174
- const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
397
+ const taskId = await getTaskId(params.taskId, params.taskName, undefined, params.customTaskId, false);
175
398
  let listId;
176
399
  if (params.listId || params.listName) {
177
400
  listId = await getListId(params.listId, params.listName);
178
401
  }
179
402
  return await taskService.duplicateTask(taskId, listId);
180
403
  }
181
- /**
182
- * Handler for getting a task
183
- */
184
- export async function getTaskHandler(params) {
185
- try {
186
- // Direct path for taskId - most efficient
187
- if (params.taskId) {
188
- const task = await taskService.getTask(params.taskId);
189
- // Add subtasks if requested
190
- if (params.subtasks) {
191
- const subtasks = await taskService.getSubtasks(task.id);
192
- return { ...task, subtasks };
193
- }
194
- return task;
195
- }
196
- // Direct path for customTaskId - also efficient
197
- if (params.customTaskId) {
198
- const task = await taskService.getTaskByCustomId(params.customTaskId);
199
- // Add subtasks if requested
200
- if (params.subtasks) {
201
- const subtasks = await taskService.getSubtasks(task.id);
202
- return { ...task, subtasks };
203
- }
204
- return task;
205
- }
206
- // Special optimized path for taskName + listName combination
207
- if (params.taskName && params.listName) {
208
- // First, get the list ID
209
- const listId = await getListId(null, params.listName);
210
- if (!listId) {
211
- throw new Error(`List "${params.listName}" not found`);
212
- }
213
- // Use the ClickUp API to get filtered tasks
214
- // Need to get all tasks and filter on client side
215
- // This is more efficient than the original approach because it's a dedicated path
216
- // that skips the global lookup framework entirely
217
- const allTasks = await taskService.getTasks(listId);
218
- // Find the matching task
219
- // Extract this to avoid dependency on internal isNameMatch implementation
220
- const matchingTask = findTaskByName(allTasks, params.taskName);
221
- if (!matchingTask) {
222
- throw new Error(`Task "${params.taskName}" not found in list "${params.listName}"`);
223
- }
224
- // Add subtasks if requested
225
- if (params.subtasks) {
226
- const subtasks = await taskService.getSubtasks(matchingTask.id);
227
- return { ...matchingTask, subtasks };
228
- }
229
- return matchingTask;
230
- }
231
- // Fallback to the original global lookup for all other cases
232
- const result = await taskService.findTasks({
233
- taskName: params.taskName,
234
- allowMultipleMatches: true,
235
- useSmartDisambiguation: false,
236
- includeFullDetails: true,
237
- includeListContext: true
238
- });
239
- // Handle the response based on the result type
240
- if (Array.isArray(result)) {
241
- // If multiple tasks matched, format them with task count
242
- return {
243
- matches: result,
244
- count: result.length
245
- };
246
- }
247
- else if (result) {
248
- // Single task found, check if we need to include subtasks
249
- if (params.subtasks) {
250
- const subtasks = await taskService.getSubtasks(result.id);
251
- return { ...result, subtasks };
252
- }
253
- // Return the single task
254
- return result;
255
- }
256
- else {
257
- throw new Error("Task not found");
258
- }
259
- }
260
- catch (error) {
261
- // Enhance error message for non-existent tasks
262
- if (params.taskName && error.message.includes('not found')) {
263
- throw new Error(`Task "${params.taskName}" not found. Please check the task name and try again.`);
264
- }
265
- // Pass along other formatted errors
266
- throw error;
267
- }
268
- }
269
- /**
270
- * Helper function to find a task by name in an array of tasks
271
- */
272
- function findTaskByName(tasks, name) {
273
- if (!tasks || !Array.isArray(tasks) || !name)
274
- return null;
275
- // Try exact match first
276
- let match = tasks.find(task => task.name === name);
277
- if (match)
278
- return match;
279
- // Try case-insensitive match
280
- match = tasks.find(task => task.name.toLowerCase() === name.toLowerCase());
281
- if (match)
282
- return match;
283
- // Try fuzzy match - looking for name as substring
284
- match = tasks.find(task => task.name.toLowerCase().includes(name.toLowerCase()));
285
- return match || null;
286
- }
287
404
  /**
288
405
  * Handler for getting tasks
289
406
  */
@@ -13,7 +13,7 @@ import { createTaskTool, getTaskTool, getTasksTool, updateTaskTool, moveTaskTool
13
13
  import { createBulkTasksTool, updateBulkTasksTool, moveBulkTasksTool, deleteBulkTasksTool } from './bulk-operations.js';
14
14
  import { getWorkspaceTasksTool } from './workspace-operations.js';
15
15
  // Import handlers
16
- import { createTaskHandler, getTaskHandler, getTasksHandler, updateTaskHandler, moveTaskHandler, duplicateTaskHandler, deleteTaskHandler, getTaskCommentsHandler, createTaskCommentHandler, createBulkTasksHandler, updateBulkTasksHandler, moveBulkTasksHandler, deleteBulkTasksHandler, getWorkspaceTasksHandler } from './handlers.js';
16
+ import { createTaskHandler, getTaskHandler, getTasksHandler, updateTaskHandler, moveTaskHandler, duplicateTaskHandler, deleteTaskHandler, getTaskCommentsHandler, createTaskCommentHandler, createBulkTasksHandler, updateBulkTasksHandler, moveBulkTasksHandler, deleteBulkTasksHandler, getWorkspaceTasksHandler, formatTaskData } from './index.js';
17
17
  // Import shared services
18
18
  import { clickUpServices } from '../../services/shared.js';
19
19
  const { task: taskService } = clickUpServices;
@@ -43,42 +43,18 @@ export const handleGetTasks = createHandlerWrapper(getTasksHandler, (tasks) => (
43
43
  tasks,
44
44
  count: tasks.length
45
45
  }));
46
- export const handleUpdateTask = async (parameters) => {
46
+ /**
47
+ * Handle task update operation
48
+ */
49
+ export async function handleUpdateTask(parameters) {
47
50
  try {
48
- // Special handling for priority parameter
49
- if (parameters.priority !== undefined) {
50
- // Ensure priority is converted to a number if it's a valid value
51
- if (parameters.priority === null) {
52
- // null is valid for clearing priority
53
- }
54
- else if (typeof parameters.priority === 'number' && [1, 2, 3, 4].includes(parameters.priority)) {
55
- // Valid priority number, keep as is
56
- }
57
- else if (typeof parameters.priority === 'string') {
58
- // Try to convert string to number
59
- const numPriority = parseInt(parameters.priority, 10);
60
- if (!isNaN(numPriority) && [1, 2, 3, 4].includes(numPriority)) {
61
- parameters.priority = numPriority;
62
- }
63
- else if (parameters.priority === 'null') {
64
- parameters.priority = null;
65
- }
66
- else {
67
- throw new Error(`Invalid priority value: ${parameters.priority}. Must be 1, 2, 3, 4, or null.`);
68
- }
69
- }
70
- else {
71
- throw new Error(`Invalid priority value: ${parameters.priority}. Must be 1, 2, 3, 4, or null.`);
72
- }
73
- }
74
- // Proceed with normal handling
75
- const result = await updateTaskHandler(parameters);
76
- return sponsorService.createResponse(result, true);
51
+ const result = await updateTaskHandler(taskService, parameters);
52
+ return sponsorService.createResponse(formatTaskData(result), true);
77
53
  }
78
54
  catch (error) {
79
- return sponsorService.createErrorResponse(error, parameters);
55
+ return sponsorService.createErrorResponse(error instanceof Error ? error.message : String(error));
80
56
  }
81
- };
57
+ }
82
58
  export const handleMoveTask = createHandlerWrapper(moveTaskHandler);
83
59
  export const handleDuplicateTask = createHandlerWrapper(duplicateTaskHandler);
84
60
  export const handleDeleteTask = createHandlerWrapper(deleteTaskHandler, () => ({
@@ -329,15 +329,17 @@ export const getTaskTool = {
329
329
 
330
330
  Valid Usage:
331
331
  1. Use taskId alone (preferred) - works with both regular and custom IDs (like "DEV-1234")
332
- 2. Use taskName + listName
333
- 3. Use customTaskId for explicit custom ID lookup
332
+ 2. Use taskName alone (will search across all lists in the workspace)
333
+ 3. Use taskName + listName (for faster, targeted search)
334
+ 4. Use customTaskId for explicit custom ID lookup
334
335
 
335
336
  Requirements:
336
- - When using taskName, listName is REQUIRED
337
- - When using customTaskId, listName is recommended for faster lookup
337
+ - EITHER taskId OR taskName OR customTaskId: REQUIRED
338
+ - listName: Optional, but recommended when using taskName for faster and more precise lookup
338
339
 
339
340
  Note:
340
- - Task names are only unique within a list, so the system needs to know which list to search in
341
+ - When using just taskName, the system performs a global search across all lists
342
+ - Task names are most unique within a specific list, so providing listName increases reliability
341
343
  - Regular task IDs are always 9 characters long (e.g., "86b394eqa")
342
344
  - Custom IDs have an uppercase prefix followed by a hyphen and number (e.g., "DEV-1234")
343
345
  - Set subtasks=true to include all subtasks in the response`,
@@ -350,11 +352,11 @@ Note:
350
352
  },
351
353
  taskName: {
352
354
  type: "string",
353
- description: "Name of task to retrieve. When using this parameter, you MUST also provide listName."
355
+ description: "Name of task to retrieve. Can be used alone for a global search, or with listName for faster lookup."
354
356
  },
355
357
  listName: {
356
358
  type: "string",
357
- description: "Name of list containing the task. REQUIRED when using taskName."
359
+ description: "Name of list containing the task. Optional but recommended when using taskName."
358
360
  },
359
361
  customTaskId: {
360
362
  type: "string",
@@ -11,7 +11,7 @@ import { formatDueDate } from '../utils.js';
11
11
  import { clickUpServices } from '../../services/shared.js';
12
12
  import { findListIDByName } from '../../tools/list.js';
13
13
  // Use shared services instance for ID resolution
14
- const { workspace: workspaceService } = clickUpServices;
14
+ const { workspace: workspaceService, task: taskService } = clickUpServices;
15
15
  //=============================================================================
16
16
  // DATA FORMATTING UTILITIES
17
17
  //=============================================================================
@@ -65,22 +65,38 @@ export function formatTaskData(task, additional = {}) {
65
65
  ...additional
66
66
  };
67
67
  }
68
- //=============================================================================
69
- // VALIDATION UTILITIES
70
- //=============================================================================
71
68
  /**
72
69
  * Validates task identification parameters
73
- * Ensures either taskId, customTaskId, or both taskName and listName are provided
74
- * When useGlobalLookup is true, allows taskName without listName
70
+ *
71
+ * @param params - Task identification parameters
72
+ * @param options - Validation options
73
+ * @returns Validation result with error message if any
75
74
  */
76
- export function validateTaskIdentification(taskId, taskName, listName, customTaskId, useGlobalLookup = true) {
75
+ export function validateTaskIdentification(params, options = {}) {
76
+ const { taskId, taskName, customTaskId, listName } = params;
77
+ const { requireTaskId = false, useGlobalLookup = true } = options;
78
+ // If taskId is required, it must be provided
79
+ if (requireTaskId && !taskId) {
80
+ return {
81
+ isValid: false,
82
+ errorMessage: 'Task ID is required for this operation'
83
+ };
84
+ }
85
+ // At least one identification method must be provided
77
86
  if (!taskId && !taskName && !customTaskId) {
78
- throw new Error("Either taskId, customTaskId, or taskName must be provided");
87
+ return {
88
+ isValid: false,
89
+ errorMessage: 'Either taskId, taskName, or customTaskId must be provided to identify the task'
90
+ };
79
91
  }
80
- // When global lookup is not enabled, we need list context for task name lookup
81
- if (!useGlobalLookup && !taskId && !customTaskId && taskName && !listName) {
82
- throw new Error("listName is required when using taskName and global lookup is disabled");
92
+ // When using taskName without global lookup, listName is required
93
+ if (taskName && !taskId && !customTaskId && !useGlobalLookup && !listName) {
94
+ return {
95
+ isValid: false,
96
+ errorMessage: 'When identifying a task by name, you must also provide the listName parameter'
97
+ };
83
98
  }
99
+ return { isValid: true };
84
100
  }
85
101
  /**
86
102
  * Validates list identification parameters
@@ -99,7 +115,7 @@ export function validateTaskUpdateData(updateData) {
99
115
  // Check if there are any valid update fields present
100
116
  const hasUpdates = Object.keys(updateData).some(key => {
101
117
  return ['name', 'description', 'markdown_description', 'status', 'priority',
102
- 'dueDate', 'startDate', 'taskId', 'taskName', 'custom_fields'].includes(key);
118
+ 'dueDate', 'startDate', 'custom_fields'].includes(key);
103
119
  });
104
120
  if (!hasUpdates) {
105
121
  throw new Error("At least one field to update must be provided");
@@ -231,3 +247,34 @@ export function extractTreePath(root, targetId) {
231
247
  // Not found in this branch
232
248
  return [];
233
249
  }
250
+ /**
251
+ * Get task ID from various identification methods
252
+ */
253
+ export async function getTaskId(taskId, taskName, listName, customTaskId, requireId = false) {
254
+ // Validate task identification
255
+ const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { requireTaskId: requireId, useGlobalLookup: true });
256
+ if (!validationResult.isValid) {
257
+ throw new Error(validationResult.errorMessage);
258
+ }
259
+ try {
260
+ const result = await taskService.findTasks({
261
+ taskId,
262
+ customTaskId,
263
+ taskName,
264
+ listName,
265
+ allowMultipleMatches: false,
266
+ useSmartDisambiguation: true,
267
+ includeFullDetails: false
268
+ });
269
+ if (!result || Array.isArray(result)) {
270
+ throw new Error(`Task not found with the provided identification`);
271
+ }
272
+ return result.id;
273
+ }
274
+ catch (error) {
275
+ if (error.message.includes('Multiple tasks found')) {
276
+ throw new Error(`Multiple tasks found with name "${taskName}". Please provide list name to disambiguate.`);
277
+ }
278
+ throw error;
279
+ }
280
+ }