@alanse/clickup-multi-mcp-server 1.0.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/Dockerfile +38 -0
- package/LICENSE +21 -0
- package/README.md +470 -0
- package/build/config.js +237 -0
- package/build/index.js +87 -0
- package/build/logger.js +163 -0
- package/build/middleware/security.js +231 -0
- package/build/server.js +288 -0
- package/build/services/clickup/base.js +432 -0
- package/build/services/clickup/bulk.js +180 -0
- package/build/services/clickup/document.js +159 -0
- package/build/services/clickup/folder.js +136 -0
- package/build/services/clickup/index.js +76 -0
- package/build/services/clickup/list.js +191 -0
- package/build/services/clickup/tag.js +239 -0
- package/build/services/clickup/task/index.js +32 -0
- package/build/services/clickup/task/task-attachments.js +105 -0
- package/build/services/clickup/task/task-comments.js +114 -0
- package/build/services/clickup/task/task-core.js +604 -0
- package/build/services/clickup/task/task-custom-fields.js +107 -0
- package/build/services/clickup/task/task-search.js +986 -0
- package/build/services/clickup/task/task-service.js +104 -0
- package/build/services/clickup/task/task-tags.js +113 -0
- package/build/services/clickup/time.js +244 -0
- package/build/services/clickup/types.js +33 -0
- package/build/services/clickup/workspace.js +397 -0
- package/build/services/shared.js +61 -0
- package/build/sse_server.js +277 -0
- package/build/tools/documents.js +489 -0
- package/build/tools/folder.js +331 -0
- package/build/tools/index.js +16 -0
- package/build/tools/list.js +428 -0
- package/build/tools/member.js +106 -0
- package/build/tools/tag.js +833 -0
- package/build/tools/task/attachments.js +357 -0
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +338 -0
- package/build/tools/task/handlers.js +919 -0
- package/build/tools/task/index.js +30 -0
- package/build/tools/task/main.js +233 -0
- package/build/tools/task/single-operations.js +469 -0
- package/build/tools/task/time-tracking.js +575 -0
- package/build/tools/task/utilities.js +310 -0
- package/build/tools/task/workspace-operations.js +258 -0
- package/build/tools/tool-enhancer.js +37 -0
- package/build/tools/utils.js +12 -0
- package/build/tools/workspace-helper.js +44 -0
- package/build/tools/workspace.js +73 -0
- package/build/utils/color-processor.js +183 -0
- package/build/utils/concurrency-utils.js +248 -0
- package/build/utils/date-utils.js +542 -0
- package/build/utils/resolver-utils.js +135 -0
- package/build/utils/sponsor-service.js +93 -0
- package/build/utils/token-utils.js +49 -0
- package/package.json +77 -0
- package/smithery.yaml +23 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* ClickUp MCP Task Operation Handlers
|
|
6
|
+
*
|
|
7
|
+
* This module implements the handlers for task operations, both for single task
|
|
8
|
+
* and bulk operations. These handlers are used by the tool definitions.
|
|
9
|
+
*/
|
|
10
|
+
import { toTaskPriority } from '../../services/clickup/types.js';
|
|
11
|
+
import { getClickUpServices } from '../../services/shared.js';
|
|
12
|
+
import { BulkService } from '../../services/clickup/bulk.js';
|
|
13
|
+
import { parseDueDate } from '../utils.js';
|
|
14
|
+
import { validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveListIdWithValidation } from './utilities.js';
|
|
15
|
+
import { handleResolveAssignees } from '../member.js';
|
|
16
|
+
import { isNameMatch } from '../../utils/resolver-utils.js';
|
|
17
|
+
import { Logger } from '../../logger.js';
|
|
18
|
+
// Use default workspace services for backwards compatibility
|
|
19
|
+
const defaultServices = getClickUpServices();
|
|
20
|
+
const { task: taskService, list: listService, workspace: workspaceService } = defaultServices;
|
|
21
|
+
// Create a bulk service instance that uses the task service
|
|
22
|
+
const bulkService = new BulkService(taskService);
|
|
23
|
+
// Create a logger instance for task handlers
|
|
24
|
+
const logger = new Logger('TaskHandlers');
|
|
25
|
+
// Token limit constant for workspace tasks
|
|
26
|
+
const WORKSPACE_TASKS_TOKEN_LIMIT = 50000;
|
|
27
|
+
// Cache for task context between sequential operations
|
|
28
|
+
const taskContextCache = new Map();
|
|
29
|
+
const TASK_CONTEXT_TTL = 5 * 60 * 1000; // 5 minutes
|
|
30
|
+
/**
|
|
31
|
+
* Store task context for sequential operations
|
|
32
|
+
*/
|
|
33
|
+
function storeTaskContext(taskName, taskId) {
|
|
34
|
+
taskContextCache.set(taskName, {
|
|
35
|
+
id: taskId,
|
|
36
|
+
timestamp: Date.now()
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get cached task context if valid
|
|
41
|
+
*/
|
|
42
|
+
function getCachedTaskContext(taskName) {
|
|
43
|
+
const context = taskContextCache.get(taskName);
|
|
44
|
+
if (!context)
|
|
45
|
+
return null;
|
|
46
|
+
if (Date.now() - context.timestamp > TASK_CONTEXT_TTL) {
|
|
47
|
+
taskContextCache.delete(taskName);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return context.id;
|
|
51
|
+
}
|
|
52
|
+
//=============================================================================
|
|
53
|
+
// SHARED UTILITY FUNCTIONS
|
|
54
|
+
//=============================================================================
|
|
55
|
+
/**
|
|
56
|
+
* Parse time estimate string into minutes
|
|
57
|
+
* Supports formats like "2h 30m", "150m", "2.5h"
|
|
58
|
+
*/
|
|
59
|
+
function parseTimeEstimate(timeEstimate) {
|
|
60
|
+
// If it's already a number, return it directly
|
|
61
|
+
if (typeof timeEstimate === 'number') {
|
|
62
|
+
return timeEstimate;
|
|
63
|
+
}
|
|
64
|
+
if (!timeEstimate || typeof timeEstimate !== 'string')
|
|
65
|
+
return 0;
|
|
66
|
+
// If it's just a number as string, parse it
|
|
67
|
+
if (/^\d+$/.test(timeEstimate)) {
|
|
68
|
+
return parseInt(timeEstimate, 10);
|
|
69
|
+
}
|
|
70
|
+
let totalMinutes = 0;
|
|
71
|
+
// Extract hours
|
|
72
|
+
const hoursMatch = timeEstimate.match(/(\d+\.?\d*)h/);
|
|
73
|
+
if (hoursMatch) {
|
|
74
|
+
totalMinutes += parseFloat(hoursMatch[1]) * 60;
|
|
75
|
+
}
|
|
76
|
+
// Extract minutes
|
|
77
|
+
const minutesMatch = timeEstimate.match(/(\d+)m/);
|
|
78
|
+
if (minutesMatch) {
|
|
79
|
+
totalMinutes += parseInt(minutesMatch[1], 10);
|
|
80
|
+
}
|
|
81
|
+
return Math.round(totalMinutes); // Return minutes
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve assignees from mixed input (user IDs, emails, usernames) to user IDs
|
|
85
|
+
*/
|
|
86
|
+
async function resolveAssignees(assignees) {
|
|
87
|
+
if (!assignees || !Array.isArray(assignees) || assignees.length === 0) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const resolved = [];
|
|
91
|
+
const toResolve = [];
|
|
92
|
+
// Separate numeric IDs from strings that need resolution
|
|
93
|
+
for (const assignee of assignees) {
|
|
94
|
+
if (typeof assignee === 'number') {
|
|
95
|
+
resolved.push(assignee);
|
|
96
|
+
}
|
|
97
|
+
else if (typeof assignee === 'string') {
|
|
98
|
+
// Check if it's a numeric string
|
|
99
|
+
const numericId = parseInt(assignee, 10);
|
|
100
|
+
if (!isNaN(numericId) && numericId.toString() === assignee) {
|
|
101
|
+
resolved.push(numericId);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// It's an email or username that needs resolution
|
|
105
|
+
toResolve.push(assignee);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Resolve emails/usernames to user IDs if any
|
|
110
|
+
if (toResolve.length > 0) {
|
|
111
|
+
try {
|
|
112
|
+
const result = await handleResolveAssignees({ assignees: toResolve });
|
|
113
|
+
// The result is wrapped by sponsorService.createResponse, so we need to parse the JSON
|
|
114
|
+
if (result.content && Array.isArray(result.content) && result.content.length > 0) {
|
|
115
|
+
const dataText = result.content[0].text;
|
|
116
|
+
const parsedData = JSON.parse(dataText);
|
|
117
|
+
if (parsedData.userIds && Array.isArray(parsedData.userIds)) {
|
|
118
|
+
for (const userId of parsedData.userIds) {
|
|
119
|
+
if (userId !== null && typeof userId === 'number') {
|
|
120
|
+
resolved.push(userId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.warn('Failed to resolve some assignees:', error.message);
|
|
128
|
+
// Continue with the IDs we could resolve
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build task update data from parameters
|
|
135
|
+
*/
|
|
136
|
+
async function buildUpdateData(params) {
|
|
137
|
+
const updateData = {};
|
|
138
|
+
if (params.name !== undefined)
|
|
139
|
+
updateData.name = params.name;
|
|
140
|
+
if (params.description !== undefined)
|
|
141
|
+
updateData.description = params.description;
|
|
142
|
+
if (params.markdown_description !== undefined)
|
|
143
|
+
updateData.markdown_description = params.markdown_description;
|
|
144
|
+
if (params.status !== undefined)
|
|
145
|
+
updateData.status = params.status;
|
|
146
|
+
// Use toTaskPriority to properly handle null values and validation
|
|
147
|
+
if (params.priority !== undefined) {
|
|
148
|
+
updateData.priority = toTaskPriority(params.priority);
|
|
149
|
+
}
|
|
150
|
+
if (params.dueDate !== undefined) {
|
|
151
|
+
const parsedDueDate = parseDueDate(params.dueDate);
|
|
152
|
+
if (parsedDueDate !== undefined) {
|
|
153
|
+
updateData.due_date = parsedDueDate;
|
|
154
|
+
updateData.due_date_time = true;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Clear the due date by setting it to null
|
|
158
|
+
updateData.due_date = null;
|
|
159
|
+
updateData.due_date_time = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (params.startDate !== undefined) {
|
|
163
|
+
const parsedStartDate = parseDueDate(params.startDate);
|
|
164
|
+
if (parsedStartDate !== undefined) {
|
|
165
|
+
updateData.start_date = parsedStartDate;
|
|
166
|
+
updateData.start_date_time = true;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Clear the start date by setting it to null
|
|
170
|
+
updateData.start_date = null;
|
|
171
|
+
updateData.start_date_time = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Handle time estimate if provided - convert from string to minutes
|
|
175
|
+
if (params.time_estimate !== undefined) {
|
|
176
|
+
// Log the time estimate for debugging
|
|
177
|
+
console.log(`Original time_estimate: ${params.time_estimate}, typeof: ${typeof params.time_estimate}`);
|
|
178
|
+
// Parse and convert to number in minutes
|
|
179
|
+
const minutes = parseTimeEstimate(params.time_estimate);
|
|
180
|
+
console.log(`Converted time_estimate: ${minutes}`);
|
|
181
|
+
updateData.time_estimate = minutes;
|
|
182
|
+
}
|
|
183
|
+
// Handle custom fields if provided
|
|
184
|
+
if (params.custom_fields !== undefined) {
|
|
185
|
+
updateData.custom_fields = params.custom_fields;
|
|
186
|
+
}
|
|
187
|
+
// Handle assignees if provided - resolve emails/usernames to user IDs
|
|
188
|
+
if (params.assignees !== undefined) {
|
|
189
|
+
// Parse assignees if it's a string (from MCP serialization)
|
|
190
|
+
let assigneesArray = params.assignees;
|
|
191
|
+
if (typeof params.assignees === 'string') {
|
|
192
|
+
try {
|
|
193
|
+
assigneesArray = JSON.parse(params.assignees);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.warn('Failed to parse assignees string:', params.assignees, error);
|
|
197
|
+
assigneesArray = [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const resolvedAssignees = await resolveAssignees(assigneesArray);
|
|
201
|
+
// Store the resolved assignees for processing in the updateTask method
|
|
202
|
+
// The actual add/rem logic will be handled there based on current vs new assignees
|
|
203
|
+
updateData.assignees = resolvedAssignees;
|
|
204
|
+
}
|
|
205
|
+
return updateData;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Core function to find a task by ID or name
|
|
209
|
+
* This consolidates all task lookup logic in one place for consistency
|
|
210
|
+
*/
|
|
211
|
+
async function findTask(params) {
|
|
212
|
+
const { taskId, taskName, listName, customTaskId, requireId = false, includeSubtasks = false } = params;
|
|
213
|
+
// Validate that we have enough information to identify a task
|
|
214
|
+
const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { requireTaskId: requireId, useGlobalLookup: true });
|
|
215
|
+
if (!validationResult.isValid) {
|
|
216
|
+
throw new Error(validationResult.errorMessage);
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
// Direct path for taskId - most efficient (now includes automatic custom ID detection)
|
|
220
|
+
if (taskId) {
|
|
221
|
+
const task = await taskService.getTask(taskId);
|
|
222
|
+
// Add subtasks if requested
|
|
223
|
+
if (includeSubtasks) {
|
|
224
|
+
const subtasks = await taskService.getSubtasks(task.id);
|
|
225
|
+
return { task, subtasks };
|
|
226
|
+
}
|
|
227
|
+
return { task };
|
|
228
|
+
}
|
|
229
|
+
// Direct path for customTaskId - for explicit custom ID requests
|
|
230
|
+
// Note: This is now mainly for backward compatibility since getTask() handles custom IDs automatically
|
|
231
|
+
if (customTaskId) {
|
|
232
|
+
const task = await taskService.getTaskByCustomId(customTaskId);
|
|
233
|
+
// Add subtasks if requested
|
|
234
|
+
if (includeSubtasks) {
|
|
235
|
+
const subtasks = await taskService.getSubtasks(task.id);
|
|
236
|
+
return { task, subtasks };
|
|
237
|
+
}
|
|
238
|
+
return { task };
|
|
239
|
+
}
|
|
240
|
+
// Special optimized path for taskName + listName combination
|
|
241
|
+
if (taskName && listName) {
|
|
242
|
+
const listId = await resolveListIdWithValidation(null, listName);
|
|
243
|
+
// Get all tasks in the list
|
|
244
|
+
const allTasks = await taskService.getTasks(listId);
|
|
245
|
+
// Find the task that matches the name
|
|
246
|
+
const matchingTask = findTaskByName(allTasks, taskName);
|
|
247
|
+
if (!matchingTask) {
|
|
248
|
+
throw new Error(`Task "${taskName}" not found in list "${listName}"`);
|
|
249
|
+
}
|
|
250
|
+
// Add subtasks if requested
|
|
251
|
+
if (includeSubtasks) {
|
|
252
|
+
const subtasks = await taskService.getSubtasks(matchingTask.id);
|
|
253
|
+
return { task: matchingTask, subtasks };
|
|
254
|
+
}
|
|
255
|
+
return { task: matchingTask };
|
|
256
|
+
}
|
|
257
|
+
// Fallback to searching all lists for taskName-only case
|
|
258
|
+
if (taskName) {
|
|
259
|
+
logger.debug(`Searching all lists for task: "${taskName}"`);
|
|
260
|
+
// Get workspace hierarchy which contains all lists
|
|
261
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
262
|
+
// Extract all list IDs from the hierarchy
|
|
263
|
+
const listIds = [];
|
|
264
|
+
const extractListIds = (node) => {
|
|
265
|
+
if (node.type === 'list') {
|
|
266
|
+
listIds.push(node.id);
|
|
267
|
+
}
|
|
268
|
+
if (node.children) {
|
|
269
|
+
node.children.forEach(extractListIds);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
// Start from the root's children
|
|
273
|
+
hierarchy.root.children.forEach(extractListIds);
|
|
274
|
+
// Search through each list
|
|
275
|
+
const searchPromises = listIds.map(async (listId) => {
|
|
276
|
+
try {
|
|
277
|
+
const tasks = await taskService.getTasks(listId);
|
|
278
|
+
const matchingTask = findTaskByName(tasks, taskName);
|
|
279
|
+
if (matchingTask) {
|
|
280
|
+
logger.debug(`Found task "${matchingTask.name}" (ID: ${matchingTask.id}) in list with ID "${listId}"`);
|
|
281
|
+
return matchingTask;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
logger.warn(`Error searching list ${listId}: ${error.message}`);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
// Wait for all searches to complete
|
|
291
|
+
const results = await Promise.all(searchPromises);
|
|
292
|
+
// Filter out null results and sort by match quality and recency
|
|
293
|
+
const matchingTasks = results
|
|
294
|
+
.filter(task => task !== null)
|
|
295
|
+
.sort((a, b) => {
|
|
296
|
+
const aMatch = isNameMatch(a.name, taskName);
|
|
297
|
+
const bMatch = isNameMatch(b.name, taskName);
|
|
298
|
+
// First sort by match quality
|
|
299
|
+
if (bMatch.score !== aMatch.score) {
|
|
300
|
+
return bMatch.score - aMatch.score;
|
|
301
|
+
}
|
|
302
|
+
// Then sort by recency
|
|
303
|
+
return parseInt(b.date_updated) - parseInt(a.date_updated);
|
|
304
|
+
});
|
|
305
|
+
if (matchingTasks.length === 0) {
|
|
306
|
+
throw new Error(`Task "${taskName}" not found in any list across your workspace. Please check the task name and try again.`);
|
|
307
|
+
}
|
|
308
|
+
const bestMatch = matchingTasks[0];
|
|
309
|
+
// Add subtasks if requested
|
|
310
|
+
if (includeSubtasks) {
|
|
311
|
+
const subtasks = await taskService.getSubtasks(bestMatch.id);
|
|
312
|
+
return { task: bestMatch, subtasks };
|
|
313
|
+
}
|
|
314
|
+
return { task: bestMatch };
|
|
315
|
+
}
|
|
316
|
+
// We shouldn't reach here if validation is working correctly
|
|
317
|
+
throw new Error("No valid task identification provided");
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
// Enhance error message for non-existent tasks
|
|
321
|
+
if (taskName && error.message.includes('not found')) {
|
|
322
|
+
throw new Error(`Task "${taskName}" not found. Please check the task name and try again.`);
|
|
323
|
+
}
|
|
324
|
+
// Pass along other formatted errors
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Helper function to find a task by name in an array of tasks
|
|
330
|
+
*/
|
|
331
|
+
function findTaskByName(tasks, name) {
|
|
332
|
+
if (!tasks || !Array.isArray(tasks) || !name)
|
|
333
|
+
return null;
|
|
334
|
+
const normalizedSearchName = name.toLowerCase().trim();
|
|
335
|
+
// Get match scores for all tasks
|
|
336
|
+
const taskMatchScores = tasks.map(task => {
|
|
337
|
+
const matchResult = isNameMatch(task.name, name);
|
|
338
|
+
return {
|
|
339
|
+
task,
|
|
340
|
+
matchResult,
|
|
341
|
+
// Parse the date_updated field as a number for sorting
|
|
342
|
+
updatedAt: task.date_updated ? parseInt(task.date_updated, 10) : 0
|
|
343
|
+
};
|
|
344
|
+
}).filter(result => result.matchResult.isMatch);
|
|
345
|
+
if (taskMatchScores.length === 0) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
// First, try to find exact matches
|
|
349
|
+
const exactMatches = taskMatchScores
|
|
350
|
+
.filter(result => result.matchResult.exactMatch)
|
|
351
|
+
.sort((a, b) => {
|
|
352
|
+
// For exact matches with the same score, sort by most recently updated
|
|
353
|
+
if (b.matchResult.score === a.matchResult.score) {
|
|
354
|
+
return b.updatedAt - a.updatedAt;
|
|
355
|
+
}
|
|
356
|
+
return b.matchResult.score - a.matchResult.score;
|
|
357
|
+
});
|
|
358
|
+
// Get the best matches based on whether we have exact matches or need to fall back to fuzzy matches
|
|
359
|
+
const bestMatches = exactMatches.length > 0 ? exactMatches : taskMatchScores.sort((a, b) => {
|
|
360
|
+
// First sort by match score (highest first)
|
|
361
|
+
if (b.matchResult.score !== a.matchResult.score) {
|
|
362
|
+
return b.matchResult.score - a.matchResult.score;
|
|
363
|
+
}
|
|
364
|
+
// Then sort by most recently updated
|
|
365
|
+
return b.updatedAt - a.updatedAt;
|
|
366
|
+
});
|
|
367
|
+
// Get the best match
|
|
368
|
+
return bestMatches[0].task;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Handler for getting a task - uses the consolidated findTask function
|
|
372
|
+
*/
|
|
373
|
+
export async function getTaskHandler(params) {
|
|
374
|
+
try {
|
|
375
|
+
const result = await findTask({
|
|
376
|
+
taskId: params.taskId,
|
|
377
|
+
taskName: params.taskName,
|
|
378
|
+
listName: params.listName,
|
|
379
|
+
customTaskId: params.customTaskId,
|
|
380
|
+
includeSubtasks: params.subtasks
|
|
381
|
+
});
|
|
382
|
+
if (result.subtasks) {
|
|
383
|
+
return { ...result.task, subtasks: result.subtasks };
|
|
384
|
+
}
|
|
385
|
+
return result.task;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get task ID from various identifiers - uses the consolidated findTask function
|
|
393
|
+
*/
|
|
394
|
+
export async function getTaskId(taskId, taskName, listName, customTaskId, requireId, includeSubtasks) {
|
|
395
|
+
// Check task context cache first if we have a task name
|
|
396
|
+
if (taskName && !taskId && !customTaskId) {
|
|
397
|
+
const cachedId = getCachedTaskContext(taskName);
|
|
398
|
+
if (cachedId) {
|
|
399
|
+
return cachedId;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const result = await findTask({
|
|
403
|
+
taskId,
|
|
404
|
+
taskName,
|
|
405
|
+
listName,
|
|
406
|
+
customTaskId,
|
|
407
|
+
requireId,
|
|
408
|
+
includeSubtasks
|
|
409
|
+
});
|
|
410
|
+
// Store task context for future operations
|
|
411
|
+
if (taskName && result.task.id) {
|
|
412
|
+
storeTaskContext(taskName, result.task.id);
|
|
413
|
+
}
|
|
414
|
+
return result.task.id;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Process a list identification validation, returning the list ID
|
|
418
|
+
*/
|
|
419
|
+
async function getListId(listId, listName) {
|
|
420
|
+
validateListIdentification(listId, listName);
|
|
421
|
+
return await resolveListIdWithValidation(listId, listName);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Extract and build task filters from parameters
|
|
425
|
+
*/
|
|
426
|
+
function buildTaskFilters(params) {
|
|
427
|
+
const { subtasks, statuses, page, order_by, reverse } = params;
|
|
428
|
+
const filters = {};
|
|
429
|
+
if (subtasks !== undefined)
|
|
430
|
+
filters.subtasks = subtasks;
|
|
431
|
+
if (statuses !== undefined)
|
|
432
|
+
filters.statuses = statuses;
|
|
433
|
+
if (page !== undefined)
|
|
434
|
+
filters.page = page;
|
|
435
|
+
if (order_by !== undefined)
|
|
436
|
+
filters.order_by = order_by;
|
|
437
|
+
if (reverse !== undefined)
|
|
438
|
+
filters.reverse = reverse;
|
|
439
|
+
return filters;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Map tasks for bulk operations, resolving task IDs
|
|
443
|
+
* Uses smart disambiguation for tasks without list context
|
|
444
|
+
*/
|
|
445
|
+
async function mapTaskIds(tasks) {
|
|
446
|
+
return Promise.all(tasks.map(async (task) => {
|
|
447
|
+
const validationResult = validateTaskIdentification({ taskId: task.taskId, taskName: task.taskName, listName: task.listName, customTaskId: task.customTaskId }, { useGlobalLookup: true });
|
|
448
|
+
if (!validationResult.isValid) {
|
|
449
|
+
throw new Error(validationResult.errorMessage);
|
|
450
|
+
}
|
|
451
|
+
return await getTaskId(task.taskId, task.taskName, task.listName, task.customTaskId);
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
//=============================================================================
|
|
455
|
+
// SINGLE TASK OPERATIONS
|
|
456
|
+
//=============================================================================
|
|
457
|
+
/**
|
|
458
|
+
* Handler for creating a task
|
|
459
|
+
*/
|
|
460
|
+
export async function createTaskHandler(params) {
|
|
461
|
+
const { name, description, markdown_description, status, dueDate, startDate, parent, tags, custom_fields, check_required_custom_fields, assignees } = params;
|
|
462
|
+
if (!name)
|
|
463
|
+
throw new Error("Task name is required");
|
|
464
|
+
// Use our helper function to validate and convert priority
|
|
465
|
+
const priority = toTaskPriority(params.priority);
|
|
466
|
+
const listId = await getListId(params.listId, params.listName);
|
|
467
|
+
// Resolve assignees if provided
|
|
468
|
+
let resolvedAssignees = undefined;
|
|
469
|
+
if (assignees) {
|
|
470
|
+
// Parse assignees if it's a string (from MCP serialization)
|
|
471
|
+
let assigneesArray = assignees;
|
|
472
|
+
if (typeof assignees === 'string') {
|
|
473
|
+
try {
|
|
474
|
+
assigneesArray = JSON.parse(assignees);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
console.warn('Failed to parse assignees string in createTask:', assignees, error);
|
|
478
|
+
assigneesArray = [];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
resolvedAssignees = await resolveAssignees(assigneesArray);
|
|
482
|
+
}
|
|
483
|
+
const taskData = {
|
|
484
|
+
name,
|
|
485
|
+
description,
|
|
486
|
+
markdown_description,
|
|
487
|
+
status,
|
|
488
|
+
parent,
|
|
489
|
+
tags,
|
|
490
|
+
custom_fields,
|
|
491
|
+
check_required_custom_fields,
|
|
492
|
+
assignees: resolvedAssignees
|
|
493
|
+
};
|
|
494
|
+
// Only include priority if explicitly provided by the user
|
|
495
|
+
if (priority !== undefined) {
|
|
496
|
+
taskData.priority = priority;
|
|
497
|
+
}
|
|
498
|
+
// Add due date if specified
|
|
499
|
+
if (dueDate) {
|
|
500
|
+
taskData.due_date = parseDueDate(dueDate);
|
|
501
|
+
taskData.due_date_time = true;
|
|
502
|
+
}
|
|
503
|
+
// Add start date if specified
|
|
504
|
+
if (startDate) {
|
|
505
|
+
taskData.start_date = parseDueDate(startDate);
|
|
506
|
+
taskData.start_date_time = true;
|
|
507
|
+
}
|
|
508
|
+
return await taskService.createTask(listId, taskData);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Handler for updating a task
|
|
512
|
+
*/
|
|
513
|
+
export async function updateTaskHandler(taskService, params) {
|
|
514
|
+
const { taskId, taskName, listName, customTaskId, ...rawUpdateData } = params;
|
|
515
|
+
// Validate task identification with global lookup enabled
|
|
516
|
+
const validationResult = validateTaskIdentification(params, { useGlobalLookup: true });
|
|
517
|
+
if (!validationResult.isValid) {
|
|
518
|
+
throw new Error(validationResult.errorMessage);
|
|
519
|
+
}
|
|
520
|
+
// Build properly formatted update data from raw parameters (now async)
|
|
521
|
+
const updateData = await buildUpdateData(rawUpdateData);
|
|
522
|
+
// Validate update data
|
|
523
|
+
validateTaskUpdateData(updateData);
|
|
524
|
+
try {
|
|
525
|
+
// Get the task ID using global lookup
|
|
526
|
+
const id = await getTaskId(taskId, taskName, listName, customTaskId);
|
|
527
|
+
return await taskService.updateTask(id, updateData);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
throw new Error(`Failed to update task: ${error instanceof Error ? error.message : String(error)}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Handler for moving a task
|
|
535
|
+
*/
|
|
536
|
+
export async function moveTaskHandler(params) {
|
|
537
|
+
const taskId = await getTaskId(params.taskId, params.taskName, undefined, params.customTaskId, false);
|
|
538
|
+
const listId = await getListId(params.listId, params.listName);
|
|
539
|
+
return await taskService.moveTask(taskId, listId);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Handler for duplicating a task
|
|
543
|
+
*/
|
|
544
|
+
export async function duplicateTaskHandler(params) {
|
|
545
|
+
const taskId = await getTaskId(params.taskId, params.taskName, undefined, params.customTaskId, false);
|
|
546
|
+
let listId;
|
|
547
|
+
if (params.listId || params.listName) {
|
|
548
|
+
listId = await getListId(params.listId, params.listName);
|
|
549
|
+
}
|
|
550
|
+
return await taskService.duplicateTask(taskId, listId);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Handler for getting tasks
|
|
554
|
+
*/
|
|
555
|
+
export async function getTasksHandler(params) {
|
|
556
|
+
const listId = await getListId(params.listId, params.listName);
|
|
557
|
+
return await taskService.getTasks(listId, buildTaskFilters(params));
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Handler for getting task comments
|
|
561
|
+
*/
|
|
562
|
+
export async function getTaskCommentsHandler(params) {
|
|
563
|
+
const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
|
|
564
|
+
const { start, startId } = params;
|
|
565
|
+
return await taskService.getTaskComments(taskId, start, startId);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Handler for creating a task comment
|
|
569
|
+
*/
|
|
570
|
+
export async function createTaskCommentHandler(params) {
|
|
571
|
+
// Validate required parameters
|
|
572
|
+
if (!params.commentText) {
|
|
573
|
+
throw new Error('Comment text is required');
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
// Resolve the task ID
|
|
577
|
+
const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
|
|
578
|
+
// Extract other parameters with defaults
|
|
579
|
+
const { commentText, notifyAll = false, assignee = null } = params;
|
|
580
|
+
// Create the comment
|
|
581
|
+
return await taskService.createTaskComment(taskId, commentText, notifyAll, assignee);
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
// If this is a task lookup error, provide more helpful message
|
|
585
|
+
if (error.message?.includes('not found') || error.message?.includes('identify task')) {
|
|
586
|
+
if (params.taskName) {
|
|
587
|
+
throw new Error(`Could not find task "${params.taskName}" in list "${params.listName}"`);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
throw new Error(`Task with ID "${params.taskId}" not found`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Otherwise, rethrow the original error
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Estimate tokens for a task response
|
|
599
|
+
* This is a simplified estimation - adjust based on actual token counting needs
|
|
600
|
+
*/
|
|
601
|
+
function estimateTaskResponseTokens(task) {
|
|
602
|
+
// Base estimation for task structure
|
|
603
|
+
let tokenCount = 0;
|
|
604
|
+
// Core fields
|
|
605
|
+
tokenCount += (task.name?.length || 0) / 4; // Approximate tokens for name
|
|
606
|
+
tokenCount += (task.description?.length || 0) / 4; // Approximate tokens for description
|
|
607
|
+
tokenCount += (task.text_content?.length || 0) / 4; // Use text_content instead of markdown_description
|
|
608
|
+
// Status and other metadata
|
|
609
|
+
tokenCount += 5; // Basic metadata fields
|
|
610
|
+
// Custom fields
|
|
611
|
+
if (task.custom_fields) {
|
|
612
|
+
tokenCount += Object.keys(task.custom_fields).length * 10; // Rough estimate per custom field
|
|
613
|
+
}
|
|
614
|
+
// Add overhead for JSON structure
|
|
615
|
+
tokenCount *= 1.1;
|
|
616
|
+
return Math.ceil(tokenCount);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Check if response would exceed token limit
|
|
620
|
+
*/
|
|
621
|
+
function wouldExceedTokenLimit(response) {
|
|
622
|
+
if (!response.tasks?.length)
|
|
623
|
+
return false;
|
|
624
|
+
// Calculate total estimated tokens
|
|
625
|
+
const totalTokens = response.tasks.reduce((sum, task) => sum + estimateTaskResponseTokens(task), 0);
|
|
626
|
+
// Add overhead for response structure
|
|
627
|
+
const estimatedTotal = totalTokens * 1.1;
|
|
628
|
+
return estimatedTotal > WORKSPACE_TASKS_TOKEN_LIMIT;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Handler for getting workspace tasks with filtering
|
|
632
|
+
*/
|
|
633
|
+
export async function getWorkspaceTasksHandler(taskService, params) {
|
|
634
|
+
try {
|
|
635
|
+
// Require at least one filter parameter
|
|
636
|
+
const hasFilter = [
|
|
637
|
+
'tags',
|
|
638
|
+
'list_ids',
|
|
639
|
+
'folder_ids',
|
|
640
|
+
'space_ids',
|
|
641
|
+
'statuses',
|
|
642
|
+
'assignees',
|
|
643
|
+
'date_created_gt',
|
|
644
|
+
'date_created_lt',
|
|
645
|
+
'date_updated_gt',
|
|
646
|
+
'date_updated_lt',
|
|
647
|
+
'due_date_gt',
|
|
648
|
+
'due_date_lt'
|
|
649
|
+
].some(key => params[key] !== undefined);
|
|
650
|
+
if (!hasFilter) {
|
|
651
|
+
throw new Error('At least one filter parameter is required (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)');
|
|
652
|
+
}
|
|
653
|
+
// Check if list_ids are provided for enhanced filtering via Views API
|
|
654
|
+
if (params.list_ids && params.list_ids.length > 0) {
|
|
655
|
+
logger.info('Using Views API for enhanced list filtering', {
|
|
656
|
+
listIds: params.list_ids,
|
|
657
|
+
listCount: params.list_ids.length
|
|
658
|
+
});
|
|
659
|
+
// Warning for broad queries
|
|
660
|
+
const hasOnlyListIds = Object.keys(params).filter(key => params[key] !== undefined && key !== 'list_ids' && key !== 'detail_level').length === 0;
|
|
661
|
+
if (hasOnlyListIds && params.list_ids.length > 5) {
|
|
662
|
+
logger.warn('Broad query detected: many lists with no additional filters', {
|
|
663
|
+
listCount: params.list_ids.length,
|
|
664
|
+
recommendation: 'Consider adding additional filters (tags, statuses, assignees, etc.) for better performance'
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
// Use Views API for enhanced list filtering
|
|
668
|
+
let allTasks = [];
|
|
669
|
+
const processedTaskIds = new Set();
|
|
670
|
+
// Create promises for concurrent fetching
|
|
671
|
+
const fetchPromises = params.list_ids.map(async (listId) => {
|
|
672
|
+
try {
|
|
673
|
+
// Get the default list view ID
|
|
674
|
+
const viewId = await taskService.getListViews(listId);
|
|
675
|
+
if (!viewId) {
|
|
676
|
+
logger.warn(`No default view found for list ${listId}, skipping`);
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
// Extract filters supported by the Views API
|
|
680
|
+
const supportedFilters = {
|
|
681
|
+
subtasks: params.subtasks,
|
|
682
|
+
include_closed: params.include_closed,
|
|
683
|
+
archived: params.archived,
|
|
684
|
+
order_by: params.order_by,
|
|
685
|
+
reverse: params.reverse,
|
|
686
|
+
page: params.page,
|
|
687
|
+
statuses: params.statuses,
|
|
688
|
+
assignees: params.assignees,
|
|
689
|
+
date_created_gt: params.date_created_gt,
|
|
690
|
+
date_created_lt: params.date_created_lt,
|
|
691
|
+
date_updated_gt: params.date_updated_gt,
|
|
692
|
+
date_updated_lt: params.date_updated_lt,
|
|
693
|
+
due_date_gt: params.due_date_gt,
|
|
694
|
+
due_date_lt: params.due_date_lt,
|
|
695
|
+
custom_fields: params.custom_fields
|
|
696
|
+
};
|
|
697
|
+
// Get tasks from the view
|
|
698
|
+
const tasksFromView = await taskService.getTasksFromView(viewId, supportedFilters);
|
|
699
|
+
return tasksFromView;
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
logger.error(`Failed to get tasks from list ${listId}`, { error: error.message });
|
|
703
|
+
return []; // Continue with other lists even if one fails
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
// Execute all fetches concurrently
|
|
707
|
+
const taskArrays = await Promise.all(fetchPromises);
|
|
708
|
+
// Aggregate tasks and remove duplicates
|
|
709
|
+
for (const tasks of taskArrays) {
|
|
710
|
+
for (const task of tasks) {
|
|
711
|
+
if (!processedTaskIds.has(task.id)) {
|
|
712
|
+
allTasks.push(task);
|
|
713
|
+
processedTaskIds.add(task.id);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
logger.info('Aggregated tasks from Views API', {
|
|
718
|
+
totalTasks: allTasks.length,
|
|
719
|
+
uniqueTasks: processedTaskIds.size
|
|
720
|
+
});
|
|
721
|
+
// Apply client-side filtering for unsupported filters
|
|
722
|
+
if (params.tags && params.tags.length > 0) {
|
|
723
|
+
allTasks = allTasks.filter(task => params.tags.every((tag) => task.tags.some(t => t.name === tag)));
|
|
724
|
+
logger.debug('Applied client-side tag filtering', {
|
|
725
|
+
tags: params.tags,
|
|
726
|
+
remainingTasks: allTasks.length
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (params.folder_ids && params.folder_ids.length > 0) {
|
|
730
|
+
allTasks = allTasks.filter(task => task.folder && params.folder_ids.includes(task.folder.id));
|
|
731
|
+
logger.debug('Applied client-side folder filtering', {
|
|
732
|
+
folderIds: params.folder_ids,
|
|
733
|
+
remainingTasks: allTasks.length
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (params.space_ids && params.space_ids.length > 0) {
|
|
737
|
+
allTasks = allTasks.filter(task => params.space_ids.includes(task.space.id));
|
|
738
|
+
logger.debug('Applied client-side space filtering', {
|
|
739
|
+
spaceIds: params.space_ids,
|
|
740
|
+
remainingTasks: allTasks.length
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
// Check token limit and format response
|
|
744
|
+
const shouldUseSummary = params.detail_level === 'summary' || wouldExceedTokenLimit({ tasks: allTasks });
|
|
745
|
+
if (shouldUseSummary) {
|
|
746
|
+
logger.info('Using summary format for Views API response', {
|
|
747
|
+
totalTasks: allTasks.length,
|
|
748
|
+
reason: params.detail_level === 'summary' ? 'requested' : 'token_limit'
|
|
749
|
+
});
|
|
750
|
+
return {
|
|
751
|
+
summaries: allTasks.map(task => ({
|
|
752
|
+
id: task.id,
|
|
753
|
+
name: task.name,
|
|
754
|
+
status: task.status.status,
|
|
755
|
+
list: {
|
|
756
|
+
id: task.list.id,
|
|
757
|
+
name: task.list.name
|
|
758
|
+
},
|
|
759
|
+
due_date: task.due_date,
|
|
760
|
+
url: task.url,
|
|
761
|
+
priority: task.priority?.priority || null,
|
|
762
|
+
tags: task.tags.map(tag => ({
|
|
763
|
+
name: tag.name,
|
|
764
|
+
tag_bg: tag.tag_bg,
|
|
765
|
+
tag_fg: tag.tag_fg
|
|
766
|
+
}))
|
|
767
|
+
})),
|
|
768
|
+
total_count: allTasks.length,
|
|
769
|
+
has_more: false,
|
|
770
|
+
next_page: 0
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
tasks: allTasks,
|
|
775
|
+
total_count: allTasks.length,
|
|
776
|
+
has_more: false,
|
|
777
|
+
next_page: 0
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
// Fallback to existing workspace-wide task retrieval when list_ids are not provided
|
|
781
|
+
logger.info('Using standard workspace task retrieval');
|
|
782
|
+
const filters = {
|
|
783
|
+
tags: params.tags,
|
|
784
|
+
list_ids: params.list_ids,
|
|
785
|
+
folder_ids: params.folder_ids,
|
|
786
|
+
space_ids: params.space_ids,
|
|
787
|
+
statuses: params.statuses,
|
|
788
|
+
include_closed: params.include_closed,
|
|
789
|
+
include_archived_lists: params.include_archived_lists,
|
|
790
|
+
include_closed_lists: params.include_closed_lists,
|
|
791
|
+
archived: params.archived,
|
|
792
|
+
order_by: params.order_by,
|
|
793
|
+
reverse: params.reverse,
|
|
794
|
+
due_date_gt: params.due_date_gt,
|
|
795
|
+
due_date_lt: params.due_date_lt,
|
|
796
|
+
date_created_gt: params.date_created_gt,
|
|
797
|
+
date_created_lt: params.date_created_lt,
|
|
798
|
+
date_updated_gt: params.date_updated_gt,
|
|
799
|
+
date_updated_lt: params.date_updated_lt,
|
|
800
|
+
assignees: params.assignees,
|
|
801
|
+
page: params.page,
|
|
802
|
+
detail_level: params.detail_level || 'detailed',
|
|
803
|
+
subtasks: params.subtasks,
|
|
804
|
+
include_subtasks: params.include_subtasks,
|
|
805
|
+
include_compact_time_entries: params.include_compact_time_entries,
|
|
806
|
+
custom_fields: params.custom_fields
|
|
807
|
+
};
|
|
808
|
+
// Get tasks with adaptive response format support
|
|
809
|
+
const response = await taskService.getWorkspaceTasks(filters);
|
|
810
|
+
// Check token limit at handler level
|
|
811
|
+
if (params.detail_level !== 'summary' && wouldExceedTokenLimit(response)) {
|
|
812
|
+
logger.info('Response would exceed token limit, fetching summary format instead');
|
|
813
|
+
// Refetch with summary format
|
|
814
|
+
const summaryResponse = await taskService.getWorkspaceTasks({
|
|
815
|
+
...filters,
|
|
816
|
+
detail_level: 'summary'
|
|
817
|
+
});
|
|
818
|
+
return summaryResponse;
|
|
819
|
+
}
|
|
820
|
+
// Return the response without adding the redundant _note field
|
|
821
|
+
return response;
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
throw new Error(`Failed to get workspace tasks: ${error.message}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
//=============================================================================
|
|
828
|
+
// BULK TASK OPERATIONS
|
|
829
|
+
//=============================================================================
|
|
830
|
+
/**
|
|
831
|
+
* Handler for creating multiple tasks
|
|
832
|
+
*/
|
|
833
|
+
export async function createBulkTasksHandler(params) {
|
|
834
|
+
const { tasks, listId, listName, options } = params;
|
|
835
|
+
// Validate tasks array
|
|
836
|
+
validateBulkTasks(tasks, 'create');
|
|
837
|
+
// Validate and resolve list ID
|
|
838
|
+
const targetListId = await resolveListIdWithValidation(listId, listName);
|
|
839
|
+
// Format tasks for creation - resolve assignees for each task
|
|
840
|
+
const formattedTasks = await Promise.all(tasks.map(async (task) => {
|
|
841
|
+
// Resolve assignees if provided
|
|
842
|
+
const resolvedAssignees = task.assignees ? await resolveAssignees(task.assignees) : undefined;
|
|
843
|
+
const taskData = {
|
|
844
|
+
name: task.name,
|
|
845
|
+
description: task.description,
|
|
846
|
+
markdown_description: task.markdown_description,
|
|
847
|
+
status: task.status,
|
|
848
|
+
tags: task.tags,
|
|
849
|
+
custom_fields: task.custom_fields,
|
|
850
|
+
assignees: resolvedAssignees
|
|
851
|
+
};
|
|
852
|
+
// Only include priority if explicitly provided by the user
|
|
853
|
+
const priority = toTaskPriority(task.priority);
|
|
854
|
+
if (priority !== undefined) {
|
|
855
|
+
taskData.priority = priority;
|
|
856
|
+
}
|
|
857
|
+
// Add due date if specified
|
|
858
|
+
if (task.dueDate) {
|
|
859
|
+
taskData.due_date = parseDueDate(task.dueDate);
|
|
860
|
+
taskData.due_date_time = true;
|
|
861
|
+
}
|
|
862
|
+
// Add start date if specified
|
|
863
|
+
if (task.startDate) {
|
|
864
|
+
taskData.start_date = parseDueDate(task.startDate);
|
|
865
|
+
taskData.start_date_time = true;
|
|
866
|
+
}
|
|
867
|
+
return taskData;
|
|
868
|
+
}));
|
|
869
|
+
// Parse bulk options
|
|
870
|
+
const bulkOptions = parseBulkOptions(options);
|
|
871
|
+
// Create tasks - pass arguments in correct order: listId, tasks, options
|
|
872
|
+
return await bulkService.createTasks(targetListId, formattedTasks, bulkOptions);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Handler for updating multiple tasks
|
|
876
|
+
*/
|
|
877
|
+
export async function updateBulkTasksHandler(params) {
|
|
878
|
+
const { tasks, options } = params;
|
|
879
|
+
// Validate tasks array
|
|
880
|
+
validateBulkTasks(tasks, 'update');
|
|
881
|
+
// Parse bulk options
|
|
882
|
+
const bulkOptions = parseBulkOptions(options);
|
|
883
|
+
// Update tasks
|
|
884
|
+
return await bulkService.updateTasks(tasks, bulkOptions);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Handler for moving multiple tasks
|
|
888
|
+
*/
|
|
889
|
+
export async function moveBulkTasksHandler(params) {
|
|
890
|
+
const { tasks, targetListId, targetListName, options } = params;
|
|
891
|
+
// Validate tasks array
|
|
892
|
+
validateBulkTasks(tasks, 'move');
|
|
893
|
+
// Validate and resolve target list ID
|
|
894
|
+
const resolvedTargetListId = await resolveListIdWithValidation(targetListId, targetListName);
|
|
895
|
+
// Parse bulk options
|
|
896
|
+
const bulkOptions = parseBulkOptions(options);
|
|
897
|
+
// Move tasks
|
|
898
|
+
return await bulkService.moveTasks(tasks, resolvedTargetListId, bulkOptions);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Handler for deleting multiple tasks
|
|
902
|
+
*/
|
|
903
|
+
export async function deleteBulkTasksHandler(params) {
|
|
904
|
+
const { tasks, options } = params;
|
|
905
|
+
// Validate tasks array
|
|
906
|
+
validateBulkTasks(tasks, 'delete');
|
|
907
|
+
// Parse bulk options
|
|
908
|
+
const bulkOptions = parseBulkOptions(options);
|
|
909
|
+
// Delete tasks
|
|
910
|
+
return await bulkService.deleteTasks(tasks, bulkOptions);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Handler for deleting a task
|
|
914
|
+
*/
|
|
915
|
+
export async function deleteTaskHandler(params) {
|
|
916
|
+
const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
|
|
917
|
+
await taskService.deleteTask(taskId);
|
|
918
|
+
return true;
|
|
919
|
+
}
|