@taazkareem/clickup-mcp-server 0.4.60 → 0.4.62
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/README.md +10 -2
- package/build/config.js +10 -0
- package/build/index.js +9 -1356
- package/build/server.js +120 -0
- package/build/services/clickup/base.js +253 -0
- package/build/services/clickup/bulk.js +116 -0
- package/build/services/clickup/folder.js +133 -0
- package/build/services/clickup/index.js +43 -0
- package/build/services/clickup/initialization.js +28 -0
- package/build/services/clickup/list.js +188 -0
- package/build/services/clickup/task.js +492 -0
- package/build/services/clickup/types.js +4 -0
- package/build/services/clickup/workspace.js +314 -0
- package/build/services/shared.js +15 -0
- package/build/tools/folder.js +356 -0
- package/build/tools/index.js +11 -0
- package/build/tools/list.js +452 -0
- package/build/tools/task.js +1486 -0
- package/build/tools/utils.js +95 -0
- package/build/tools/workspace.js +132 -0
- package/package.json +3 -1
- package/build/services/clickup.js +0 -765
- package/build/types/clickup.js +0 -1
|
@@ -0,0 +1,1486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickUp MCP Task Tools
|
|
3
|
+
*
|
|
4
|
+
* This module defines task-related tools including creating, updating,
|
|
5
|
+
* moving, duplicating, and deleting tasks. It also provides tools for
|
|
6
|
+
* retrieving task details.
|
|
7
|
+
*/
|
|
8
|
+
import { createClickUpServices } from '../services/clickup/index.js';
|
|
9
|
+
import config from '../config.js';
|
|
10
|
+
import { findListIDByName } from './list.js';
|
|
11
|
+
import { parseDueDate } from './utils.js';
|
|
12
|
+
// Initialize ClickUp services using the factory function
|
|
13
|
+
const services = createClickUpServices({
|
|
14
|
+
apiKey: config.clickupApiKey,
|
|
15
|
+
teamId: config.clickupTeamId
|
|
16
|
+
});
|
|
17
|
+
// Extract the services we need for task operations
|
|
18
|
+
const { task: taskService, workspace: workspaceService } = services;
|
|
19
|
+
/**
|
|
20
|
+
* Tool definition for creating a single task
|
|
21
|
+
*/
|
|
22
|
+
export const createTaskTool = {
|
|
23
|
+
name: "create_task",
|
|
24
|
+
description: "Create a single task in a ClickUp list. Use this tool for individual task creation only. For multiple tasks, use create_bulk_tasks instead. Before calling this tool, check if you already have the necessary list ID from previous responses in the conversation history, as this avoids redundant lookups. When creating a task, you must provide either a listId or listName.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
name: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Name of the task. Put a relevant emoji followed by a blank space before the name."
|
|
31
|
+
},
|
|
32
|
+
description: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Plain text description for the task"
|
|
35
|
+
},
|
|
36
|
+
markdown_description: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Markdown formatted description for the task. If provided, this takes precedence over description"
|
|
39
|
+
},
|
|
40
|
+
listId: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "ID of the list to create the task in (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
|
|
43
|
+
},
|
|
44
|
+
listName: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Name of the list to create the task in - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
|
|
47
|
+
},
|
|
48
|
+
status: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "OPTIONAL: Override the default ClickUp status. In most cases, you should omit this to use ClickUp defaults"
|
|
51
|
+
},
|
|
52
|
+
priority: {
|
|
53
|
+
type: "number",
|
|
54
|
+
description: "Priority of the task (1-4), where 1 is urgent/highest priority and 4 is lowest priority. Only set this when the user explicitly requests a priority level."
|
|
55
|
+
},
|
|
56
|
+
dueDate: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Due date of the task (Unix timestamp in milliseconds). Convert dates to this format before submitting."
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
required: ["name"]
|
|
62
|
+
},
|
|
63
|
+
async handler({ name, description, markdown_description, dueDate, priority, status, listId, listName }) {
|
|
64
|
+
let targetListId = listId;
|
|
65
|
+
// If no listId but listName is provided, look up the list ID
|
|
66
|
+
if (!targetListId && listName) {
|
|
67
|
+
// Use workspace service to find list by name
|
|
68
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
69
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
70
|
+
if (!listInfo) {
|
|
71
|
+
throw new Error(`List "${listName}" not found`);
|
|
72
|
+
}
|
|
73
|
+
targetListId = listInfo.id;
|
|
74
|
+
}
|
|
75
|
+
if (!targetListId) {
|
|
76
|
+
throw new Error("Either listId or listName must be provided");
|
|
77
|
+
}
|
|
78
|
+
// Prepare task data
|
|
79
|
+
const taskData = {
|
|
80
|
+
name,
|
|
81
|
+
description,
|
|
82
|
+
markdown_description,
|
|
83
|
+
status,
|
|
84
|
+
priority: priority,
|
|
85
|
+
due_date: dueDate ? parseDueDate(dueDate) : undefined
|
|
86
|
+
};
|
|
87
|
+
// Add due_date_time flag if due date is set
|
|
88
|
+
if (dueDate && taskData.due_date) {
|
|
89
|
+
taskData.due_date_time = true;
|
|
90
|
+
}
|
|
91
|
+
// Create the task
|
|
92
|
+
const createdTask = await taskService.createTask(targetListId, taskData);
|
|
93
|
+
// Format response
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: JSON.stringify({
|
|
98
|
+
id: createdTask.id,
|
|
99
|
+
name: createdTask.name,
|
|
100
|
+
url: createdTask.url,
|
|
101
|
+
status: createdTask.status?.status || "New",
|
|
102
|
+
list: createdTask.list.name,
|
|
103
|
+
space: createdTask.space.name,
|
|
104
|
+
folder: createdTask.folder?.name
|
|
105
|
+
}, null, 2)
|
|
106
|
+
}]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Tool definition for updating a task
|
|
112
|
+
*/
|
|
113
|
+
export const updateTaskTool = {
|
|
114
|
+
name: "update_task",
|
|
115
|
+
description: "Modify an existing task's properties. Valid parameter combinations:\n1. Use taskId alone (preferred if you have it)\n2. Use taskName + optional listName (to disambiguate if multiple tasks have the same name)\n\nAt least one update field (name, description, status, priority) must be provided. Only specified fields will be updated.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
taskId: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "ID of the task to update (preferred). Use this instead of taskName if you have it from a previous response."
|
|
122
|
+
},
|
|
123
|
+
taskName: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Name of the task to update. Only use this if you don't have taskId. Warning: Task names may not be unique."
|
|
126
|
+
},
|
|
127
|
+
listName: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Name of the list containing the task. Required when using taskName if multiple tasks have the same name."
|
|
130
|
+
},
|
|
131
|
+
name: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "New name for the task. Include emoji prefix if appropriate."
|
|
134
|
+
},
|
|
135
|
+
description: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "New plain text description. Will be ignored if markdown_description is provided."
|
|
138
|
+
},
|
|
139
|
+
markdown_description: {
|
|
140
|
+
type: "string",
|
|
141
|
+
description: "New markdown description. Takes precedence over plain text description."
|
|
142
|
+
},
|
|
143
|
+
status: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "New status. Must be valid for the task's current list."
|
|
146
|
+
},
|
|
147
|
+
priority: {
|
|
148
|
+
type: ["number", "null"],
|
|
149
|
+
enum: [1, 2, 3, 4, null],
|
|
150
|
+
description: "New priority: 1 (urgent) to 4 (low). Set null to clear priority."
|
|
151
|
+
},
|
|
152
|
+
dueDate: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "New due date (Unix timestamp in milliseconds)"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
required: []
|
|
158
|
+
},
|
|
159
|
+
async handler({ taskId, taskName, listName, name, description, markdown_description, status, priority, dueDate }) {
|
|
160
|
+
let targetTaskId = taskId;
|
|
161
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
162
|
+
if (!targetTaskId && taskName) {
|
|
163
|
+
// First find the list ID if listName is provided
|
|
164
|
+
let listId;
|
|
165
|
+
if (listName) {
|
|
166
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
167
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
168
|
+
if (!listInfo) {
|
|
169
|
+
throw new Error(`List "${listName}" not found`);
|
|
170
|
+
}
|
|
171
|
+
listId = listInfo.id;
|
|
172
|
+
}
|
|
173
|
+
// Now find the task
|
|
174
|
+
const tasks = await taskService.getTasks(listId || '');
|
|
175
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
176
|
+
if (!foundTask) {
|
|
177
|
+
throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
|
|
178
|
+
}
|
|
179
|
+
targetTaskId = foundTask.id;
|
|
180
|
+
}
|
|
181
|
+
if (!targetTaskId) {
|
|
182
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
183
|
+
}
|
|
184
|
+
// Prepare update data
|
|
185
|
+
const updateData = {};
|
|
186
|
+
if (name !== undefined)
|
|
187
|
+
updateData.name = name;
|
|
188
|
+
if (description !== undefined)
|
|
189
|
+
updateData.description = description;
|
|
190
|
+
if (markdown_description !== undefined)
|
|
191
|
+
updateData.markdown_description = markdown_description;
|
|
192
|
+
if (status !== undefined)
|
|
193
|
+
updateData.status = status;
|
|
194
|
+
if (priority !== undefined) {
|
|
195
|
+
updateData.priority = priority === null ? null : priority;
|
|
196
|
+
}
|
|
197
|
+
if (dueDate !== undefined) {
|
|
198
|
+
updateData.due_date = dueDate ? parseDueDate(dueDate) : null;
|
|
199
|
+
if (dueDate && updateData.due_date) {
|
|
200
|
+
updateData.due_date_time = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Update the task
|
|
204
|
+
const updatedTask = await taskService.updateTask(targetTaskId, updateData);
|
|
205
|
+
// Format response
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: JSON.stringify({
|
|
210
|
+
id: updatedTask.id,
|
|
211
|
+
name: updatedTask.name,
|
|
212
|
+
url: updatedTask.url,
|
|
213
|
+
status: updatedTask.status?.status || "Unknown",
|
|
214
|
+
updated: true
|
|
215
|
+
}, null, 2)
|
|
216
|
+
}]
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Tool definition for moving a task
|
|
222
|
+
*/
|
|
223
|
+
export const moveTaskTool = {
|
|
224
|
+
name: "move_task",
|
|
225
|
+
description: "Move a task to a different list. Valid parameter combinations:\n1. Use taskId + (listId or listName) - preferred\n2. Use taskName + sourceListName + (listId or listName)\n\nWARNING: Task statuses may reset if destination list has different status options.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
taskId: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "ID of the task to move (preferred). Use this instead of taskName if you have it."
|
|
232
|
+
},
|
|
233
|
+
taskName: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Name of the task to move. When using this, you MUST also provide sourceListName."
|
|
236
|
+
},
|
|
237
|
+
sourceListName: {
|
|
238
|
+
type: "string",
|
|
239
|
+
description: "REQUIRED with taskName: Current list containing the task."
|
|
240
|
+
},
|
|
241
|
+
listId: {
|
|
242
|
+
type: "string",
|
|
243
|
+
description: "ID of destination list (preferred). Use this instead of listName if you have it."
|
|
244
|
+
},
|
|
245
|
+
listName: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "Name of destination list. Only use if you don't have listId."
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
required: []
|
|
251
|
+
},
|
|
252
|
+
async handler({ taskId, taskName, sourceListName, listId, listName }) {
|
|
253
|
+
let targetTaskId = taskId;
|
|
254
|
+
let targetListId = listId;
|
|
255
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
256
|
+
if (!targetTaskId && taskName) {
|
|
257
|
+
// First find the source list ID if sourceListName is provided
|
|
258
|
+
let sourceListId;
|
|
259
|
+
if (sourceListName) {
|
|
260
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
261
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
|
|
262
|
+
if (!listInfo) {
|
|
263
|
+
throw new Error(`Source list "${sourceListName}" not found`);
|
|
264
|
+
}
|
|
265
|
+
sourceListId = listInfo.id;
|
|
266
|
+
}
|
|
267
|
+
// Now find the task
|
|
268
|
+
const tasks = await taskService.getTasks(sourceListId || '');
|
|
269
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
270
|
+
if (!foundTask) {
|
|
271
|
+
throw new Error(`Task "${taskName}" not found${sourceListName ? ` in list "${sourceListName}"` : ""}`);
|
|
272
|
+
}
|
|
273
|
+
targetTaskId = foundTask.id;
|
|
274
|
+
}
|
|
275
|
+
if (!targetTaskId) {
|
|
276
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
277
|
+
}
|
|
278
|
+
// If no listId but listName is provided, look up the list ID
|
|
279
|
+
if (!targetListId && listName) {
|
|
280
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
281
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
282
|
+
if (!listInfo) {
|
|
283
|
+
throw new Error(`List "${listName}" not found`);
|
|
284
|
+
}
|
|
285
|
+
targetListId = listInfo.id;
|
|
286
|
+
}
|
|
287
|
+
if (!targetListId) {
|
|
288
|
+
throw new Error("Either listId or listName must be provided");
|
|
289
|
+
}
|
|
290
|
+
// Move the task
|
|
291
|
+
const movedTask = await taskService.moveTask(targetTaskId, targetListId);
|
|
292
|
+
// Format response
|
|
293
|
+
return {
|
|
294
|
+
content: [{
|
|
295
|
+
type: "text",
|
|
296
|
+
text: JSON.stringify({
|
|
297
|
+
id: movedTask.id,
|
|
298
|
+
name: movedTask.name,
|
|
299
|
+
url: movedTask.url,
|
|
300
|
+
status: movedTask.status?.status || "Unknown",
|
|
301
|
+
list: movedTask.list.name,
|
|
302
|
+
moved: true
|
|
303
|
+
}, null, 2)
|
|
304
|
+
}]
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Tool definition for duplicating a task
|
|
310
|
+
*/
|
|
311
|
+
export const duplicateTaskTool = {
|
|
312
|
+
name: "duplicate_task",
|
|
313
|
+
description: "Create a copy of a task in the same or different list. Valid parameter combinations:\n1. Use taskId + optional (listId or listName) - preferred\n2. Use taskName + sourceListName + optional (listId or listName)\n\nThe duplicate preserves the original task's properties.",
|
|
314
|
+
inputSchema: {
|
|
315
|
+
type: "object",
|
|
316
|
+
properties: {
|
|
317
|
+
taskId: {
|
|
318
|
+
type: "string",
|
|
319
|
+
description: "ID of task to duplicate (preferred). Use this instead of taskName if you have it."
|
|
320
|
+
},
|
|
321
|
+
taskName: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "Name of task to duplicate. When using this, you MUST provide sourceListName."
|
|
324
|
+
},
|
|
325
|
+
sourceListName: {
|
|
326
|
+
type: "string",
|
|
327
|
+
description: "REQUIRED with taskName: List containing the original task."
|
|
328
|
+
},
|
|
329
|
+
listId: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "ID of list for the duplicate (optional). Defaults to same list as original."
|
|
332
|
+
},
|
|
333
|
+
listName: {
|
|
334
|
+
type: "string",
|
|
335
|
+
description: "Name of list for the duplicate. Only use if you don't have listId."
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
required: []
|
|
339
|
+
},
|
|
340
|
+
async handler({ taskId, taskName, sourceListName, listId, listName }) {
|
|
341
|
+
let targetTaskId = taskId;
|
|
342
|
+
let sourceListId;
|
|
343
|
+
// If sourceListName is provided, find the source list ID
|
|
344
|
+
if (sourceListName) {
|
|
345
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
346
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
|
|
347
|
+
if (!listInfo) {
|
|
348
|
+
throw new Error(`Source list "${sourceListName}" not found`);
|
|
349
|
+
}
|
|
350
|
+
sourceListId = listInfo.id;
|
|
351
|
+
}
|
|
352
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
353
|
+
if (!targetTaskId && taskName) {
|
|
354
|
+
// Find the task in the source list if specified, otherwise search all tasks
|
|
355
|
+
if (sourceListId) {
|
|
356
|
+
const tasks = await taskService.getTasks(sourceListId);
|
|
357
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
358
|
+
if (!foundTask) {
|
|
359
|
+
throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
|
|
360
|
+
}
|
|
361
|
+
targetTaskId = foundTask.id;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// Without a source list, we need to search more broadly
|
|
365
|
+
throw new Error("When using taskName, sourceListName must be provided to find the task");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (!targetTaskId) {
|
|
369
|
+
throw new Error("Either taskId or taskName (with sourceListName) must be provided");
|
|
370
|
+
}
|
|
371
|
+
let targetListId = listId;
|
|
372
|
+
// If no listId but listName is provided, look up the list ID
|
|
373
|
+
if (!targetListId && listName) {
|
|
374
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
375
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
376
|
+
if (!listInfo) {
|
|
377
|
+
throw new Error(`Target list "${listName}" not found`);
|
|
378
|
+
}
|
|
379
|
+
targetListId = listInfo.id;
|
|
380
|
+
}
|
|
381
|
+
// Duplicate the task
|
|
382
|
+
const task = await taskService.duplicateTask(targetTaskId, targetListId);
|
|
383
|
+
// Format response
|
|
384
|
+
return {
|
|
385
|
+
content: [{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: JSON.stringify({
|
|
388
|
+
id: task.id,
|
|
389
|
+
name: task.name,
|
|
390
|
+
url: task.url,
|
|
391
|
+
duplicated: true,
|
|
392
|
+
list: task.list.name,
|
|
393
|
+
space: task.space.name,
|
|
394
|
+
folder: task.folder?.name
|
|
395
|
+
}, null, 2)
|
|
396
|
+
}]
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* Tool definition for getting task details
|
|
402
|
+
*/
|
|
403
|
+
export const getTaskTool = {
|
|
404
|
+
name: "get_task",
|
|
405
|
+
description: "Retrieve detailed information about a specific task. Valid parameter combinations:\n1. Use taskId alone (preferred)\n2. Use taskName + optional listName (to disambiguate if multiple tasks have the same name)",
|
|
406
|
+
inputSchema: {
|
|
407
|
+
type: "object",
|
|
408
|
+
properties: {
|
|
409
|
+
taskId: {
|
|
410
|
+
type: "string",
|
|
411
|
+
description: "ID of task to retrieve (preferred). Use this instead of taskName if you have it."
|
|
412
|
+
},
|
|
413
|
+
taskName: {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Name of task to retrieve. Warning: Task names may not be unique."
|
|
416
|
+
},
|
|
417
|
+
listName: {
|
|
418
|
+
type: "string",
|
|
419
|
+
description: "Name of list containing the task. Helps find the right task when using taskName."
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
required: []
|
|
423
|
+
},
|
|
424
|
+
async handler({ taskId, taskName, listName }) {
|
|
425
|
+
let targetTaskId = taskId;
|
|
426
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
427
|
+
if (!targetTaskId && taskName) {
|
|
428
|
+
let listId;
|
|
429
|
+
if (listName) {
|
|
430
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
431
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
432
|
+
if (!listInfo) {
|
|
433
|
+
throw new Error(`List "${listName}" not found`);
|
|
434
|
+
}
|
|
435
|
+
listId = listInfo.id;
|
|
436
|
+
}
|
|
437
|
+
// Now find the task
|
|
438
|
+
const tasks = await taskService.getTasks(listId || '');
|
|
439
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
440
|
+
if (!foundTask) {
|
|
441
|
+
throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
|
|
442
|
+
}
|
|
443
|
+
targetTaskId = foundTask.id;
|
|
444
|
+
}
|
|
445
|
+
if (!targetTaskId) {
|
|
446
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
447
|
+
}
|
|
448
|
+
// Get the task
|
|
449
|
+
const task = await taskService.getTask(targetTaskId);
|
|
450
|
+
// Format response
|
|
451
|
+
return {
|
|
452
|
+
content: [{
|
|
453
|
+
type: "text",
|
|
454
|
+
text: JSON.stringify({
|
|
455
|
+
id: task.id,
|
|
456
|
+
name: task.name,
|
|
457
|
+
description: task.description,
|
|
458
|
+
status: task.status?.status || "Unknown",
|
|
459
|
+
priority: task.priority,
|
|
460
|
+
due_date: task.due_date,
|
|
461
|
+
url: task.url,
|
|
462
|
+
list: task.list.name,
|
|
463
|
+
space: task.space.name,
|
|
464
|
+
folder: task.folder?.name,
|
|
465
|
+
creator: task.creator,
|
|
466
|
+
assignees: task.assignees,
|
|
467
|
+
tags: task.tags,
|
|
468
|
+
time_estimate: task.time_estimate,
|
|
469
|
+
time_spent: task.time_spent,
|
|
470
|
+
}, null, 2)
|
|
471
|
+
}]
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* Tool definition for getting tasks from a list
|
|
477
|
+
*/
|
|
478
|
+
export const getTasksTool = {
|
|
479
|
+
name: "get_tasks",
|
|
480
|
+
description: "Retrieve tasks from a list with optional filtering. You MUST provide either:\n1. listId (preferred)\n2. listName\n\nUse filters to narrow down results by status, dates, etc.",
|
|
481
|
+
inputSchema: {
|
|
482
|
+
type: "object",
|
|
483
|
+
properties: {
|
|
484
|
+
listId: {
|
|
485
|
+
type: "string",
|
|
486
|
+
description: "ID of list to get tasks from (preferred). Use this instead of listName if you have it."
|
|
487
|
+
},
|
|
488
|
+
listName: {
|
|
489
|
+
type: "string",
|
|
490
|
+
description: "Name of list to get tasks from. Only use if you don't have listId."
|
|
491
|
+
},
|
|
492
|
+
archived: {
|
|
493
|
+
type: "boolean",
|
|
494
|
+
description: "Include archived tasks"
|
|
495
|
+
},
|
|
496
|
+
page: {
|
|
497
|
+
type: "number",
|
|
498
|
+
description: "Page number for pagination (starts at 0)"
|
|
499
|
+
},
|
|
500
|
+
order_by: {
|
|
501
|
+
type: "string",
|
|
502
|
+
description: "Sort field: due_date, created, updated"
|
|
503
|
+
},
|
|
504
|
+
reverse: {
|
|
505
|
+
type: "boolean",
|
|
506
|
+
description: "Reverse sort order (descending)"
|
|
507
|
+
},
|
|
508
|
+
subtasks: {
|
|
509
|
+
type: "boolean",
|
|
510
|
+
description: "Include subtasks"
|
|
511
|
+
},
|
|
512
|
+
statuses: {
|
|
513
|
+
type: "array",
|
|
514
|
+
items: {
|
|
515
|
+
type: "string"
|
|
516
|
+
},
|
|
517
|
+
description: "Filter by status names (e.g. ['To Do', 'In Progress'])"
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
required: []
|
|
521
|
+
},
|
|
522
|
+
async handler({ listId, listName, archived, page, order_by, reverse, subtasks, statuses }) {
|
|
523
|
+
let targetListId = listId;
|
|
524
|
+
// If no listId but listName is provided, look up the list ID
|
|
525
|
+
if (!targetListId && listName) {
|
|
526
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
527
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
528
|
+
if (!listInfo) {
|
|
529
|
+
throw new Error(`List "${listName}" not found`);
|
|
530
|
+
}
|
|
531
|
+
targetListId = listInfo.id;
|
|
532
|
+
}
|
|
533
|
+
if (!targetListId) {
|
|
534
|
+
throw new Error("Either listId or listName must be provided");
|
|
535
|
+
}
|
|
536
|
+
// Prepare filter options - remove archived as it's not in TaskFilters
|
|
537
|
+
const filters = {
|
|
538
|
+
page,
|
|
539
|
+
order_by,
|
|
540
|
+
reverse,
|
|
541
|
+
subtasks,
|
|
542
|
+
statuses
|
|
543
|
+
};
|
|
544
|
+
// Get tasks with filters
|
|
545
|
+
const tasks = await taskService.getTasks(targetListId, filters);
|
|
546
|
+
// Format response
|
|
547
|
+
return {
|
|
548
|
+
content: [{
|
|
549
|
+
type: "text",
|
|
550
|
+
text: JSON.stringify({
|
|
551
|
+
list_id: targetListId,
|
|
552
|
+
task_count: tasks.length,
|
|
553
|
+
tasks: tasks.map((task) => ({
|
|
554
|
+
id: task.id,
|
|
555
|
+
name: task.name,
|
|
556
|
+
status: task.status?.status || "Unknown",
|
|
557
|
+
priority: task.priority,
|
|
558
|
+
due_date: task.due_date,
|
|
559
|
+
url: task.url
|
|
560
|
+
}))
|
|
561
|
+
}, null, 2)
|
|
562
|
+
}]
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
/**
|
|
567
|
+
* Tool definition for deleting a task
|
|
568
|
+
*/
|
|
569
|
+
export const deleteTaskTool = {
|
|
570
|
+
name: "delete_task",
|
|
571
|
+
description: "\u26a0\ufe0f PERMANENTLY DELETE a task. This action cannot be undone. Valid parameter combinations:\n1. Use taskId alone (preferred and safest)\n2. Use taskName + optional listName (use with caution)",
|
|
572
|
+
inputSchema: {
|
|
573
|
+
type: "object",
|
|
574
|
+
properties: {
|
|
575
|
+
taskId: {
|
|
576
|
+
type: "string",
|
|
577
|
+
description: "ID of task to delete (preferred). Use this instead of taskName for safety."
|
|
578
|
+
},
|
|
579
|
+
taskName: {
|
|
580
|
+
type: "string",
|
|
581
|
+
description: "Name of task to delete. Use with extreme caution as names may not be unique."
|
|
582
|
+
},
|
|
583
|
+
listName: {
|
|
584
|
+
type: "string",
|
|
585
|
+
description: "Name of list containing the task. Helps ensure correct task deletion when using taskName."
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
/**
|
|
591
|
+
* Tool definition for deleting multiple tasks
|
|
592
|
+
*/
|
|
593
|
+
export const deleteBulkTasksTool = {
|
|
594
|
+
name: "delete_bulk_tasks",
|
|
595
|
+
description: "\u26a0\ufe0f PERMANENTLY DELETE multiple tasks. This action cannot be undone. For each task, you MUST provide either:\n1. taskId alone (preferred and safest)\n2. taskName + listName (use with caution)",
|
|
596
|
+
inputSchema: {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
tasks: {
|
|
600
|
+
type: "array",
|
|
601
|
+
description: "Array of tasks to delete",
|
|
602
|
+
items: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
taskId: {
|
|
606
|
+
type: "string",
|
|
607
|
+
description: "Task ID (preferred). Use instead of taskName if available."
|
|
608
|
+
},
|
|
609
|
+
taskName: {
|
|
610
|
+
type: "string",
|
|
611
|
+
description: "Task name. Requires listName when used."
|
|
612
|
+
},
|
|
613
|
+
listName: {
|
|
614
|
+
type: "string",
|
|
615
|
+
description: "REQUIRED with taskName: List containing the task."
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
required: ["tasks"]
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
/**
|
|
625
|
+
* Tool definition for creating multiple tasks at once
|
|
626
|
+
*/
|
|
627
|
+
export const createBulkTasksTool = {
|
|
628
|
+
name: "create_bulk_tasks",
|
|
629
|
+
description: "Create multiple tasks in a list efficiently. You MUST provide:\n1. An array of tasks with required properties\n2. Either listId or listName to specify the target list\n\nOptional: Configure batch size and concurrency for performance.",
|
|
630
|
+
inputSchema: {
|
|
631
|
+
type: "object",
|
|
632
|
+
properties: {
|
|
633
|
+
listId: {
|
|
634
|
+
type: "string",
|
|
635
|
+
description: "ID of list for new tasks (preferred). Use this instead of listName if you have it."
|
|
636
|
+
},
|
|
637
|
+
listName: {
|
|
638
|
+
type: "string",
|
|
639
|
+
description: "Name of list for new tasks. Only use if you don't have listId."
|
|
640
|
+
},
|
|
641
|
+
tasks: {
|
|
642
|
+
type: "array",
|
|
643
|
+
description: "Array of tasks to create. Each task must have at least a name.",
|
|
644
|
+
items: {
|
|
645
|
+
type: "object",
|
|
646
|
+
properties: {
|
|
647
|
+
name: {
|
|
648
|
+
type: "string",
|
|
649
|
+
description: "Task name with emoji prefix"
|
|
650
|
+
},
|
|
651
|
+
description: {
|
|
652
|
+
type: "string",
|
|
653
|
+
description: "Plain text description"
|
|
654
|
+
},
|
|
655
|
+
markdown_description: {
|
|
656
|
+
type: "string",
|
|
657
|
+
description: "Markdown description (overrides plain text)"
|
|
658
|
+
},
|
|
659
|
+
status: {
|
|
660
|
+
type: "string",
|
|
661
|
+
description: "Task status (uses list default if omitted)"
|
|
662
|
+
},
|
|
663
|
+
priority: {
|
|
664
|
+
type: "number",
|
|
665
|
+
description: "Priority 1-4 (1=urgent, 4=low)"
|
|
666
|
+
},
|
|
667
|
+
dueDate: {
|
|
668
|
+
type: "string",
|
|
669
|
+
description: "Due date (Unix timestamp ms)"
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
required: ["name"]
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
options: {
|
|
676
|
+
type: "object",
|
|
677
|
+
description: "Optional processing settings",
|
|
678
|
+
properties: {
|
|
679
|
+
batchSize: {
|
|
680
|
+
type: "number",
|
|
681
|
+
description: "Tasks per batch (default: 10)"
|
|
682
|
+
},
|
|
683
|
+
concurrency: {
|
|
684
|
+
type: "number",
|
|
685
|
+
description: "Parallel operations (default: 1)"
|
|
686
|
+
},
|
|
687
|
+
continueOnError: {
|
|
688
|
+
type: "boolean",
|
|
689
|
+
description: "Continue if some tasks fail"
|
|
690
|
+
},
|
|
691
|
+
retryCount: {
|
|
692
|
+
type: "number",
|
|
693
|
+
description: "Retry attempts for failures"
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
required: ["tasks"]
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* Tool definition for updating multiple tasks
|
|
703
|
+
*/
|
|
704
|
+
export const updateBulkTasksTool = {
|
|
705
|
+
name: "update_bulk_tasks",
|
|
706
|
+
description: "Update multiple tasks efficiently. For each task, you MUST provide either:\n1. taskId alone (preferred)\n2. taskName + listName\n\nOnly specified fields will be updated for each task.",
|
|
707
|
+
inputSchema: {
|
|
708
|
+
type: "object",
|
|
709
|
+
properties: {
|
|
710
|
+
tasks: {
|
|
711
|
+
type: "array",
|
|
712
|
+
description: "Array of tasks to update",
|
|
713
|
+
items: {
|
|
714
|
+
type: "object",
|
|
715
|
+
properties: {
|
|
716
|
+
taskId: {
|
|
717
|
+
type: "string",
|
|
718
|
+
description: "Task ID (preferred). Use instead of taskName if available."
|
|
719
|
+
},
|
|
720
|
+
taskName: {
|
|
721
|
+
type: "string",
|
|
722
|
+
description: "Task name. Requires listName when used."
|
|
723
|
+
},
|
|
724
|
+
listName: {
|
|
725
|
+
type: "string",
|
|
726
|
+
description: "REQUIRED with taskName: List containing the task."
|
|
727
|
+
},
|
|
728
|
+
name: {
|
|
729
|
+
type: "string",
|
|
730
|
+
description: "New name with emoji prefix"
|
|
731
|
+
},
|
|
732
|
+
description: {
|
|
733
|
+
type: "string",
|
|
734
|
+
description: "New plain text description"
|
|
735
|
+
},
|
|
736
|
+
markdown_description: {
|
|
737
|
+
type: "string",
|
|
738
|
+
description: "New markdown description"
|
|
739
|
+
},
|
|
740
|
+
status: {
|
|
741
|
+
type: "string",
|
|
742
|
+
description: "New status"
|
|
743
|
+
},
|
|
744
|
+
priority: {
|
|
745
|
+
type: ["number", "null"],
|
|
746
|
+
enum: [1, 2, 3, 4, null],
|
|
747
|
+
description: "New priority (1-4 or null)"
|
|
748
|
+
},
|
|
749
|
+
dueDate: {
|
|
750
|
+
type: "string",
|
|
751
|
+
description: "New due date (Unix timestamp in milliseconds)"
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
required: ["tasks"]
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
/**
|
|
761
|
+
* Tool definition for moving multiple tasks
|
|
762
|
+
*/
|
|
763
|
+
export const moveBulkTasksTool = {
|
|
764
|
+
name: "move_bulk_tasks",
|
|
765
|
+
description: "Move multiple tasks to a different list efficiently. For each task, you MUST provide either:\n1. taskId alone (preferred)\n2. taskName + listName\n\nWARNING: Task statuses may reset if target list has different status options.",
|
|
766
|
+
inputSchema: {
|
|
767
|
+
type: "object",
|
|
768
|
+
properties: {
|
|
769
|
+
tasks: {
|
|
770
|
+
type: "array",
|
|
771
|
+
description: "Array of tasks to move",
|
|
772
|
+
items: {
|
|
773
|
+
type: "object",
|
|
774
|
+
properties: {
|
|
775
|
+
taskId: {
|
|
776
|
+
type: "string",
|
|
777
|
+
description: "Task ID (preferred). Use instead of taskName if available."
|
|
778
|
+
},
|
|
779
|
+
taskName: {
|
|
780
|
+
type: "string",
|
|
781
|
+
description: "Task name. Requires listName when used."
|
|
782
|
+
},
|
|
783
|
+
listName: {
|
|
784
|
+
type: "string",
|
|
785
|
+
description: "REQUIRED with taskName: List containing the task."
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
targetListId: {
|
|
791
|
+
type: "string",
|
|
792
|
+
description: "ID of destination list (preferred). Use instead of targetListName if available."
|
|
793
|
+
},
|
|
794
|
+
targetListName: {
|
|
795
|
+
type: "string",
|
|
796
|
+
description: "Name of destination list. Only use if you don't have targetListId."
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
required: ["tasks"]
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* Handler for bulk task updates
|
|
804
|
+
*/
|
|
805
|
+
export async function handleUpdateBulkTasks({ tasks }) {
|
|
806
|
+
if (!tasks || !tasks.length) {
|
|
807
|
+
throw new Error("No tasks provided for bulk update");
|
|
808
|
+
}
|
|
809
|
+
const results = {
|
|
810
|
+
total: tasks.length,
|
|
811
|
+
successful: 0,
|
|
812
|
+
failed: 0,
|
|
813
|
+
failures: []
|
|
814
|
+
};
|
|
815
|
+
for (const task of tasks) {
|
|
816
|
+
try {
|
|
817
|
+
let taskId = task.taskId;
|
|
818
|
+
if (!taskId && task.taskName) {
|
|
819
|
+
if (!task.listName) {
|
|
820
|
+
throw new Error(`List name is required when using task name for task "${task.taskName}"`);
|
|
821
|
+
}
|
|
822
|
+
const listInfo = await findListIDByName(workspaceService, task.listName);
|
|
823
|
+
if (!listInfo) {
|
|
824
|
+
throw new Error(`List "${task.listName}" not found`);
|
|
825
|
+
}
|
|
826
|
+
const taskList = await taskService.getTasks(listInfo.id);
|
|
827
|
+
const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
|
|
828
|
+
if (!foundTask) {
|
|
829
|
+
throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
|
|
830
|
+
}
|
|
831
|
+
taskId = foundTask.id;
|
|
832
|
+
}
|
|
833
|
+
if (!taskId) {
|
|
834
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
835
|
+
}
|
|
836
|
+
await taskService.updateTask(taskId, {
|
|
837
|
+
name: task.name,
|
|
838
|
+
description: task.description,
|
|
839
|
+
markdown_description: task.markdown_description,
|
|
840
|
+
status: task.status,
|
|
841
|
+
priority: task.priority,
|
|
842
|
+
due_date: task.dueDate ? parseDueDate(task.dueDate) : undefined
|
|
843
|
+
});
|
|
844
|
+
results.successful++;
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
results.failed++;
|
|
848
|
+
results.failures.push({
|
|
849
|
+
task: task.taskId || task.taskName,
|
|
850
|
+
error: error.message
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
content: [{
|
|
856
|
+
type: "text",
|
|
857
|
+
text: JSON.stringify(results, null, 2)
|
|
858
|
+
}]
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Handler for bulk task creation
|
|
863
|
+
*/
|
|
864
|
+
export async function handleCreateBulkTasks(parameters) {
|
|
865
|
+
// Validate required parameters
|
|
866
|
+
const { tasks, listId, listName } = parameters;
|
|
867
|
+
if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
|
|
868
|
+
throw new Error('You must provide a non-empty array of tasks to create');
|
|
869
|
+
}
|
|
870
|
+
let targetListId = listId;
|
|
871
|
+
// If no listId but listName is provided, look up the list ID
|
|
872
|
+
if (!targetListId && listName) {
|
|
873
|
+
const listInfo = await findListIDByName(workspaceService, listName);
|
|
874
|
+
if (!listInfo) {
|
|
875
|
+
throw new Error(`List "${listName}" not found`);
|
|
876
|
+
}
|
|
877
|
+
targetListId = listInfo.id;
|
|
878
|
+
}
|
|
879
|
+
if (!targetListId) {
|
|
880
|
+
throw new Error("Either listId or listName must be provided");
|
|
881
|
+
}
|
|
882
|
+
const results = {
|
|
883
|
+
total: tasks.length,
|
|
884
|
+
successful: 0,
|
|
885
|
+
failed: 0,
|
|
886
|
+
failures: []
|
|
887
|
+
};
|
|
888
|
+
// Map tasks to ClickUp format
|
|
889
|
+
const clickupTasks = tasks.map((task) => {
|
|
890
|
+
const taskData = {
|
|
891
|
+
name: task.name,
|
|
892
|
+
description: task.description,
|
|
893
|
+
markdown_description: task.markdown_description,
|
|
894
|
+
status: task.status,
|
|
895
|
+
priority: task.priority,
|
|
896
|
+
due_date: task.dueDate ? parseDueDate(task.dueDate) : undefined
|
|
897
|
+
};
|
|
898
|
+
// Add due_date_time flag if due date is set
|
|
899
|
+
if (task.dueDate && taskData.due_date) {
|
|
900
|
+
taskData.due_date_time = true;
|
|
901
|
+
}
|
|
902
|
+
return taskData;
|
|
903
|
+
});
|
|
904
|
+
// Create tasks in bulk using the task service
|
|
905
|
+
try {
|
|
906
|
+
const bulkResult = await taskService.createBulkTasks(targetListId, { tasks: clickupTasks });
|
|
907
|
+
// Update results based on bulk operation outcome
|
|
908
|
+
results.successful = bulkResult.successfulItems.length;
|
|
909
|
+
results.failed = bulkResult.failedItems.length;
|
|
910
|
+
results.failures = bulkResult.failedItems.map(failure => ({
|
|
911
|
+
task: failure.item.name,
|
|
912
|
+
error: failure.error.message
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
// If the bulk operation itself fails, mark all tasks as failed
|
|
917
|
+
results.failed = tasks.length;
|
|
918
|
+
results.failures = tasks.map(task => ({
|
|
919
|
+
task: task.name,
|
|
920
|
+
error: error.message
|
|
921
|
+
}));
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
content: [{
|
|
925
|
+
type: "text",
|
|
926
|
+
text: JSON.stringify(results, null, 2)
|
|
927
|
+
}]
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Handler for the create_task tool
|
|
932
|
+
*/
|
|
933
|
+
export async function handleCreateTask(parameters) {
|
|
934
|
+
const { name, description, markdown_description, listId, listName, status, priority, dueDate } = parameters;
|
|
935
|
+
// Validate required fields
|
|
936
|
+
if (!name) {
|
|
937
|
+
throw new Error("Task name is required");
|
|
938
|
+
}
|
|
939
|
+
let targetListId = listId;
|
|
940
|
+
// If no listId but listName is provided, look up the list ID
|
|
941
|
+
if (!targetListId && listName) {
|
|
942
|
+
// Use workspace service to find the list by name in the hierarchy
|
|
943
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
944
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
945
|
+
if (!listInfo) {
|
|
946
|
+
throw new Error(`List "${listName}" not found`);
|
|
947
|
+
}
|
|
948
|
+
targetListId = listInfo.id;
|
|
949
|
+
}
|
|
950
|
+
if (!targetListId) {
|
|
951
|
+
throw new Error("Either listId or listName must be provided");
|
|
952
|
+
}
|
|
953
|
+
// Prepare task data
|
|
954
|
+
const taskData = {
|
|
955
|
+
name,
|
|
956
|
+
description,
|
|
957
|
+
markdown_description,
|
|
958
|
+
status,
|
|
959
|
+
priority: priority,
|
|
960
|
+
due_date: dueDate ? parseDueDate(dueDate) : undefined
|
|
961
|
+
};
|
|
962
|
+
// Add due_date_time flag if due date is set
|
|
963
|
+
if (dueDate && taskData.due_date) {
|
|
964
|
+
taskData.due_date_time = true;
|
|
965
|
+
}
|
|
966
|
+
// Create the task
|
|
967
|
+
const task = await taskService.createTask(targetListId, taskData);
|
|
968
|
+
// Format response
|
|
969
|
+
return {
|
|
970
|
+
content: [{
|
|
971
|
+
type: "text",
|
|
972
|
+
text: JSON.stringify({
|
|
973
|
+
id: task.id,
|
|
974
|
+
name: task.name,
|
|
975
|
+
url: task.url,
|
|
976
|
+
status: task.status?.status || "New",
|
|
977
|
+
list: task.list.name,
|
|
978
|
+
space: task.space.name,
|
|
979
|
+
folder: task.folder?.name
|
|
980
|
+
}, null, 2)
|
|
981
|
+
}]
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Handler for the update_task tool
|
|
986
|
+
*/
|
|
987
|
+
export async function handleUpdateTask(parameters) {
|
|
988
|
+
const { taskId, taskName, listName, name, description, markdown_description, status, priority, dueDate } = parameters;
|
|
989
|
+
let targetTaskId = taskId;
|
|
990
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
991
|
+
if (!targetTaskId && taskName) {
|
|
992
|
+
let listId;
|
|
993
|
+
// If listName is provided, find the list ID first
|
|
994
|
+
if (listName) {
|
|
995
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
996
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
997
|
+
if (!listInfo) {
|
|
998
|
+
throw new Error(`List "${listName}" not found`);
|
|
999
|
+
}
|
|
1000
|
+
listId = listInfo.id;
|
|
1001
|
+
}
|
|
1002
|
+
// Now find the task
|
|
1003
|
+
const tasks = await taskService.getTasks(listId || '');
|
|
1004
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
1005
|
+
if (!foundTask) {
|
|
1006
|
+
throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
|
|
1007
|
+
}
|
|
1008
|
+
targetTaskId = foundTask.id;
|
|
1009
|
+
}
|
|
1010
|
+
if (!targetTaskId) {
|
|
1011
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
1012
|
+
}
|
|
1013
|
+
// Prepare update data
|
|
1014
|
+
const updateData = {};
|
|
1015
|
+
if (name !== undefined)
|
|
1016
|
+
updateData.name = name;
|
|
1017
|
+
if (description !== undefined)
|
|
1018
|
+
updateData.description = description;
|
|
1019
|
+
if (markdown_description !== undefined)
|
|
1020
|
+
updateData.markdown_description = markdown_description;
|
|
1021
|
+
if (status !== undefined)
|
|
1022
|
+
updateData.status = status;
|
|
1023
|
+
if (priority !== undefined) {
|
|
1024
|
+
updateData.priority = priority === null ? null : priority;
|
|
1025
|
+
}
|
|
1026
|
+
if (dueDate !== undefined) {
|
|
1027
|
+
updateData.due_date = dueDate ? parseDueDate(dueDate) : null;
|
|
1028
|
+
if (dueDate && updateData.due_date) {
|
|
1029
|
+
updateData.due_date_time = true;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
// Update the task
|
|
1033
|
+
const task = await taskService.updateTask(targetTaskId, updateData);
|
|
1034
|
+
// Format response
|
|
1035
|
+
return {
|
|
1036
|
+
content: [{
|
|
1037
|
+
type: "text",
|
|
1038
|
+
text: JSON.stringify({
|
|
1039
|
+
id: task.id,
|
|
1040
|
+
name: task.name,
|
|
1041
|
+
url: task.url,
|
|
1042
|
+
status: task.status?.status || "Unknown",
|
|
1043
|
+
updated: true,
|
|
1044
|
+
list: task.list.name,
|
|
1045
|
+
space: task.space.name,
|
|
1046
|
+
folder: task.folder?.name
|
|
1047
|
+
}, null, 2)
|
|
1048
|
+
}]
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Handler for the move_task tool
|
|
1053
|
+
*/
|
|
1054
|
+
export async function handleMoveTask(parameters) {
|
|
1055
|
+
const { taskId, taskName, sourceListName, listId, listName } = parameters;
|
|
1056
|
+
let targetTaskId = taskId;
|
|
1057
|
+
let sourceListId;
|
|
1058
|
+
// If sourceListName is provided, find the source list ID
|
|
1059
|
+
if (sourceListName) {
|
|
1060
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
1061
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
|
|
1062
|
+
if (!listInfo) {
|
|
1063
|
+
throw new Error(`Source list "${sourceListName}" not found`);
|
|
1064
|
+
}
|
|
1065
|
+
sourceListId = listInfo.id;
|
|
1066
|
+
}
|
|
1067
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
1068
|
+
if (!targetTaskId && taskName) {
|
|
1069
|
+
// Find the task in the source list if specified, otherwise search all tasks
|
|
1070
|
+
if (sourceListId) {
|
|
1071
|
+
const tasks = await taskService.getTasks(sourceListId);
|
|
1072
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
1073
|
+
if (!foundTask) {
|
|
1074
|
+
throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
|
|
1075
|
+
}
|
|
1076
|
+
targetTaskId = foundTask.id;
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
// Without a source list, we need to search more broadly
|
|
1080
|
+
// This is less efficient but necessary if source list is unknown
|
|
1081
|
+
throw new Error("When using taskName, sourceListName must be provided to find the task");
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (!targetTaskId) {
|
|
1085
|
+
throw new Error("Either taskId or taskName (with sourceListName) must be provided");
|
|
1086
|
+
}
|
|
1087
|
+
let targetListId = listId;
|
|
1088
|
+
// If no listId but listName is provided, look up the list ID
|
|
1089
|
+
if (!targetListId && listName) {
|
|
1090
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
1091
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
1092
|
+
if (!listInfo) {
|
|
1093
|
+
throw new Error(`Target list "${listName}" not found`);
|
|
1094
|
+
}
|
|
1095
|
+
targetListId = listInfo.id;
|
|
1096
|
+
}
|
|
1097
|
+
if (!targetListId) {
|
|
1098
|
+
throw new Error("Either listId or listName must be provided for the target list");
|
|
1099
|
+
}
|
|
1100
|
+
// Move the task
|
|
1101
|
+
const task = await taskService.moveTask(targetTaskId, targetListId);
|
|
1102
|
+
// Format response
|
|
1103
|
+
return {
|
|
1104
|
+
content: [{
|
|
1105
|
+
type: "text",
|
|
1106
|
+
text: JSON.stringify({
|
|
1107
|
+
id: task.id,
|
|
1108
|
+
name: task.name,
|
|
1109
|
+
url: task.url,
|
|
1110
|
+
moved: true,
|
|
1111
|
+
list: task.list.name,
|
|
1112
|
+
space: task.space.name,
|
|
1113
|
+
folder: task.folder?.name
|
|
1114
|
+
}, null, 2)
|
|
1115
|
+
}]
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Handler for the duplicate_task tool
|
|
1120
|
+
*/
|
|
1121
|
+
export async function handleDuplicateTask(parameters) {
|
|
1122
|
+
const { taskId, taskName, sourceListName, listId, listName } = parameters;
|
|
1123
|
+
let targetTaskId = taskId;
|
|
1124
|
+
let sourceListId;
|
|
1125
|
+
// If sourceListName is provided, find the source list ID
|
|
1126
|
+
if (sourceListName) {
|
|
1127
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
1128
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
|
|
1129
|
+
if (!listInfo) {
|
|
1130
|
+
throw new Error(`Source list "${sourceListName}" not found`);
|
|
1131
|
+
}
|
|
1132
|
+
sourceListId = listInfo.id;
|
|
1133
|
+
}
|
|
1134
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
1135
|
+
if (!targetTaskId && taskName) {
|
|
1136
|
+
// Find the task in the source list if specified, otherwise search all tasks
|
|
1137
|
+
if (sourceListId) {
|
|
1138
|
+
const tasks = await taskService.getTasks(sourceListId);
|
|
1139
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
1140
|
+
if (!foundTask) {
|
|
1141
|
+
throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
|
|
1142
|
+
}
|
|
1143
|
+
targetTaskId = foundTask.id;
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
// Without a source list, we need to search more broadly
|
|
1147
|
+
throw new Error("When using taskName, sourceListName must be provided to find the task");
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
if (!targetTaskId) {
|
|
1151
|
+
throw new Error("Either taskId or taskName (with sourceListName) must be provided");
|
|
1152
|
+
}
|
|
1153
|
+
let targetListId = listId;
|
|
1154
|
+
// If no listId but listName is provided, look up the list ID
|
|
1155
|
+
if (!targetListId && listName) {
|
|
1156
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
1157
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
1158
|
+
if (!listInfo) {
|
|
1159
|
+
throw new Error(`Target list "${listName}" not found`);
|
|
1160
|
+
}
|
|
1161
|
+
targetListId = listInfo.id;
|
|
1162
|
+
}
|
|
1163
|
+
// Duplicate the task
|
|
1164
|
+
const task = await taskService.duplicateTask(targetTaskId, targetListId);
|
|
1165
|
+
// Format response
|
|
1166
|
+
return {
|
|
1167
|
+
content: [{
|
|
1168
|
+
type: "text",
|
|
1169
|
+
text: JSON.stringify({
|
|
1170
|
+
id: task.id,
|
|
1171
|
+
name: task.name,
|
|
1172
|
+
url: task.url,
|
|
1173
|
+
duplicated: true,
|
|
1174
|
+
list: task.list.name,
|
|
1175
|
+
space: task.space.name,
|
|
1176
|
+
folder: task.folder?.name
|
|
1177
|
+
}, null, 2)
|
|
1178
|
+
}]
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Handler for the get_tasks tool
|
|
1183
|
+
*/
|
|
1184
|
+
export async function handleGetTasks(parameters) {
|
|
1185
|
+
const { listId, listName, archived, page, order_by, reverse, subtasks, statuses, include_closed, assignees, due_date_gt, due_date_lt, date_created_gt, date_created_lt, date_updated_gt, date_updated_lt, custom_fields } = parameters;
|
|
1186
|
+
let targetListId = listId;
|
|
1187
|
+
// If no listId but listName is provided, look up the list ID
|
|
1188
|
+
if (!targetListId && listName) {
|
|
1189
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
1190
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
1191
|
+
if (!listInfo) {
|
|
1192
|
+
throw new Error(`List "${listName}" not found`);
|
|
1193
|
+
}
|
|
1194
|
+
targetListId = listInfo.id;
|
|
1195
|
+
}
|
|
1196
|
+
if (!targetListId) {
|
|
1197
|
+
throw new Error("Either listId or listName must be provided");
|
|
1198
|
+
}
|
|
1199
|
+
// Prepare filter options - remove archived as it's not in TaskFilters
|
|
1200
|
+
const filters = {
|
|
1201
|
+
page,
|
|
1202
|
+
order_by,
|
|
1203
|
+
reverse,
|
|
1204
|
+
subtasks,
|
|
1205
|
+
statuses
|
|
1206
|
+
};
|
|
1207
|
+
// Get tasks with filters
|
|
1208
|
+
const tasks = await taskService.getTasks(targetListId, filters);
|
|
1209
|
+
// Format response
|
|
1210
|
+
return {
|
|
1211
|
+
content: [{
|
|
1212
|
+
type: "text",
|
|
1213
|
+
text: JSON.stringify({
|
|
1214
|
+
list_id: targetListId,
|
|
1215
|
+
task_count: tasks.length,
|
|
1216
|
+
tasks: tasks.map((task) => ({
|
|
1217
|
+
id: task.id,
|
|
1218
|
+
name: task.name,
|
|
1219
|
+
status: task.status?.status || "Unknown",
|
|
1220
|
+
priority: task.priority,
|
|
1221
|
+
due_date: task.due_date,
|
|
1222
|
+
url: task.url
|
|
1223
|
+
}))
|
|
1224
|
+
}, null, 2)
|
|
1225
|
+
}]
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Handler for the get_task tool
|
|
1230
|
+
*/
|
|
1231
|
+
export async function handleGetTask(parameters) {
|
|
1232
|
+
const { taskId, taskName, listName } = parameters;
|
|
1233
|
+
let targetTaskId = taskId;
|
|
1234
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
1235
|
+
if (!targetTaskId && taskName) {
|
|
1236
|
+
let listId;
|
|
1237
|
+
// If listName is provided, find the list ID first
|
|
1238
|
+
if (listName) {
|
|
1239
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
1240
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
1241
|
+
if (!listInfo) {
|
|
1242
|
+
throw new Error(`List "${listName}" not found`);
|
|
1243
|
+
}
|
|
1244
|
+
listId = listInfo.id;
|
|
1245
|
+
}
|
|
1246
|
+
// Now find the task
|
|
1247
|
+
const tasks = await taskService.getTasks(listId || '');
|
|
1248
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
1249
|
+
if (!foundTask) {
|
|
1250
|
+
throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
|
|
1251
|
+
}
|
|
1252
|
+
targetTaskId = foundTask.id;
|
|
1253
|
+
}
|
|
1254
|
+
if (!targetTaskId) {
|
|
1255
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
1256
|
+
}
|
|
1257
|
+
// Get the task
|
|
1258
|
+
const task = await taskService.getTask(targetTaskId);
|
|
1259
|
+
// Format response
|
|
1260
|
+
return {
|
|
1261
|
+
content: [{
|
|
1262
|
+
type: "text",
|
|
1263
|
+
text: JSON.stringify(task, null, 2)
|
|
1264
|
+
}]
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Handler for the delete_task tool
|
|
1269
|
+
*/
|
|
1270
|
+
export async function handleDeleteTask(parameters) {
|
|
1271
|
+
const { taskId, taskName, listName } = parameters;
|
|
1272
|
+
let targetTaskId = taskId;
|
|
1273
|
+
// If no taskId but taskName is provided, look up the task ID
|
|
1274
|
+
if (!targetTaskId && taskName) {
|
|
1275
|
+
let listId;
|
|
1276
|
+
// If listName is provided, find the list ID first
|
|
1277
|
+
if (listName) {
|
|
1278
|
+
const hierarchy = await workspaceService.getWorkspaceHierarchy();
|
|
1279
|
+
const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
|
|
1280
|
+
if (!listInfo) {
|
|
1281
|
+
throw new Error(`List "${listName}" not found`);
|
|
1282
|
+
}
|
|
1283
|
+
listId = listInfo.id;
|
|
1284
|
+
}
|
|
1285
|
+
// Now find the task
|
|
1286
|
+
const tasks = await taskService.getTasks(listId || '');
|
|
1287
|
+
const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
|
|
1288
|
+
if (!foundTask) {
|
|
1289
|
+
throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
|
|
1290
|
+
}
|
|
1291
|
+
targetTaskId = foundTask.id;
|
|
1292
|
+
}
|
|
1293
|
+
if (!targetTaskId) {
|
|
1294
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
1295
|
+
}
|
|
1296
|
+
// Get task info before deleting (for the response)
|
|
1297
|
+
let taskInfo;
|
|
1298
|
+
try {
|
|
1299
|
+
taskInfo = await taskService.getTask(targetTaskId);
|
|
1300
|
+
}
|
|
1301
|
+
catch (error) {
|
|
1302
|
+
// If we can't get the task info, we'll continue with deletion anyway
|
|
1303
|
+
console.error("Error fetching task before deletion:", error);
|
|
1304
|
+
}
|
|
1305
|
+
// Delete the task
|
|
1306
|
+
await taskService.deleteTask(targetTaskId);
|
|
1307
|
+
// Format response
|
|
1308
|
+
return {
|
|
1309
|
+
content: [{
|
|
1310
|
+
type: "text",
|
|
1311
|
+
text: JSON.stringify({
|
|
1312
|
+
id: targetTaskId,
|
|
1313
|
+
name: taskInfo?.name || "Unknown",
|
|
1314
|
+
deleted: true,
|
|
1315
|
+
list: taskInfo?.list?.name || "Unknown",
|
|
1316
|
+
space: taskInfo?.space?.name || "Unknown"
|
|
1317
|
+
}, null, 2)
|
|
1318
|
+
}]
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Handler for the delete_bulk_tasks tool
|
|
1323
|
+
*/
|
|
1324
|
+
export async function handleDeleteBulkTasks({ tasks }) {
|
|
1325
|
+
if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
|
|
1326
|
+
throw new Error('You must provide a non-empty array of tasks to delete');
|
|
1327
|
+
}
|
|
1328
|
+
const results = {
|
|
1329
|
+
total: tasks.length,
|
|
1330
|
+
successful: 0,
|
|
1331
|
+
failed: 0,
|
|
1332
|
+
failures: [],
|
|
1333
|
+
deleted: []
|
|
1334
|
+
};
|
|
1335
|
+
// Collect all task IDs for deletion
|
|
1336
|
+
const taskIdsToDelete = [];
|
|
1337
|
+
const taskMap = new Map();
|
|
1338
|
+
// First, resolve all task IDs
|
|
1339
|
+
for (const task of tasks) {
|
|
1340
|
+
try {
|
|
1341
|
+
let taskId = task.taskId;
|
|
1342
|
+
if (!taskId && task.taskName) {
|
|
1343
|
+
if (!task.listName) {
|
|
1344
|
+
throw new Error(`List name is required when using task name for task "${task.taskName}"`);
|
|
1345
|
+
}
|
|
1346
|
+
const listInfo = await findListIDByName(workspaceService, task.listName);
|
|
1347
|
+
if (!listInfo) {
|
|
1348
|
+
throw new Error(`List "${task.listName}" not found`);
|
|
1349
|
+
}
|
|
1350
|
+
const taskList = await taskService.getTasks(listInfo.id);
|
|
1351
|
+
const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
|
|
1352
|
+
if (!foundTask) {
|
|
1353
|
+
throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
|
|
1354
|
+
}
|
|
1355
|
+
taskId = foundTask.id;
|
|
1356
|
+
// Store original task info for the response
|
|
1357
|
+
taskMap.set(taskId, { id: taskId, name: foundTask.name, originalTask: task });
|
|
1358
|
+
}
|
|
1359
|
+
else if (taskId) {
|
|
1360
|
+
// Store task ID with basic info for the response
|
|
1361
|
+
taskMap.set(taskId, { id: taskId, name: task.taskName || "Unknown", originalTask: task });
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
throw new Error("Either taskId or taskName must be provided for each task");
|
|
1365
|
+
}
|
|
1366
|
+
taskIdsToDelete.push(taskId);
|
|
1367
|
+
}
|
|
1368
|
+
catch (error) {
|
|
1369
|
+
results.failed++;
|
|
1370
|
+
results.failures.push({
|
|
1371
|
+
task: task.taskId || task.taskName,
|
|
1372
|
+
error: error.message
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
// Perform the bulk delete operation if we have tasks to delete
|
|
1377
|
+
if (taskIdsToDelete.length > 0) {
|
|
1378
|
+
try {
|
|
1379
|
+
const bulkResult = await taskService.deleteBulkTasks(taskIdsToDelete);
|
|
1380
|
+
// Process successful deletions
|
|
1381
|
+
for (const deletedId of bulkResult.successfulItems) {
|
|
1382
|
+
results.successful++;
|
|
1383
|
+
const taskInfo = taskMap.get(deletedId);
|
|
1384
|
+
results.deleted.push({
|
|
1385
|
+
id: deletedId,
|
|
1386
|
+
name: taskInfo?.name || "Unknown",
|
|
1387
|
+
deleted: true
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
// Process failed deletions
|
|
1391
|
+
for (const failure of bulkResult.failedItems) {
|
|
1392
|
+
results.failed++;
|
|
1393
|
+
const taskInfo = taskMap.get(failure.item);
|
|
1394
|
+
results.failures.push({
|
|
1395
|
+
task: taskInfo?.name || failure.item,
|
|
1396
|
+
error: failure.error.message
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
// If the bulk delete fails entirely, mark all remaining tasks as failed
|
|
1402
|
+
for (const taskId of taskIdsToDelete) {
|
|
1403
|
+
const taskInfo = taskMap.get(taskId);
|
|
1404
|
+
if (taskInfo && !results.deleted.some(t => t.id === taskId) &&
|
|
1405
|
+
!results.failures.some(f => f.task === taskId || f.task === taskInfo.name)) {
|
|
1406
|
+
results.failed++;
|
|
1407
|
+
results.failures.push({
|
|
1408
|
+
task: taskInfo.name || taskId,
|
|
1409
|
+
error: error.message
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return {
|
|
1416
|
+
content: [{
|
|
1417
|
+
type: "text",
|
|
1418
|
+
text: JSON.stringify(results, null, 2)
|
|
1419
|
+
}]
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Handler for bulk task moves
|
|
1424
|
+
*/
|
|
1425
|
+
export async function handleMoveBulkTasks(parameters) {
|
|
1426
|
+
const { tasks, targetListId, targetListName } = parameters;
|
|
1427
|
+
if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
|
|
1428
|
+
throw new Error('You must provide a non-empty array of tasks to move');
|
|
1429
|
+
}
|
|
1430
|
+
let finalTargetListId = targetListId;
|
|
1431
|
+
// If no targetListId but targetListName is provided, look up the list ID
|
|
1432
|
+
if (!finalTargetListId && targetListName) {
|
|
1433
|
+
const listInfo = await findListIDByName(workspaceService, targetListName);
|
|
1434
|
+
if (!listInfo) {
|
|
1435
|
+
throw new Error(`Target list "${targetListName}" not found`);
|
|
1436
|
+
}
|
|
1437
|
+
finalTargetListId = listInfo.id;
|
|
1438
|
+
}
|
|
1439
|
+
if (!finalTargetListId) {
|
|
1440
|
+
throw new Error("Either targetListId or targetListName must be provided");
|
|
1441
|
+
}
|
|
1442
|
+
const results = {
|
|
1443
|
+
total: tasks.length,
|
|
1444
|
+
successful: 0,
|
|
1445
|
+
failed: 0,
|
|
1446
|
+
failures: []
|
|
1447
|
+
};
|
|
1448
|
+
for (const task of tasks) {
|
|
1449
|
+
try {
|
|
1450
|
+
let taskId = task.taskId;
|
|
1451
|
+
if (!taskId && task.taskName) {
|
|
1452
|
+
if (!task.listName) {
|
|
1453
|
+
throw new Error(`List name is required when using task name for task "${task.taskName}"`);
|
|
1454
|
+
}
|
|
1455
|
+
const listInfo = await findListIDByName(workspaceService, task.listName);
|
|
1456
|
+
if (!listInfo) {
|
|
1457
|
+
throw new Error(`List "${task.listName}" not found`);
|
|
1458
|
+
}
|
|
1459
|
+
const taskList = await taskService.getTasks(listInfo.id);
|
|
1460
|
+
const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
|
|
1461
|
+
if (!foundTask) {
|
|
1462
|
+
throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
|
|
1463
|
+
}
|
|
1464
|
+
taskId = foundTask.id;
|
|
1465
|
+
}
|
|
1466
|
+
if (!taskId) {
|
|
1467
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
1468
|
+
}
|
|
1469
|
+
await taskService.moveTask(taskId, finalTargetListId);
|
|
1470
|
+
results.successful++;
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
results.failed++;
|
|
1474
|
+
results.failures.push({
|
|
1475
|
+
task: task.taskId || task.taskName,
|
|
1476
|
+
error: error.message
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
content: [{
|
|
1482
|
+
type: "text",
|
|
1483
|
+
text: JSON.stringify(results, null, 2)
|
|
1484
|
+
}]
|
|
1485
|
+
};
|
|
1486
|
+
}
|