@taazkareem/clickup-mcp-server 0.6.2 → 0.6.3

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.
Files changed (41) hide show
  1. package/README.md +6 -6
  2. package/build/logger.js +26 -1
  3. package/build/server.js +1 -1
  4. package/build/services/clickup/base.js +22 -1
  5. package/build/services/clickup/bulk.js +76 -45
  6. package/build/services/clickup/index.js +2 -2
  7. package/build/services/clickup/task/index.js +32 -0
  8. package/build/services/clickup/task/task-attachments.js +97 -0
  9. package/build/services/clickup/task/task-comments.js +104 -0
  10. package/build/services/clickup/task/task-core.js +477 -0
  11. package/build/services/clickup/task/task-custom-fields.js +97 -0
  12. package/build/services/clickup/task/task-search.js +462 -0
  13. package/build/services/clickup/task/task-service.js +25 -0
  14. package/build/services/clickup/task/task-tags.js +101 -0
  15. package/build/services/clickup/workspace.js +81 -36
  16. package/build/tools/folder.js +1 -1
  17. package/build/tools/list.js +2 -4
  18. package/build/tools/task/attachments.js +18 -5
  19. package/build/tools/task/attachments.types.js +9 -0
  20. package/build/tools/task/bulk-operations.js +111 -15
  21. package/build/tools/task/handlers.js +169 -24
  22. package/build/tools/task/index.js +1 -1
  23. package/build/tools/task/main.js +36 -1
  24. package/build/tools/task/single-operations.js +51 -4
  25. package/build/tools/task/utilities.js +24 -71
  26. package/build/tools/utils.js +2 -2
  27. package/build/utils/date-utils.js +149 -30
  28. package/build/utils/resolver-utils.js +33 -40
  29. package/build/utils/sponsor-service.js +1 -1
  30. package/package.json +1 -1
  31. package/build/mcp-tools.js +0 -64
  32. package/build/server-state.js +0 -93
  33. package/build/server.log +0 -0
  34. package/build/services/clickup/task.js +0 -701
  35. package/build/tools/bulk-tasks.js +0 -36
  36. package/build/tools/debug.js +0 -76
  37. package/build/tools/logs.js +0 -55
  38. package/build/tools/task.js +0 -1554
  39. package/build/utils/params-utils.js +0 -39
  40. package/build/utils/sponsor-analytics.js +0 -100
  41. package/build/utils/sponsor-utils.js +0 -57
@@ -1,701 +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
- export class TaskService extends BaseClickUpService {
18
- constructor(apiKey, teamId, baseUrl, workspaceService) {
19
- super(apiKey, teamId, baseUrl);
20
- this.workspaceService = null;
21
- if (workspaceService) {
22
- this.workspaceService = workspaceService;
23
- this.logOperation('constructor', { usingSharedWorkspaceService: true });
24
- }
25
- // Initialize list service for list lookups
26
- this.listService = new ListService(apiKey, teamId, baseUrl, this.workspaceService);
27
- this.logOperation('constructor', { initialized: true });
28
- }
29
- /**
30
- * Helper method to handle errors consistently
31
- * @param error The error that occurred
32
- * @param message Optional custom error message
33
- * @returns A ClickUpServiceError
34
- */
35
- handleError(error, message) {
36
- if (error instanceof ClickUpServiceError) {
37
- return error;
38
- }
39
- return new ClickUpServiceError(message || `Task service error: ${error.message}`, ErrorCode.UNKNOWN, error);
40
- }
41
- /**
42
- * Build URL parameters from task filters
43
- * @param filters Task filters to convert to URL parameters
44
- * @returns URLSearchParams object
45
- */
46
- buildTaskFilterParams(filters) {
47
- const params = new URLSearchParams();
48
- // Add all filters to the query parameters
49
- if (filters.include_closed)
50
- params.append('include_closed', String(filters.include_closed));
51
- if (filters.subtasks)
52
- params.append('subtasks', String(filters.subtasks));
53
- if (filters.page)
54
- params.append('page', String(filters.page));
55
- if (filters.order_by)
56
- params.append('order_by', filters.order_by);
57
- if (filters.reverse)
58
- params.append('reverse', String(filters.reverse));
59
- // Array parameters
60
- if (filters.statuses && filters.statuses.length > 0) {
61
- filters.statuses.forEach(status => params.append('statuses[]', status));
62
- }
63
- if (filters.assignees && filters.assignees.length > 0) {
64
- filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
65
- }
66
- // Team tasks endpoint specific parameters
67
- if (filters.tags && filters.tags.length > 0) {
68
- filters.tags.forEach(tag => params.append('tags[]', tag));
69
- }
70
- if (filters.list_ids && filters.list_ids.length > 0) {
71
- filters.list_ids.forEach(id => params.append('list_ids[]', id));
72
- }
73
- if (filters.folder_ids && filters.folder_ids.length > 0) {
74
- filters.folder_ids.forEach(id => params.append('folder_ids[]', id));
75
- }
76
- if (filters.space_ids && filters.space_ids.length > 0) {
77
- filters.space_ids.forEach(id => params.append('space_ids[]', id));
78
- }
79
- if (filters.archived !== undefined)
80
- params.append('archived', String(filters.archived));
81
- if (filters.include_closed_lists !== undefined)
82
- params.append('include_closed_lists', String(filters.include_closed_lists));
83
- if (filters.include_archived_lists !== undefined)
84
- params.append('include_archived_lists', String(filters.include_archived_lists));
85
- if (filters.include_compact_time_entries !== undefined)
86
- params.append('include_compact_time_entries', String(filters.include_compact_time_entries));
87
- // Date filters
88
- if (filters.due_date_gt)
89
- params.append('due_date_gt', String(filters.due_date_gt));
90
- if (filters.due_date_lt)
91
- params.append('due_date_lt', String(filters.due_date_lt));
92
- if (filters.date_created_gt)
93
- params.append('date_created_gt', String(filters.date_created_gt));
94
- if (filters.date_created_lt)
95
- params.append('date_created_lt', String(filters.date_created_lt));
96
- if (filters.date_updated_gt)
97
- params.append('date_updated_gt', String(filters.date_updated_gt));
98
- if (filters.date_updated_lt)
99
- params.append('date_updated_lt', String(filters.date_updated_lt));
100
- // Handle custom fields if present
101
- if (filters.custom_fields) {
102
- Object.entries(filters.custom_fields).forEach(([key, value]) => {
103
- params.append(`custom_fields[${key}]`, String(value));
104
- });
105
- }
106
- return params;
107
- }
108
- /**
109
- * Extract priority value from a task
110
- * @param task The task to extract priority from
111
- * @returns TaskPriority or null
112
- */
113
- extractPriorityValue(task) {
114
- if (!task.priority || !task.priority.id) {
115
- return null;
116
- }
117
- const priorityValue = parseInt(task.priority.id);
118
- // Ensure it's in the valid range 1-4
119
- if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
120
- return null;
121
- }
122
- return priorityValue;
123
- }
124
- /**
125
- * Extract task data for creation/duplication
126
- * @param task The source task
127
- * @param nameOverride Optional override for the task name
128
- * @returns CreateTaskData object
129
- */
130
- extractTaskData(task, nameOverride) {
131
- return {
132
- name: nameOverride || task.name,
133
- description: task.description || '',
134
- status: task.status?.status,
135
- priority: this.extractPriorityValue(task),
136
- due_date: task.due_date ? Number(task.due_date) : undefined,
137
- assignees: task.assignees?.map(a => a.id) || []
138
- };
139
- }
140
- /**
141
- * Find a matching task by name using different matching strategies
142
- * @param tasks List of tasks to search
143
- * @param taskName Name to search for
144
- * @returns Matching task or null
145
- */
146
- findMatchingTask(tasks, taskName) {
147
- // Normalize the search term
148
- const normalizedSearchTerm = taskName.toLowerCase().trim();
149
- // First try exact match
150
- let matchingTask = tasks.find(task => task.name.toLowerCase().trim() === normalizedSearchTerm);
151
- // If no exact match, try substring match
152
- if (!matchingTask) {
153
- matchingTask = tasks.find(task => task.name.toLowerCase().trim().includes(normalizedSearchTerm) ||
154
- normalizedSearchTerm.includes(task.name.toLowerCase().trim()));
155
- }
156
- // If still no match and there are emoji characters, try matching without emoji
157
- if (!matchingTask && /[\p{Emoji}]/u.test(normalizedSearchTerm)) {
158
- matchingTask = this.findMatchingTaskWithoutEmoji(tasks, normalizedSearchTerm);
159
- }
160
- return matchingTask || null;
161
- }
162
- /**
163
- * Find matching task with emoji characters removed
164
- * @param tasks List of tasks to search
165
- * @param searchTerm Search term (with emoji)
166
- * @returns Matching task or null
167
- */
168
- findMatchingTaskWithoutEmoji(tasks, searchTerm) {
169
- // Remove emoji and try again (simple approximation)
170
- const withoutEmoji = searchTerm.replace(/[\p{Emoji}]/gu, '').trim();
171
- return tasks.find(task => {
172
- const taskNameWithoutEmoji = task.name.toLowerCase().replace(/[\p{Emoji}]/gu, '').trim();
173
- return taskNameWithoutEmoji === withoutEmoji ||
174
- taskNameWithoutEmoji.includes(withoutEmoji) ||
175
- withoutEmoji.includes(taskNameWithoutEmoji);
176
- }) || null;
177
- }
178
- /**
179
- * Create a new task in the specified list
180
- * @param listId The ID of the list to create the task in
181
- * @param taskData The data for the new task
182
- * @returns The created task
183
- */
184
- async createTask(listId, taskData) {
185
- this.logOperation('createTask', { listId, ...taskData });
186
- try {
187
- return await this.makeRequest(async () => {
188
- const response = await this.client.post(`/list/${listId}/task`, taskData);
189
- return response.data;
190
- });
191
- }
192
- catch (error) {
193
- throw this.handleError(error, `Failed to create task in list ${listId}`);
194
- }
195
- }
196
- /**
197
- * Get all tasks in a list with optional filtering
198
- * @param listId The ID of the list to get tasks from
199
- * @param filters Optional filters to apply
200
- * @returns List of tasks matching the filters
201
- */
202
- async getTasks(listId, filters = {}) {
203
- this.logOperation('getTasks', { listId, filters });
204
- try {
205
- return await this.makeRequest(async () => {
206
- const params = this.buildTaskFilterParams(filters);
207
- const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
208
- return response.data.tasks;
209
- });
210
- }
211
- catch (error) {
212
- throw this.handleError(error, `Failed to get tasks from list ${listId}`);
213
- }
214
- }
215
- /**
216
- * Get a specific task by ID
217
- * @param taskId The ID of the task to retrieve
218
- * @returns The task details
219
- */
220
- async getTask(taskId) {
221
- this.logOperation('getTask', { taskId });
222
- try {
223
- return await this.makeRequest(async () => {
224
- const response = await this.client.get(`/task/${taskId}`);
225
- return response.data;
226
- });
227
- }
228
- catch (error) {
229
- throw this.handleError(error, `Failed to get task ${taskId}`);
230
- }
231
- }
232
- /**
233
- * Get subtasks of a specific task
234
- * @param taskId The ID of the parent task
235
- * @returns Array of subtask details
236
- */
237
- async getSubtasks(taskId) {
238
- this.logOperation('getSubtasks', { taskId });
239
- try {
240
- return await this.makeRequest(async () => {
241
- // First, get the task to get its list ID
242
- const task = await this.getTask(taskId);
243
- const listId = task.list.id;
244
- // Then get all tasks from the list
245
- const allTasks = await this.getTasks(listId, { subtasks: true });
246
- // Filter tasks that have the specified task as parent
247
- return allTasks.filter(t => t.parent === taskId || t.top_level_parent === taskId);
248
- });
249
- }
250
- catch (error) {
251
- throw this.handleError(error, `Failed to get subtasks of task ${taskId}`);
252
- }
253
- }
254
- /**
255
- * Get a specific task by its custom ID
256
- * @param customTaskId The custom ID of the task (e.g., "DEV-1234")
257
- * @param listId Optional list ID to search within for better performance
258
- * @returns The task details
259
- */
260
- async getTaskByCustomId(customTaskId, listId) {
261
- this.logOperation('getTaskByCustomId', { customTaskId, listId });
262
- try {
263
- return await this.makeRequest(async () => {
264
- // Build query with custom_task_ids=true and team_id
265
- const params = new URLSearchParams({
266
- custom_task_ids: 'true',
267
- team_id: this.teamId
268
- });
269
- // Use the ClickUp API endpoint for retrieving tasks by ID
270
- // With custom_task_ids=true parameter, it will treat the ID as a custom ID
271
- const response = await this.client.get(`/task/${customTaskId}?${params.toString()}`);
272
- return response.data;
273
- });
274
- }
275
- catch (error) {
276
- // Enhance error message for custom ID lookups
277
- if (error?.response?.status === 404) {
278
- throw this.handleError(error, `Task with custom ID ${customTaskId} not found`);
279
- }
280
- throw this.handleError(error, `Failed to get task with custom ID ${customTaskId}`);
281
- }
282
- }
283
- /**
284
- * Update an existing task
285
- * @param taskId ID of the task to update
286
- * @param updateData Data to update on the task
287
- * @returns The updated task
288
- */
289
- async updateTask(taskId, updateData) {
290
- this.logOperation('updateTask', { taskId, ...updateData });
291
- try {
292
- return await this.makeRequest(async () => {
293
- const response = await this.client.put(`/task/${taskId}`, updateData);
294
- return response.data;
295
- });
296
- }
297
- catch (error) {
298
- throw this.handleError(error, `Failed to update task ${taskId}`);
299
- }
300
- }
301
- /**
302
- * Delete a task
303
- * @param taskId The ID of the task to delete
304
- * @returns Success indicator
305
- */
306
- async deleteTask(taskId) {
307
- this.logOperation('deleteTask', { taskId });
308
- try {
309
- await this.makeRequest(async () => {
310
- await this.client.delete(`/task/${taskId}`);
311
- });
312
- return {
313
- success: true
314
- };
315
- }
316
- catch (error) {
317
- throw this.handleError(error, `Failed to delete task ${taskId}`);
318
- }
319
- }
320
- /**
321
- * Find a task by name within a list
322
- * @param listId The list ID to search within
323
- * @param taskName The name of the task to find
324
- * @returns The task if found, otherwise null
325
- */
326
- async findTaskByName(listId, taskName) {
327
- this.logOperation('findTaskByName', { listId, taskName });
328
- try {
329
- const tasks = await this.getTasks(listId);
330
- return this.findMatchingTask(tasks, taskName);
331
- }
332
- catch (error) {
333
- throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
334
- }
335
- }
336
- /**
337
- * Move a task to a different list
338
- * @param taskId The ID of the task to move
339
- * @param destinationListId The ID of the list to move the task to
340
- * @returns The updated task
341
- */
342
- async moveTask(taskId, destinationListId) {
343
- this.logOperation('moveTask', { taskId, destinationListId });
344
- try {
345
- // First, get both the task and list info in parallel to save time
346
- const [originalTask, destinationList] = await Promise.all([
347
- this.getTask(taskId),
348
- this.listService.getList(destinationListId)
349
- ]);
350
- const currentStatus = originalTask.status?.status;
351
- const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
352
- // Determine the appropriate status for the destination list
353
- let newStatus = availableStatuses.includes(currentStatus || '')
354
- ? currentStatus // Keep the same status if available in destination list
355
- : destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
356
- // Prepare the task data for the new list
357
- const taskData = this.extractTaskData(originalTask);
358
- taskData.status = newStatus;
359
- // Create new task and delete old one in a single makeRequest call
360
- return await this.makeRequest(async () => {
361
- // First create the new task
362
- const response = await this.client.post(`/list/${destinationListId}/task`, taskData);
363
- // Then delete the original task
364
- await this.client.delete(`/task/${taskId}`);
365
- // Add a property to indicate the task was moved
366
- const newTask = {
367
- ...response.data,
368
- moved: true,
369
- originalId: taskId
370
- };
371
- return newTask;
372
- });
373
- }
374
- catch (error) {
375
- throw this.handleError(error, 'Failed to move task');
376
- }
377
- }
378
- /**
379
- * Create a duplicate of an existing task
380
- * @param taskId The ID of the task to duplicate
381
- * @param listId Optional destination list ID (defaults to the same list)
382
- * @returns The newly created duplicate task
383
- */
384
- async duplicateTask(taskId, listId) {
385
- this.logOperation('duplicateTask', { taskId, listId });
386
- try {
387
- // Get the original task to duplicate
388
- const originalTask = await this.getTask(taskId);
389
- // Create a copy of the task data with "(copy)" appended to the name
390
- const newTaskData = this.extractTaskData(originalTask, `${originalTask.name} (copy)`);
391
- // Create the new task in the specified list or original list
392
- const targetListId = listId || originalTask.list.id;
393
- return await this.createTask(targetListId, newTaskData);
394
- }
395
- catch (error) {
396
- throw this.handleError(error, 'Failed to duplicate task');
397
- }
398
- }
399
- /**
400
- * Get all comments for a task
401
- *
402
- * @param taskId ID of the task to get comments for
403
- * @param start Optional pagination start
404
- * @param startId Optional comment ID to start from
405
- * @returns Array of task comments
406
- */
407
- async getTaskComments(taskId, start, startId) {
408
- this.logOperation('getTaskComments', { taskId, start, startId });
409
- try {
410
- // Build query parameters for pagination
411
- const queryParams = new URLSearchParams();
412
- if (start !== undefined) {
413
- queryParams.append('start', start.toString());
414
- }
415
- if (startId) {
416
- queryParams.append('start_id', startId);
417
- }
418
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
419
- const response = await this.client.get(`/task/${taskId}/comment${queryString}`);
420
- return response.data.comments || [];
421
- }
422
- catch (error) {
423
- throw this.handleError(error, 'Failed to get task comments');
424
- }
425
- }
426
- /**
427
- * Create a comment on a task
428
- *
429
- * @param taskId ID of the task to comment on
430
- * @param commentText Text content of the comment
431
- * @param notifyAll Whether to notify all assignees
432
- * @param assignee Optional user ID to assign the comment to
433
- * @returns The created comment
434
- */
435
- async createTaskComment(taskId, commentText, notifyAll = false, assignee) {
436
- this.logOperation('createTaskComment', { taskId, commentText, notifyAll, assignee });
437
- try {
438
- const payload = {
439
- comment_text: commentText,
440
- notify_all: notifyAll
441
- };
442
- if (assignee) {
443
- payload.assignee = assignee;
444
- }
445
- // Make the request directly without using makeRequest for better error handling
446
- const response = await this.client.post(`/task/${taskId}/comment`, payload);
447
- // Handle different response formats from ClickUp API
448
- if (response.data) {
449
- if (response.data.comment) {
450
- // Standard format: { comment: ClickUpComment }
451
- return response.data.comment;
452
- }
453
- else if (response.data.id && (response.data.comment_text || response.data.comment)) {
454
- // Direct format: the comment object itself
455
- return response.data;
456
- }
457
- else {
458
- // Fallback: construct a minimal valid comment object
459
- return {
460
- id: response.data.id || `custom-${Date.now()}`,
461
- comment: response.data.comment || commentText,
462
- comment_text: response.data.comment_text || commentText,
463
- user: response.data.user || { id: 0, username: 'Unknown', email: '', color: '' },
464
- date: response.data.date || new Date().toISOString(),
465
- resolved: false
466
- };
467
- }
468
- }
469
- throw new Error('Invalid response from ClickUp API');
470
- }
471
- catch (error) {
472
- // Check if comment might have been created despite error
473
- if (error.response?.status === 200 || error.response?.status === 201) {
474
- // Try to construct a comment object from what we know
475
- return {
476
- id: `fallback-${Date.now()}`,
477
- comment: commentText,
478
- comment_text: commentText,
479
- user: { id: 0, username: 'Unknown', email: '', color: '' },
480
- date: new Date().toISOString(),
481
- resolved: false
482
- };
483
- }
484
- throw this.handleError(error, 'Failed to create task comment');
485
- }
486
- }
487
- /**
488
- * Validate that a list exists
489
- *
490
- * @param listId ID of the list to validate
491
- * @throws ClickUpServiceError if the list doesn't exist
492
- */
493
- async validateListExists(listId) {
494
- this.logOperation('validateListExists', { listId });
495
- try {
496
- const list = await this.listService.getList(listId);
497
- if (!list) {
498
- throw new ClickUpServiceError(`List not found: ${listId}`, ErrorCode.NOT_FOUND, { listId });
499
- }
500
- }
501
- catch (error) {
502
- if (error instanceof ClickUpServiceError) {
503
- throw error;
504
- }
505
- throw new ClickUpServiceError(`Failed to validate list existence: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.UNKNOWN, { listId });
506
- }
507
- }
508
- /**
509
- * Upload a file attachment to a ClickUp task
510
- * @param taskId The ID of the task to attach the file to
511
- * @param fileData The file data as a Buffer
512
- * @param fileName The name of the file
513
- * @returns Promise resolving to the attachment response from ClickUp
514
- */
515
- async uploadTaskAttachment(taskId, fileData, fileName) {
516
- this.logOperation('uploadTaskAttachment', { taskId, fileName, fileSize: fileData.length });
517
- try {
518
- return await this.makeRequest(async () => {
519
- // Create FormData for multipart/form-data upload
520
- const FormData = (await import('form-data')).default;
521
- const formData = new FormData();
522
- // Add the file to the form data
523
- formData.append('attachment', fileData, {
524
- filename: fileName,
525
- contentType: 'application/octet-stream' // Let ClickUp determine the content type
526
- });
527
- // Use the raw axios client for this request since we need to handle FormData
528
- const response = await this.client.post(`/task/${taskId}/attachment`, formData, {
529
- headers: {
530
- ...formData.getHeaders(),
531
- 'Authorization': this.apiKey
532
- }
533
- });
534
- return response.data;
535
- });
536
- }
537
- catch (error) {
538
- throw this.handleError(error, `Failed to upload attachment to task ${taskId}`);
539
- }
540
- }
541
- /**
542
- * Upload a file attachment to a ClickUp task from a URL
543
- * @param taskId The ID of the task to attach the file to
544
- * @param fileUrl The URL of the file to download and attach
545
- * @param fileName Optional file name (if not provided, it will be extracted from the URL)
546
- * @param authHeader Optional authorization header for the URL
547
- * @returns Promise resolving to the attachment response from ClickUp
548
- */
549
- async uploadTaskAttachmentFromUrl(taskId, fileUrl, fileName, authHeader) {
550
- this.logOperation('uploadTaskAttachmentFromUrl', { taskId, fileUrl, fileName });
551
- try {
552
- return await this.makeRequest(async () => {
553
- // Import required modules
554
- const axios = (await import('axios')).default;
555
- const FormData = (await import('form-data')).default;
556
- // Download the file from the URL
557
- const headers = {};
558
- if (authHeader) {
559
- headers['Authorization'] = authHeader;
560
- }
561
- const response = await axios.get(fileUrl, {
562
- responseType: 'arraybuffer',
563
- headers
564
- });
565
- // Extract filename from URL if not provided
566
- const actualFileName = fileName || fileUrl.split('/').pop() || 'downloaded-file';
567
- // Create FormData for multipart/form-data upload
568
- const formData = new FormData();
569
- // Add the file to the form data
570
- formData.append('attachment', Buffer.from(response.data), {
571
- filename: actualFileName,
572
- contentType: 'application/octet-stream'
573
- });
574
- // Upload the file to ClickUp
575
- const uploadResponse = await this.client.post(`/task/${taskId}/attachment`, formData, {
576
- headers: {
577
- ...formData.getHeaders(),
578
- 'Authorization': this.apiKey
579
- }
580
- });
581
- return uploadResponse.data;
582
- });
583
- }
584
- catch (error) {
585
- throw this.handleError(error, `Failed to upload attachment from URL to task ${taskId}`);
586
- }
587
- }
588
- /**
589
- * Format task data for summary view
590
- * @param task The task to format
591
- * @returns TaskSummary object
592
- */
593
- formatTaskSummary(task) {
594
- return {
595
- id: task.id,
596
- name: task.name,
597
- status: task.status.status,
598
- list: {
599
- id: task.list.id,
600
- name: task.list.name
601
- },
602
- due_date: task.due_date,
603
- url: task.url,
604
- priority: this.extractPriorityValue(task),
605
- tags: task.tags.map(tag => ({
606
- name: tag.name,
607
- tag_bg: tag.tag_bg,
608
- tag_fg: tag.tag_fg
609
- }))
610
- };
611
- }
612
- /**
613
- * Estimates token count for a task in JSON format
614
- * @param task ClickUp task
615
- * @returns Estimated token count
616
- */
617
- estimateTaskTokens(task) {
618
- return estimateTokensFromObject(task);
619
- }
620
- /**
621
- * Get filtered tasks across the entire team/workspace using tags and other filters
622
- * @param filters Task filters to apply including tags, list/folder/space filtering
623
- * @returns Either a DetailedTaskResponse or WorkspaceTasksResponse depending on detail_level
624
- */
625
- async getWorkspaceTasks(filters = {}) {
626
- try {
627
- this.logOperation('getWorkspaceTasks', { filters });
628
- const params = this.buildTaskFilterParams(filters);
629
- const response = await this.client.get(`/team/${this.teamId}/task`, {
630
- params
631
- });
632
- const tasks = response.data.tasks;
633
- const totalCount = tasks.length; // Note: This is just the current page count
634
- const hasMore = totalCount === 100; // ClickUp returns max 100 tasks per page
635
- const nextPage = (filters.page || 0) + 1;
636
- // If the estimated token count exceeds 50,000 or detail_level is 'summary',
637
- // return summary format for efficiency and to avoid hitting token limits
638
- const TOKEN_LIMIT = 50000;
639
- // Estimate tokens for the full response
640
- let tokensExceedLimit = false;
641
- if (filters.detail_level !== 'summary' && tasks.length > 0) {
642
- // We only need to check token count if detailed was requested
643
- // For summary requests, we always return summary format
644
- // First check with a sample task - if one task exceeds the limit, we definitely need summary
645
- const sampleTask = tasks[0];
646
- // Check if all tasks would exceed the token limit
647
- const estimatedTokensPerTask = this.estimateTaskTokens(sampleTask);
648
- const estimatedTotalTokens = estimatedTokensPerTask * tasks.length;
649
- // Add 10% overhead for the response wrapper
650
- tokensExceedLimit = estimatedTotalTokens * 1.1 > TOKEN_LIMIT;
651
- // Double-check with more precise estimation if we're close to the limit
652
- if (!tokensExceedLimit && estimatedTotalTokens * 1.1 > TOKEN_LIMIT * 0.8) {
653
- // More precise check - build a representative sample and extrapolate
654
- tokensExceedLimit = wouldExceedTokenLimit({ tasks, total_count: totalCount, has_more: hasMore, next_page: nextPage }, TOKEN_LIMIT);
655
- }
656
- }
657
- // Determine if we should return summary or detailed based on request and token limit
658
- const shouldUseSummary = filters.detail_level === 'summary' || tokensExceedLimit;
659
- this.logOperation('getWorkspaceTasks', {
660
- totalTasks: tasks.length,
661
- estimatedTokens: tasks.reduce((count, task) => count + this.estimateTaskTokens(task), 0),
662
- usingDetailedFormat: !shouldUseSummary,
663
- requestedFormat: filters.detail_level || 'auto'
664
- });
665
- if (shouldUseSummary) {
666
- return {
667
- summaries: tasks.map(task => this.formatTaskSummary(task)),
668
- total_count: totalCount,
669
- has_more: hasMore,
670
- next_page: nextPage
671
- };
672
- }
673
- return {
674
- tasks,
675
- total_count: totalCount,
676
- has_more: hasMore,
677
- next_page: nextPage
678
- };
679
- }
680
- catch (error) {
681
- this.logOperation('getWorkspaceTasks', { error: error.message, status: error.response?.status });
682
- throw this.handleError(error, 'Failed to get workspace tasks');
683
- }
684
- }
685
- /**
686
- * Get task summaries for lightweight retrieval
687
- * @param filters Task filters to apply
688
- * @returns WorkspaceTasksResponse with task summaries
689
- */
690
- async getTaskSummaries(filters = {}) {
691
- return this.getWorkspaceTasks({ ...filters, detail_level: 'summary' });
692
- }
693
- /**
694
- * Get detailed task data
695
- * @param filters Task filters to apply
696
- * @returns DetailedTaskResponse with full task data
697
- */
698
- async getTaskDetails(filters = {}) {
699
- return this.getWorkspaceTasks({ ...filters, detail_level: 'detailed' });
700
- }
701
- }