@taazkareem/clickup-mcp-server 0.6.9 → 0.7.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/README.md +73 -51
- package/build/config.js +17 -1
- package/build/server.js +65 -6
- package/build/services/clickup/document.js +159 -0
- package/build/services/clickup/index.js +11 -1
- package/build/services/clickup/task/task-core.js +14 -2
- package/build/services/clickup/time.js +244 -0
- package/build/services/clickup/types.js +11 -0
- package/build/services/shared.js +1 -1
- package/build/tools/documents.js +501 -0
- package/build/tools/task/bulk-operations.js +2 -1
- package/build/tools/task/index.js +2 -0
- package/build/tools/task/single-operations.js +2 -1
- package/build/tools/task/time-tracking.js +684 -0
- package/build/utils/sponsor-service.js +1 -1
- package/package.json +1 -1
- package/build/mcp-tools.js +0 -64
- package/build/server-state.js +0 -93
- package/build/server.log +0 -76
- package/build/services/clickup/task/handlers.js +0 -1
- package/build/services/clickup/task.js +0 -976
- package/build/services/clickup/tools/tag.js +0 -149
- package/build/tools/bulk-tasks.js +0 -36
- package/build/tools/debug.js +0 -76
- package/build/tools/logs.js +0 -55
- package/build/tools/task.js +0 -1554
- package/build/utils/params-utils.js +0 -39
- package/build/utils/sponsor-analytics.js +0 -100
- package/build/utils/sponsor-utils.js +0 -57
|
@@ -1,976 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
-
* SPDX-License-Identifier: MIT
|
|
4
|
-
*
|
|
5
|
-
* ClickUp Task Service
|
|
6
|
-
*
|
|
7
|
-
* Handles all operations related to tasks in ClickUp, including:
|
|
8
|
-
* - Creating tasks (single and bulk)
|
|
9
|
-
* - Retrieving tasks (single or multiple)
|
|
10
|
-
* - Updating tasks
|
|
11
|
-
* - Deleting tasks
|
|
12
|
-
* - Finding tasks by name
|
|
13
|
-
*/
|
|
14
|
-
import { BaseClickUpService, ErrorCode, ClickUpServiceError } from './base.js';
|
|
15
|
-
import { ListService } from './list.js';
|
|
16
|
-
import { estimateTokensFromObject, wouldExceedTokenLimit } from '../../utils/token-utils.js';
|
|
17
|
-
import { isNameMatch } from '../../utils/resolver-utils.js';
|
|
18
|
-
import { findListIDByName } from '../../tools/list.js';
|
|
19
|
-
export class TaskService extends BaseClickUpService {
|
|
20
|
-
constructor(apiKey, teamId, baseUrl, workspaceService) {
|
|
21
|
-
super(apiKey, teamId, baseUrl);
|
|
22
|
-
this.workspaceService = null;
|
|
23
|
-
if (workspaceService) {
|
|
24
|
-
this.workspaceService = workspaceService;
|
|
25
|
-
this.logOperation('constructor', { usingSharedWorkspaceService: true });
|
|
26
|
-
}
|
|
27
|
-
// Initialize list service for list lookups
|
|
28
|
-
this.listService = new ListService(apiKey, teamId, baseUrl, this.workspaceService);
|
|
29
|
-
this.logOperation('constructor', { initialized: true });
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Helper method to handle errors consistently
|
|
33
|
-
* @param error The error that occurred
|
|
34
|
-
* @param message Optional custom error message
|
|
35
|
-
* @returns A ClickUpServiceError
|
|
36
|
-
*/
|
|
37
|
-
handleError(error, message) {
|
|
38
|
-
if (error instanceof ClickUpServiceError) {
|
|
39
|
-
return error;
|
|
40
|
-
}
|
|
41
|
-
return new ClickUpServiceError(message || `Task service error: ${error.message}`, ErrorCode.UNKNOWN, error);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Build URL parameters from task filters
|
|
45
|
-
* @param filters Task filters to convert to URL parameters
|
|
46
|
-
* @returns URLSearchParams object
|
|
47
|
-
*/
|
|
48
|
-
buildTaskFilterParams(filters) {
|
|
49
|
-
const params = new URLSearchParams();
|
|
50
|
-
// Add all filters to the query parameters
|
|
51
|
-
if (filters.include_closed)
|
|
52
|
-
params.append('include_closed', String(filters.include_closed));
|
|
53
|
-
if (filters.subtasks)
|
|
54
|
-
params.append('subtasks', String(filters.subtasks));
|
|
55
|
-
if (filters.page)
|
|
56
|
-
params.append('page', String(filters.page));
|
|
57
|
-
if (filters.order_by)
|
|
58
|
-
params.append('order_by', filters.order_by);
|
|
59
|
-
if (filters.reverse)
|
|
60
|
-
params.append('reverse', String(filters.reverse));
|
|
61
|
-
// Array parameters
|
|
62
|
-
if (filters.statuses && filters.statuses.length > 0) {
|
|
63
|
-
filters.statuses.forEach(status => params.append('statuses[]', status));
|
|
64
|
-
}
|
|
65
|
-
if (filters.assignees && filters.assignees.length > 0) {
|
|
66
|
-
filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
|
|
67
|
-
}
|
|
68
|
-
// Team tasks endpoint specific parameters
|
|
69
|
-
if (filters.tags && filters.tags.length > 0) {
|
|
70
|
-
filters.tags.forEach(tag => params.append('tags[]', tag));
|
|
71
|
-
}
|
|
72
|
-
if (filters.list_ids && filters.list_ids.length > 0) {
|
|
73
|
-
filters.list_ids.forEach(id => params.append('list_ids[]', id));
|
|
74
|
-
}
|
|
75
|
-
if (filters.folder_ids && filters.folder_ids.length > 0) {
|
|
76
|
-
filters.folder_ids.forEach(id => params.append('folder_ids[]', id));
|
|
77
|
-
}
|
|
78
|
-
if (filters.space_ids && filters.space_ids.length > 0) {
|
|
79
|
-
filters.space_ids.forEach(id => params.append('space_ids[]', id));
|
|
80
|
-
}
|
|
81
|
-
if (filters.archived !== undefined)
|
|
82
|
-
params.append('archived', String(filters.archived));
|
|
83
|
-
if (filters.include_closed_lists !== undefined)
|
|
84
|
-
params.append('include_closed_lists', String(filters.include_closed_lists));
|
|
85
|
-
if (filters.include_archived_lists !== undefined)
|
|
86
|
-
params.append('include_archived_lists', String(filters.include_archived_lists));
|
|
87
|
-
if (filters.include_compact_time_entries !== undefined)
|
|
88
|
-
params.append('include_compact_time_entries', String(filters.include_compact_time_entries));
|
|
89
|
-
// Date filters
|
|
90
|
-
if (filters.due_date_gt)
|
|
91
|
-
params.append('due_date_gt', String(filters.due_date_gt));
|
|
92
|
-
if (filters.due_date_lt)
|
|
93
|
-
params.append('due_date_lt', String(filters.due_date_lt));
|
|
94
|
-
if (filters.date_created_gt)
|
|
95
|
-
params.append('date_created_gt', String(filters.date_created_gt));
|
|
96
|
-
if (filters.date_created_lt)
|
|
97
|
-
params.append('date_created_lt', String(filters.date_created_lt));
|
|
98
|
-
if (filters.date_updated_gt)
|
|
99
|
-
params.append('date_updated_gt', String(filters.date_updated_gt));
|
|
100
|
-
if (filters.date_updated_lt)
|
|
101
|
-
params.append('date_updated_lt', String(filters.date_updated_lt));
|
|
102
|
-
// Handle custom fields if present
|
|
103
|
-
if (filters.custom_fields) {
|
|
104
|
-
Object.entries(filters.custom_fields).forEach(([key, value]) => {
|
|
105
|
-
params.append(`custom_fields[${key}]`, String(value));
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return params;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Extract priority value from a task
|
|
112
|
-
* @param task The task to extract priority from
|
|
113
|
-
* @returns TaskPriority or null
|
|
114
|
-
*/
|
|
115
|
-
extractPriorityValue(task) {
|
|
116
|
-
if (!task.priority || !task.priority.id) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
const priorityValue = parseInt(task.priority.id);
|
|
120
|
-
// Ensure it's in the valid range 1-4
|
|
121
|
-
if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
return priorityValue;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Extract task data for creation/duplication
|
|
128
|
-
* @param task The source task
|
|
129
|
-
* @param nameOverride Optional override for the task name
|
|
130
|
-
* @returns CreateTaskData object
|
|
131
|
-
*/
|
|
132
|
-
extractTaskData(task, nameOverride) {
|
|
133
|
-
return {
|
|
134
|
-
name: nameOverride || task.name,
|
|
135
|
-
description: task.description || '',
|
|
136
|
-
status: task.status?.status,
|
|
137
|
-
priority: this.extractPriorityValue(task),
|
|
138
|
-
due_date: task.due_date ? Number(task.due_date) : undefined,
|
|
139
|
-
assignees: task.assignees?.map(a => a.id) || []
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Create a new task in the specified list
|
|
144
|
-
* @param listId The ID of the list to create the task in
|
|
145
|
-
* @param taskData The data for the new task
|
|
146
|
-
* @returns The created task
|
|
147
|
-
*/
|
|
148
|
-
async createTask(listId, taskData) {
|
|
149
|
-
this.logOperation('createTask', { listId, ...taskData });
|
|
150
|
-
try {
|
|
151
|
-
return await this.makeRequest(async () => {
|
|
152
|
-
const response = await this.client.post(`/list/${listId}/task`, taskData);
|
|
153
|
-
return response.data;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
throw this.handleError(error, `Failed to create task in list ${listId}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Get all tasks in a list with optional filtering
|
|
162
|
-
* @param listId The ID of the list to get tasks from
|
|
163
|
-
* @param filters Optional filters to apply
|
|
164
|
-
* @returns List of tasks matching the filters
|
|
165
|
-
*/
|
|
166
|
-
async getTasks(listId, filters = {}) {
|
|
167
|
-
this.logOperation('getTasks', { listId, filters });
|
|
168
|
-
try {
|
|
169
|
-
return await this.makeRequest(async () => {
|
|
170
|
-
const params = this.buildTaskFilterParams(filters);
|
|
171
|
-
const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
|
|
172
|
-
return response.data.tasks;
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
throw this.handleError(error, `Failed to get tasks from list ${listId}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Get a specific task by ID
|
|
181
|
-
* @param taskId The ID of the task to retrieve
|
|
182
|
-
* @returns The task details
|
|
183
|
-
*/
|
|
184
|
-
async getTask(taskId) {
|
|
185
|
-
this.logOperation('getTask', { taskId });
|
|
186
|
-
try {
|
|
187
|
-
return await this.makeRequest(async () => {
|
|
188
|
-
const response = await this.client.get(`/task/${taskId}`);
|
|
189
|
-
return response.data;
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
throw this.handleError(error, `Failed to get task ${taskId}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Get subtasks of a specific task
|
|
198
|
-
* @param taskId The ID of the parent task
|
|
199
|
-
* @returns Array of subtask details
|
|
200
|
-
*/
|
|
201
|
-
async getSubtasks(taskId) {
|
|
202
|
-
this.logOperation('getSubtasks', { taskId });
|
|
203
|
-
try {
|
|
204
|
-
return await this.makeRequest(async () => {
|
|
205
|
-
// First, get the task to get its list ID
|
|
206
|
-
const task = await this.getTask(taskId);
|
|
207
|
-
const listId = task.list.id;
|
|
208
|
-
// Then get all tasks from the list
|
|
209
|
-
const allTasks = await this.getTasks(listId, { subtasks: true });
|
|
210
|
-
// Filter tasks that have the specified task as parent
|
|
211
|
-
return allTasks.filter(t => t.parent === taskId || t.top_level_parent === taskId);
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
throw this.handleError(error, `Failed to get subtasks of task ${taskId}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Get a specific task by its custom ID
|
|
220
|
-
* @param customTaskId The custom ID of the task (e.g., "DEV-1234")
|
|
221
|
-
* @param listId Optional list ID to search within for better performance
|
|
222
|
-
* @returns The task details
|
|
223
|
-
*/
|
|
224
|
-
async getTaskByCustomId(customTaskId, listId) {
|
|
225
|
-
this.logOperation('getTaskByCustomId', { customTaskId, listId });
|
|
226
|
-
try {
|
|
227
|
-
return await this.makeRequest(async () => {
|
|
228
|
-
// Build query with custom_task_ids=true and team_id
|
|
229
|
-
const params = new URLSearchParams({
|
|
230
|
-
custom_task_ids: 'true',
|
|
231
|
-
team_id: this.teamId
|
|
232
|
-
});
|
|
233
|
-
// Use the ClickUp API endpoint for retrieving tasks by ID
|
|
234
|
-
// With custom_task_ids=true parameter, it will treat the ID as a custom ID
|
|
235
|
-
const response = await this.client.get(`/task/${customTaskId}?${params.toString()}`);
|
|
236
|
-
return response.data;
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
// Enhance error message for custom ID lookups
|
|
241
|
-
if (error?.response?.status === 404) {
|
|
242
|
-
throw this.handleError(error, `Task with custom ID ${customTaskId} not found`);
|
|
243
|
-
}
|
|
244
|
-
throw this.handleError(error, `Failed to get task with custom ID ${customTaskId}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Update an existing task
|
|
249
|
-
* @param taskId ID of the task to update
|
|
250
|
-
* @param updateData Data to update on the task
|
|
251
|
-
* @returns The updated task
|
|
252
|
-
*/
|
|
253
|
-
async updateTask(taskId, updateData) {
|
|
254
|
-
this.logOperation('updateTask', { taskId, ...updateData });
|
|
255
|
-
try {
|
|
256
|
-
return await this.makeRequest(async () => {
|
|
257
|
-
const response = await this.client.put(`/task/${taskId}`, updateData);
|
|
258
|
-
return response.data;
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
throw this.handleError(error, `Failed to update task ${taskId}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Delete a task
|
|
267
|
-
* @param taskId The ID of the task to delete
|
|
268
|
-
* @returns Success indicator
|
|
269
|
-
*/
|
|
270
|
-
async deleteTask(taskId) {
|
|
271
|
-
this.logOperation('deleteTask', { taskId });
|
|
272
|
-
try {
|
|
273
|
-
await this.makeRequest(async () => {
|
|
274
|
-
await this.client.delete(`/task/${taskId}`);
|
|
275
|
-
});
|
|
276
|
-
return {
|
|
277
|
-
success: true
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
throw this.handleError(error, `Failed to delete task ${taskId}`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Find a task by name within a list
|
|
286
|
-
* @param listId The list ID to search within
|
|
287
|
-
* @param taskName The name of the task to find
|
|
288
|
-
* @returns The task if found, otherwise null
|
|
289
|
-
*/
|
|
290
|
-
async findTaskByName(listId, taskName) {
|
|
291
|
-
this.logOperation('findTaskByName', { listId, taskName });
|
|
292
|
-
try {
|
|
293
|
-
const tasks = await this.getTasks(listId);
|
|
294
|
-
// Find task by exact match first, then case-insensitive, then substring
|
|
295
|
-
// Exact match
|
|
296
|
-
let match = tasks.find(task => task.name === taskName);
|
|
297
|
-
if (match)
|
|
298
|
-
return match;
|
|
299
|
-
// Case-insensitive match
|
|
300
|
-
match = tasks.find(task => task.name.toLowerCase() === taskName.toLowerCase());
|
|
301
|
-
if (match)
|
|
302
|
-
return match;
|
|
303
|
-
// Substring match
|
|
304
|
-
match = tasks.find(task => task.name.toLowerCase().includes(taskName.toLowerCase()));
|
|
305
|
-
return match || null;
|
|
306
|
-
}
|
|
307
|
-
catch (error) {
|
|
308
|
-
throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Move a task to a different list
|
|
313
|
-
* @param taskId The ID of the task to move
|
|
314
|
-
* @param destinationListId The ID of the list to move the task to
|
|
315
|
-
* @returns The updated task
|
|
316
|
-
*/
|
|
317
|
-
async moveTask(taskId, destinationListId) {
|
|
318
|
-
this.logOperation('moveTask', { taskId, destinationListId });
|
|
319
|
-
try {
|
|
320
|
-
// First, get both the task and list info in parallel to save time
|
|
321
|
-
const [originalTask, destinationList] = await Promise.all([
|
|
322
|
-
this.getTask(taskId),
|
|
323
|
-
this.listService.getList(destinationListId)
|
|
324
|
-
]);
|
|
325
|
-
const currentStatus = originalTask.status?.status;
|
|
326
|
-
const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
|
|
327
|
-
// Determine the appropriate status for the destination list
|
|
328
|
-
let newStatus = availableStatuses.includes(currentStatus || '')
|
|
329
|
-
? currentStatus // Keep the same status if available in destination list
|
|
330
|
-
: destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
|
|
331
|
-
// Prepare the task data for the new list
|
|
332
|
-
const taskData = this.extractTaskData(originalTask);
|
|
333
|
-
taskData.status = newStatus;
|
|
334
|
-
// Create new task and delete old one in a single makeRequest call
|
|
335
|
-
return await this.makeRequest(async () => {
|
|
336
|
-
// First create the new task
|
|
337
|
-
const response = await this.client.post(`/list/${destinationListId}/task`, taskData);
|
|
338
|
-
// Then delete the original task
|
|
339
|
-
await this.client.delete(`/task/${taskId}`);
|
|
340
|
-
// Add a property to indicate the task was moved
|
|
341
|
-
const newTask = {
|
|
342
|
-
...response.data,
|
|
343
|
-
moved: true,
|
|
344
|
-
originalId: taskId
|
|
345
|
-
};
|
|
346
|
-
return newTask;
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
throw this.handleError(error, 'Failed to move task');
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Create a duplicate of an existing task
|
|
355
|
-
* @param taskId The ID of the task to duplicate
|
|
356
|
-
* @param listId Optional destination list ID (defaults to the same list)
|
|
357
|
-
* @returns The newly created duplicate task
|
|
358
|
-
*/
|
|
359
|
-
async duplicateTask(taskId, listId) {
|
|
360
|
-
this.logOperation('duplicateTask', { taskId, listId });
|
|
361
|
-
try {
|
|
362
|
-
// Get the original task to duplicate
|
|
363
|
-
const originalTask = await this.getTask(taskId);
|
|
364
|
-
// Create a copy of the task data with "(copy)" appended to the name
|
|
365
|
-
const newTaskData = this.extractTaskData(originalTask, `${originalTask.name} (copy)`);
|
|
366
|
-
// Create the new task in the specified list or original list
|
|
367
|
-
const targetListId = listId || originalTask.list.id;
|
|
368
|
-
return await this.createTask(targetListId, newTaskData);
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
throw this.handleError(error, 'Failed to duplicate task');
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Get all comments for a task
|
|
376
|
-
*
|
|
377
|
-
* @param taskId ID of the task to get comments for
|
|
378
|
-
* @param start Optional pagination start
|
|
379
|
-
* @param startId Optional comment ID to start from
|
|
380
|
-
* @returns Array of task comments
|
|
381
|
-
*/
|
|
382
|
-
async getTaskComments(taskId, start, startId) {
|
|
383
|
-
this.logOperation('getTaskComments', { taskId, start, startId });
|
|
384
|
-
try {
|
|
385
|
-
// Build query parameters for pagination
|
|
386
|
-
const queryParams = new URLSearchParams();
|
|
387
|
-
if (start !== undefined) {
|
|
388
|
-
queryParams.append('start', start.toString());
|
|
389
|
-
}
|
|
390
|
-
if (startId) {
|
|
391
|
-
queryParams.append('start_id', startId);
|
|
392
|
-
}
|
|
393
|
-
const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
|
|
394
|
-
const response = await this.client.get(`/task/${taskId}/comment${queryString}`);
|
|
395
|
-
return response.data.comments || [];
|
|
396
|
-
}
|
|
397
|
-
catch (error) {
|
|
398
|
-
throw this.handleError(error, 'Failed to get task comments');
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Create a comment on a task
|
|
403
|
-
*
|
|
404
|
-
* @param taskId ID of the task to comment on
|
|
405
|
-
* @param commentText Text content of the comment
|
|
406
|
-
* @param notifyAll Whether to notify all assignees
|
|
407
|
-
* @param assignee Optional user ID to assign the comment to
|
|
408
|
-
* @returns The created comment
|
|
409
|
-
*/
|
|
410
|
-
async createTaskComment(taskId, commentText, notifyAll = false, assignee) {
|
|
411
|
-
this.logOperation('createTaskComment', { taskId, commentText, notifyAll, assignee });
|
|
412
|
-
try {
|
|
413
|
-
const payload = {
|
|
414
|
-
comment_text: commentText,
|
|
415
|
-
notify_all: notifyAll
|
|
416
|
-
};
|
|
417
|
-
if (assignee) {
|
|
418
|
-
payload.assignee = assignee;
|
|
419
|
-
}
|
|
420
|
-
// Make the request directly without using makeRequest for better error handling
|
|
421
|
-
const response = await this.client.post(`/task/${taskId}/comment`, payload);
|
|
422
|
-
// Handle different response formats from ClickUp API
|
|
423
|
-
if (response.data) {
|
|
424
|
-
if (response.data.comment) {
|
|
425
|
-
// Standard format: { comment: ClickUpComment }
|
|
426
|
-
return response.data.comment;
|
|
427
|
-
}
|
|
428
|
-
else if (response.data.id && (response.data.comment_text || response.data.comment)) {
|
|
429
|
-
// Direct format: the comment object itself
|
|
430
|
-
return response.data;
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
// Fallback: construct a minimal valid comment object
|
|
434
|
-
return {
|
|
435
|
-
id: response.data.id || `custom-${Date.now()}`,
|
|
436
|
-
comment: response.data.comment || commentText,
|
|
437
|
-
comment_text: response.data.comment_text || commentText,
|
|
438
|
-
user: response.data.user || { id: 0, username: 'Unknown', email: '', color: '' },
|
|
439
|
-
date: response.data.date || new Date().toISOString(),
|
|
440
|
-
resolved: false
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
throw new Error('Invalid response from ClickUp API');
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
// Check if comment might have been created despite error
|
|
448
|
-
if (error.response?.status === 200 || error.response?.status === 201) {
|
|
449
|
-
// Try to construct a comment object from what we know
|
|
450
|
-
return {
|
|
451
|
-
id: `fallback-${Date.now()}`,
|
|
452
|
-
comment: commentText,
|
|
453
|
-
comment_text: commentText,
|
|
454
|
-
user: { id: 0, username: 'Unknown', email: '', color: '' },
|
|
455
|
-
date: new Date().toISOString(),
|
|
456
|
-
resolved: false
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
throw this.handleError(error, 'Failed to create task comment');
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Validate that a list exists
|
|
464
|
-
*
|
|
465
|
-
* @param listId ID of the list to validate
|
|
466
|
-
* @throws ClickUpServiceError if the list doesn't exist
|
|
467
|
-
*/
|
|
468
|
-
async validateListExists(listId) {
|
|
469
|
-
this.logOperation('validateListExists', { listId });
|
|
470
|
-
try {
|
|
471
|
-
const list = await this.listService.getList(listId);
|
|
472
|
-
if (!list) {
|
|
473
|
-
throw new ClickUpServiceError(`List not found: ${listId}`, ErrorCode.NOT_FOUND, { listId });
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
catch (error) {
|
|
477
|
-
if (error instanceof ClickUpServiceError) {
|
|
478
|
-
throw error;
|
|
479
|
-
}
|
|
480
|
-
throw new ClickUpServiceError(`Failed to validate list existence: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.UNKNOWN, { listId });
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Upload a file attachment to a ClickUp task
|
|
485
|
-
* @param taskId The ID of the task to attach the file to
|
|
486
|
-
* @param fileData The file data as a Buffer
|
|
487
|
-
* @param fileName The name of the file
|
|
488
|
-
* @returns Promise resolving to the attachment response from ClickUp
|
|
489
|
-
*/
|
|
490
|
-
async uploadTaskAttachment(taskId, fileData, fileName) {
|
|
491
|
-
this.logOperation('uploadTaskAttachment', { taskId, fileName, fileSize: fileData.length });
|
|
492
|
-
try {
|
|
493
|
-
return await this.makeRequest(async () => {
|
|
494
|
-
// Create FormData for multipart/form-data upload
|
|
495
|
-
const FormData = (await import('form-data')).default;
|
|
496
|
-
const formData = new FormData();
|
|
497
|
-
// Add the file to the form data
|
|
498
|
-
formData.append('attachment', fileData, {
|
|
499
|
-
filename: fileName,
|
|
500
|
-
contentType: 'application/octet-stream' // Let ClickUp determine the content type
|
|
501
|
-
});
|
|
502
|
-
// Use the raw axios client for this request since we need to handle FormData
|
|
503
|
-
const response = await this.client.post(`/task/${taskId}/attachment`, formData, {
|
|
504
|
-
headers: {
|
|
505
|
-
...formData.getHeaders(),
|
|
506
|
-
'Authorization': this.apiKey
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
return response.data;
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
catch (error) {
|
|
513
|
-
throw this.handleError(error, `Failed to upload attachment to task ${taskId}`);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Upload a file attachment to a ClickUp task from a URL
|
|
518
|
-
* @param taskId The ID of the task to attach the file to
|
|
519
|
-
* @param fileUrl The URL of the file to download and attach
|
|
520
|
-
* @param fileName Optional file name (if not provided, it will be extracted from the URL)
|
|
521
|
-
* @param authHeader Optional authorization header for the URL
|
|
522
|
-
* @returns Promise resolving to the attachment response from ClickUp
|
|
523
|
-
*/
|
|
524
|
-
async uploadTaskAttachmentFromUrl(taskId, fileUrl, fileName, authHeader) {
|
|
525
|
-
this.logOperation('uploadTaskAttachmentFromUrl', { taskId, fileUrl, fileName });
|
|
526
|
-
try {
|
|
527
|
-
return await this.makeRequest(async () => {
|
|
528
|
-
// Import required modules
|
|
529
|
-
const axios = (await import('axios')).default;
|
|
530
|
-
const FormData = (await import('form-data')).default;
|
|
531
|
-
// Download the file from the URL
|
|
532
|
-
const headers = {};
|
|
533
|
-
if (authHeader) {
|
|
534
|
-
headers['Authorization'] = authHeader;
|
|
535
|
-
}
|
|
536
|
-
const response = await axios.get(fileUrl, {
|
|
537
|
-
responseType: 'arraybuffer',
|
|
538
|
-
headers
|
|
539
|
-
});
|
|
540
|
-
// Extract filename from URL if not provided
|
|
541
|
-
const actualFileName = fileName || fileUrl.split('/').pop() || 'downloaded-file';
|
|
542
|
-
// Create FormData for multipart/form-data upload
|
|
543
|
-
const formData = new FormData();
|
|
544
|
-
// Add the file to the form data
|
|
545
|
-
formData.append('attachment', Buffer.from(response.data), {
|
|
546
|
-
filename: actualFileName,
|
|
547
|
-
contentType: 'application/octet-stream'
|
|
548
|
-
});
|
|
549
|
-
// Upload the file to ClickUp
|
|
550
|
-
const uploadResponse = await this.client.post(`/task/${taskId}/attachment`, formData, {
|
|
551
|
-
headers: {
|
|
552
|
-
...formData.getHeaders(),
|
|
553
|
-
'Authorization': this.apiKey
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
return uploadResponse.data;
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
catch (error) {
|
|
560
|
-
throw this.handleError(error, `Failed to upload attachment from URL to task ${taskId}`);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Format task data for summary view
|
|
565
|
-
* @param task The task to format
|
|
566
|
-
* @returns TaskSummary object
|
|
567
|
-
*/
|
|
568
|
-
formatTaskSummary(task) {
|
|
569
|
-
return {
|
|
570
|
-
id: task.id,
|
|
571
|
-
name: task.name,
|
|
572
|
-
status: task.status.status,
|
|
573
|
-
list: {
|
|
574
|
-
id: task.list.id,
|
|
575
|
-
name: task.list.name
|
|
576
|
-
},
|
|
577
|
-
due_date: task.due_date,
|
|
578
|
-
url: task.url,
|
|
579
|
-
priority: this.extractPriorityValue(task),
|
|
580
|
-
tags: task.tags.map(tag => ({
|
|
581
|
-
name: tag.name,
|
|
582
|
-
tag_bg: tag.tag_bg,
|
|
583
|
-
tag_fg: tag.tag_fg
|
|
584
|
-
}))
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Estimates token count for a task in JSON format
|
|
589
|
-
* @param task ClickUp task
|
|
590
|
-
* @returns Estimated token count
|
|
591
|
-
*/
|
|
592
|
-
estimateTaskTokens(task) {
|
|
593
|
-
return estimateTokensFromObject(task);
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Get filtered tasks across the entire team/workspace using tags and other filters
|
|
597
|
-
* @param filters Task filters to apply including tags, list/folder/space filtering
|
|
598
|
-
* @returns Either a DetailedTaskResponse or WorkspaceTasksResponse depending on detail_level
|
|
599
|
-
*/
|
|
600
|
-
async getWorkspaceTasks(filters = {}) {
|
|
601
|
-
try {
|
|
602
|
-
this.logOperation('getWorkspaceTasks', { filters });
|
|
603
|
-
const params = this.buildTaskFilterParams(filters);
|
|
604
|
-
const response = await this.client.get(`/team/${this.teamId}/task`, {
|
|
605
|
-
params
|
|
606
|
-
});
|
|
607
|
-
const tasks = response.data.tasks;
|
|
608
|
-
const totalCount = tasks.length; // Note: This is just the current page count
|
|
609
|
-
const hasMore = totalCount === 100; // ClickUp returns max 100 tasks per page
|
|
610
|
-
const nextPage = (filters.page || 0) + 1;
|
|
611
|
-
// If the estimated token count exceeds 50,000 or detail_level is 'summary',
|
|
612
|
-
// return summary format for efficiency and to avoid hitting token limits
|
|
613
|
-
const TOKEN_LIMIT = 50000;
|
|
614
|
-
// Estimate tokens for the full response
|
|
615
|
-
let tokensExceedLimit = false;
|
|
616
|
-
if (filters.detail_level !== 'summary' && tasks.length > 0) {
|
|
617
|
-
// We only need to check token count if detailed was requested
|
|
618
|
-
// For summary requests, we always return summary format
|
|
619
|
-
// First check with a sample task - if one task exceeds the limit, we definitely need summary
|
|
620
|
-
const sampleTask = tasks[0];
|
|
621
|
-
// Check if all tasks would exceed the token limit
|
|
622
|
-
const estimatedTokensPerTask = this.estimateTaskTokens(sampleTask);
|
|
623
|
-
const estimatedTotalTokens = estimatedTokensPerTask * tasks.length;
|
|
624
|
-
// Add 10% overhead for the response wrapper
|
|
625
|
-
tokensExceedLimit = estimatedTotalTokens * 1.1 > TOKEN_LIMIT;
|
|
626
|
-
// Double-check with more precise estimation if we're close to the limit
|
|
627
|
-
if (!tokensExceedLimit && estimatedTotalTokens * 1.1 > TOKEN_LIMIT * 0.8) {
|
|
628
|
-
// More precise check - build a representative sample and extrapolate
|
|
629
|
-
tokensExceedLimit = wouldExceedTokenLimit({ tasks, total_count: totalCount, has_more: hasMore, next_page: nextPage }, TOKEN_LIMIT);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
// Determine if we should return summary or detailed based on request and token limit
|
|
633
|
-
const shouldUseSummary = filters.detail_level === 'summary' || tokensExceedLimit;
|
|
634
|
-
this.logOperation('getWorkspaceTasks', {
|
|
635
|
-
totalTasks: tasks.length,
|
|
636
|
-
estimatedTokens: tasks.reduce((count, task) => count + this.estimateTaskTokens(task), 0),
|
|
637
|
-
usingDetailedFormat: !shouldUseSummary,
|
|
638
|
-
requestedFormat: filters.detail_level || 'auto'
|
|
639
|
-
});
|
|
640
|
-
if (shouldUseSummary) {
|
|
641
|
-
return {
|
|
642
|
-
summaries: tasks.map(task => this.formatTaskSummary(task)),
|
|
643
|
-
total_count: totalCount,
|
|
644
|
-
has_more: hasMore,
|
|
645
|
-
next_page: nextPage
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
return {
|
|
649
|
-
tasks,
|
|
650
|
-
total_count: totalCount,
|
|
651
|
-
has_more: hasMore,
|
|
652
|
-
next_page: nextPage
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
catch (error) {
|
|
656
|
-
this.logOperation('getWorkspaceTasks', { error: error.message, status: error.response?.status });
|
|
657
|
-
throw this.handleError(error, 'Failed to get workspace tasks');
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Get task summaries for lightweight retrieval
|
|
662
|
-
* @param filters Task filters to apply
|
|
663
|
-
* @returns WorkspaceTasksResponse with task summaries
|
|
664
|
-
*/
|
|
665
|
-
async getTaskSummaries(filters = {}) {
|
|
666
|
-
return this.getWorkspaceTasks({ ...filters, detail_level: 'summary' });
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Get detailed task data
|
|
670
|
-
* @param filters Task filters to apply
|
|
671
|
-
* @returns DetailedTaskResponse with full task data
|
|
672
|
-
*/
|
|
673
|
-
async getTaskDetails(filters = {}) {
|
|
674
|
-
return this.getWorkspaceTasks({ ...filters, detail_level: 'detailed' });
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Unified method for finding tasks by ID or name with consistent handling of global lookup
|
|
678
|
-
*
|
|
679
|
-
* This method provides a single entry point for all task lookup operations:
|
|
680
|
-
* - Direct lookup by task ID (regular or custom)
|
|
681
|
-
* - Lookup by task name within a specific list
|
|
682
|
-
* - Global lookup by task name across the entire workspace
|
|
683
|
-
*
|
|
684
|
-
* @param options Lookup options with the following parameters:
|
|
685
|
-
* - taskId: Optional task ID for direct lookup
|
|
686
|
-
* - customTaskId: Optional custom task ID for direct lookup
|
|
687
|
-
* - taskName: Optional task name to search for
|
|
688
|
-
* - listId: Optional list ID to scope the search
|
|
689
|
-
* - listName: Optional list name to scope the search
|
|
690
|
-
* - allowMultipleMatches: Whether to return all matches instead of throwing an error
|
|
691
|
-
* - useSmartDisambiguation: Whether to automatically select the most recently updated task
|
|
692
|
-
* - includeFullDetails: Whether to include full task details (true) or just task summaries (false)
|
|
693
|
-
* - includeListContext: Whether to include list/folder/space context with results
|
|
694
|
-
* @returns Either a single task or an array of tasks depending on options
|
|
695
|
-
* @throws Error if task cannot be found or if multiple matches are found when not allowed
|
|
696
|
-
*/
|
|
697
|
-
async findTasks({ taskId, customTaskId, taskName, listId, listName, allowMultipleMatches = false, useSmartDisambiguation = false, includeFullDetails = true, includeListContext = true }) {
|
|
698
|
-
try {
|
|
699
|
-
this.logOperation('findTasks', {
|
|
700
|
-
taskId,
|
|
701
|
-
customTaskId,
|
|
702
|
-
taskName,
|
|
703
|
-
listId,
|
|
704
|
-
listName,
|
|
705
|
-
allowMultipleMatches,
|
|
706
|
-
useSmartDisambiguation
|
|
707
|
-
});
|
|
708
|
-
// Case 1: Direct task ID lookup (highest priority)
|
|
709
|
-
if (taskId) {
|
|
710
|
-
// Check if it looks like a custom ID
|
|
711
|
-
if (taskId.includes('-') && /^[A-Z]+\-\d+$/.test(taskId)) {
|
|
712
|
-
this.logOperation('findTasks', { detectedCustomId: taskId });
|
|
713
|
-
try {
|
|
714
|
-
// Try to get it as a custom ID first
|
|
715
|
-
let resolvedListId;
|
|
716
|
-
if (listId) {
|
|
717
|
-
resolvedListId = listId;
|
|
718
|
-
}
|
|
719
|
-
else if (listName) {
|
|
720
|
-
const listInfo = await findListIDByName(this.workspaceService, listName);
|
|
721
|
-
if (listInfo) {
|
|
722
|
-
resolvedListId = listInfo.id;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
const foundTask = await this.getTaskByCustomId(taskId, resolvedListId);
|
|
726
|
-
return foundTask;
|
|
727
|
-
}
|
|
728
|
-
catch (error) {
|
|
729
|
-
// If it fails as a custom ID, try as a regular ID
|
|
730
|
-
this.logOperation('findTasks', {
|
|
731
|
-
message: `Failed to find task with custom ID "${taskId}", falling back to regular ID`,
|
|
732
|
-
error: error.message
|
|
733
|
-
});
|
|
734
|
-
return await this.getTask(taskId);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
// Regular task ID
|
|
738
|
-
return await this.getTask(taskId);
|
|
739
|
-
}
|
|
740
|
-
// Case 2: Explicit custom task ID lookup
|
|
741
|
-
if (customTaskId) {
|
|
742
|
-
let resolvedListId;
|
|
743
|
-
if (listId) {
|
|
744
|
-
resolvedListId = listId;
|
|
745
|
-
}
|
|
746
|
-
else if (listName) {
|
|
747
|
-
const listInfo = await findListIDByName(this.workspaceService, listName);
|
|
748
|
-
if (listInfo) {
|
|
749
|
-
resolvedListId = listInfo.id;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return await this.getTaskByCustomId(customTaskId, resolvedListId);
|
|
753
|
-
}
|
|
754
|
-
// Case 3: Task name lookup (requires either list context or global lookup)
|
|
755
|
-
if (taskName) {
|
|
756
|
-
// Case 3a: Task name + list context - search in specific list
|
|
757
|
-
if (listId || listName) {
|
|
758
|
-
let resolvedListId;
|
|
759
|
-
if (listId) {
|
|
760
|
-
resolvedListId = listId;
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
const listInfo = await findListIDByName(this.workspaceService, listName);
|
|
764
|
-
if (!listInfo) {
|
|
765
|
-
throw new Error(`List "${listName}" not found`);
|
|
766
|
-
}
|
|
767
|
-
resolvedListId = listInfo.id;
|
|
768
|
-
}
|
|
769
|
-
const foundTask = await this.findTaskByName(resolvedListId, taskName);
|
|
770
|
-
if (!foundTask) {
|
|
771
|
-
throw new Error(`Task "${taskName}" not found in list`);
|
|
772
|
-
}
|
|
773
|
-
// If includeFullDetails is true and we need context not already in the task,
|
|
774
|
-
// get full details, otherwise return what we already have
|
|
775
|
-
if (includeFullDetails && (!foundTask.list || !foundTask.list.name || !foundTask.status)) {
|
|
776
|
-
return await this.getTask(foundTask.id);
|
|
777
|
-
}
|
|
778
|
-
return foundTask;
|
|
779
|
-
}
|
|
780
|
-
// Case 3b: Task name without list context - global lookup across workspace
|
|
781
|
-
// Get lightweight task summaries for efficient first-pass filtering
|
|
782
|
-
const response = await this.getTaskSummaries({});
|
|
783
|
-
if (!this.workspaceService) {
|
|
784
|
-
throw new Error("Workspace service required for global task lookup");
|
|
785
|
-
}
|
|
786
|
-
// Create an index to efficiently look up list context information
|
|
787
|
-
const hierarchy = await this.workspaceService.getWorkspaceHierarchy();
|
|
788
|
-
const listContextMap = new Map();
|
|
789
|
-
// Function to recursively build list context map
|
|
790
|
-
function buildListContextMap(nodes, spaceId, spaceName, folderId, folderName) {
|
|
791
|
-
for (const node of nodes) {
|
|
792
|
-
if (node.type === 'space') {
|
|
793
|
-
// Process space children
|
|
794
|
-
if (node.children) {
|
|
795
|
-
buildListContextMap(node.children, node.id, node.name);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
else if (node.type === 'folder') {
|
|
799
|
-
// Process folder children
|
|
800
|
-
if (node.children) {
|
|
801
|
-
buildListContextMap(node.children, spaceId, spaceName, node.id, node.name);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
else if (node.type === 'list') {
|
|
805
|
-
// Add list context to map
|
|
806
|
-
listContextMap.set(node.id, {
|
|
807
|
-
listId: node.id,
|
|
808
|
-
listName: node.name,
|
|
809
|
-
spaceId: spaceId,
|
|
810
|
-
spaceName: spaceName,
|
|
811
|
-
folderId,
|
|
812
|
-
folderName
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
// Build the context map
|
|
818
|
-
buildListContextMap(hierarchy.root.children);
|
|
819
|
-
// Find tasks that match the provided name
|
|
820
|
-
const initialMatches = [];
|
|
821
|
-
// Process task summaries to find initial matches
|
|
822
|
-
for (const taskSummary of response.summaries) {
|
|
823
|
-
if (isNameMatch(taskSummary.name, taskName)) {
|
|
824
|
-
// Get list context information
|
|
825
|
-
const listContext = listContextMap.get(taskSummary.list.id);
|
|
826
|
-
if (listContext) {
|
|
827
|
-
// Store task summary and context
|
|
828
|
-
initialMatches.push({
|
|
829
|
-
id: taskSummary.id,
|
|
830
|
-
task: taskSummary,
|
|
831
|
-
listContext
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
// Handle the no matches case
|
|
837
|
-
if (initialMatches.length === 0) {
|
|
838
|
-
throw new Error(`Task "${taskName}" not found in any list across your workspace. Please check the task name and try again.`);
|
|
839
|
-
}
|
|
840
|
-
// Handle the single match case - we can return early if we don't need full details
|
|
841
|
-
if (initialMatches.length === 1 && !useSmartDisambiguation && !includeFullDetails) {
|
|
842
|
-
const match = initialMatches[0];
|
|
843
|
-
if (includeListContext) {
|
|
844
|
-
return {
|
|
845
|
-
...match.task,
|
|
846
|
-
list: {
|
|
847
|
-
id: match.listContext.listId,
|
|
848
|
-
name: match.listContext.listName
|
|
849
|
-
},
|
|
850
|
-
folder: match.listContext.folderId ? {
|
|
851
|
-
id: match.listContext.folderId,
|
|
852
|
-
name: match.listContext.folderName
|
|
853
|
-
} : undefined,
|
|
854
|
-
space: {
|
|
855
|
-
id: match.listContext.spaceId,
|
|
856
|
-
name: match.listContext.spaceName
|
|
857
|
-
}
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
return match.task;
|
|
861
|
-
}
|
|
862
|
-
// For multiple matches or when we need details, fetch full task info
|
|
863
|
-
const fullMatches = [];
|
|
864
|
-
try {
|
|
865
|
-
// Process in sequence for better reliability
|
|
866
|
-
for (const match of initialMatches) {
|
|
867
|
-
const fullTask = await this.getTask(match.id);
|
|
868
|
-
if (includeListContext) {
|
|
869
|
-
// Enhance task with context information
|
|
870
|
-
fullTask.list = {
|
|
871
|
-
...fullTask.list,
|
|
872
|
-
name: match.listContext.listName
|
|
873
|
-
};
|
|
874
|
-
if (match.listContext.folderId) {
|
|
875
|
-
fullTask.folder = {
|
|
876
|
-
id: match.listContext.folderId,
|
|
877
|
-
name: match.listContext.folderName
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
fullTask.space = {
|
|
881
|
-
id: match.listContext.spaceId,
|
|
882
|
-
name: match.listContext.spaceName
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
fullMatches.push(fullTask);
|
|
886
|
-
}
|
|
887
|
-
// Sort by update time for disambiguation
|
|
888
|
-
if (fullMatches.length > 1) {
|
|
889
|
-
fullMatches.sort((a, b) => {
|
|
890
|
-
const aDate = parseInt(a.date_updated || '0', 10);
|
|
891
|
-
const bDate = parseInt(b.date_updated || '0', 10);
|
|
892
|
-
return bDate - aDate; // Most recent first
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
catch (error) {
|
|
897
|
-
this.logOperation('findTasks', {
|
|
898
|
-
error: error.message,
|
|
899
|
-
message: "Failed to get detailed task information"
|
|
900
|
-
});
|
|
901
|
-
// If detailed fetch fails, use the summaries with context info
|
|
902
|
-
// This fallback ensures we still return something useful
|
|
903
|
-
if (allowMultipleMatches) {
|
|
904
|
-
return initialMatches.map(match => ({
|
|
905
|
-
...match.task,
|
|
906
|
-
list: {
|
|
907
|
-
id: match.listContext.listId,
|
|
908
|
-
name: match.listContext.listName
|
|
909
|
-
},
|
|
910
|
-
folder: match.listContext.folderId ? {
|
|
911
|
-
id: match.listContext.folderId,
|
|
912
|
-
name: match.listContext.folderName
|
|
913
|
-
} : undefined,
|
|
914
|
-
space: {
|
|
915
|
-
id: match.listContext.spaceId,
|
|
916
|
-
name: match.listContext.spaceName
|
|
917
|
-
}
|
|
918
|
-
}));
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
// For single result, return the first match
|
|
922
|
-
const match = initialMatches[0];
|
|
923
|
-
return {
|
|
924
|
-
...match.task,
|
|
925
|
-
list: {
|
|
926
|
-
id: match.listContext.listId,
|
|
927
|
-
name: match.listContext.listName
|
|
928
|
-
},
|
|
929
|
-
folder: match.listContext.folderId ? {
|
|
930
|
-
id: match.listContext.folderId,
|
|
931
|
-
name: match.listContext.folderName
|
|
932
|
-
} : undefined,
|
|
933
|
-
space: {
|
|
934
|
-
id: match.listContext.spaceId,
|
|
935
|
-
name: match.listContext.spaceName
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
// Return results based on options
|
|
941
|
-
if (fullMatches.length === 1 || useSmartDisambiguation) {
|
|
942
|
-
return fullMatches[0]; // Return most recently updated if multiple and smart disambiguation enabled
|
|
943
|
-
}
|
|
944
|
-
else if (allowMultipleMatches) {
|
|
945
|
-
return fullMatches; // Return all matches
|
|
946
|
-
}
|
|
947
|
-
else {
|
|
948
|
-
// Format error message for multiple matches
|
|
949
|
-
const matchesInfo = fullMatches.map(task => {
|
|
950
|
-
const listName = task.list?.name || "Unknown list";
|
|
951
|
-
const folderName = task.folder?.name;
|
|
952
|
-
const spaceName = task.space?.name || "Unknown space";
|
|
953
|
-
const updateTime = task.date_updated
|
|
954
|
-
? new Date(parseInt(task.date_updated, 10)).toLocaleString()
|
|
955
|
-
: "Unknown date";
|
|
956
|
-
const location = `list "${listName}"${folderName ? ` (folder: "${folderName}")` : ''} (space: "${spaceName}")`;
|
|
957
|
-
return `- "${task.name}" in ${location} - Updated ${updateTime}`;
|
|
958
|
-
}).join('\n');
|
|
959
|
-
throw new Error(`Multiple tasks found with name "${taskName}":\n${matchesInfo}\n\nPlease provide list context to disambiguate or set allowMultipleMatches to true.`);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
// No valid lookup parameters provided
|
|
963
|
-
throw new Error("At least one of taskId, customTaskId, or taskName must be provided");
|
|
964
|
-
}
|
|
965
|
-
catch (error) {
|
|
966
|
-
if (error.message?.includes('Task "') && error.message?.includes('not found')) {
|
|
967
|
-
throw error;
|
|
968
|
-
}
|
|
969
|
-
if (error.message?.includes('Multiple tasks found')) {
|
|
970
|
-
throw error;
|
|
971
|
-
}
|
|
972
|
-
// Unexpected errors
|
|
973
|
-
throw this.handleError(error, `Error finding task: ${error.message}`);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
}
|