@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.
- package/README.md +6 -6
- package/build/logger.js +26 -1
- package/build/server.js +1 -1
- package/build/services/clickup/base.js +22 -1
- package/build/services/clickup/bulk.js +76 -45
- package/build/services/clickup/index.js +2 -2
- package/build/services/clickup/task/index.js +32 -0
- package/build/services/clickup/task/task-attachments.js +97 -0
- package/build/services/clickup/task/task-comments.js +104 -0
- package/build/services/clickup/task/task-core.js +477 -0
- package/build/services/clickup/task/task-custom-fields.js +97 -0
- package/build/services/clickup/task/task-search.js +462 -0
- package/build/services/clickup/task/task-service.js +25 -0
- package/build/services/clickup/task/task-tags.js +101 -0
- package/build/services/clickup/workspace.js +81 -36
- package/build/tools/folder.js +1 -1
- package/build/tools/list.js +2 -4
- package/build/tools/task/attachments.js +18 -5
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +111 -15
- package/build/tools/task/handlers.js +169 -24
- package/build/tools/task/index.js +1 -1
- package/build/tools/task/main.js +36 -1
- package/build/tools/task/single-operations.js +51 -4
- package/build/tools/task/utilities.js +24 -71
- package/build/tools/utils.js +2 -2
- package/build/utils/date-utils.js +149 -30
- package/build/utils/resolver-utils.js +33 -40
- 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 -0
- package/build/services/clickup/task.js +0 -701
- 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
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* ClickUp Task Service - Search Module
|
|
6
|
+
*
|
|
7
|
+
* Handles search and lookup operations for tasks in ClickUp, including:
|
|
8
|
+
* - Finding tasks by name
|
|
9
|
+
* - Global workspace task lookup
|
|
10
|
+
* - Task summaries and detailed task data
|
|
11
|
+
*/
|
|
12
|
+
import { TaskServiceCore } from './task-core.js';
|
|
13
|
+
import { isNameMatch } from '../../../utils/resolver-utils.js';
|
|
14
|
+
import { findListIDByName } from '../../../tools/list.js';
|
|
15
|
+
import { estimateTokensFromObject, wouldExceedTokenLimit } from '../../../utils/token-utils.js';
|
|
16
|
+
/**
|
|
17
|
+
* Search functionality for the TaskService
|
|
18
|
+
*/
|
|
19
|
+
export class TaskServiceSearch extends TaskServiceCore {
|
|
20
|
+
/**
|
|
21
|
+
* Find a task by name within a specific list
|
|
22
|
+
* @param listId The ID of the list to search in
|
|
23
|
+
* @param taskName The name of the task to find
|
|
24
|
+
* @returns The task if found, otherwise null
|
|
25
|
+
*/
|
|
26
|
+
async findTaskByName(listId, taskName) {
|
|
27
|
+
this.logOperation('findTaskByName', { listId, taskName });
|
|
28
|
+
try {
|
|
29
|
+
const tasks = await this.getTasks(listId);
|
|
30
|
+
// Find task by exact match first, then case-insensitive, then substring
|
|
31
|
+
// Exact match
|
|
32
|
+
let match = tasks.find(task => task.name === taskName);
|
|
33
|
+
if (match)
|
|
34
|
+
return match;
|
|
35
|
+
// Case-insensitive match
|
|
36
|
+
match = tasks.find(task => task.name.toLowerCase() === taskName.toLowerCase());
|
|
37
|
+
if (match)
|
|
38
|
+
return match;
|
|
39
|
+
// Substring match
|
|
40
|
+
match = tasks.find(task => task.name.toLowerCase().includes(taskName.toLowerCase()));
|
|
41
|
+
return match || null;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Formats a task into a lightweight summary format
|
|
49
|
+
* @param task The task to format
|
|
50
|
+
* @returns A TaskSummary object
|
|
51
|
+
*/
|
|
52
|
+
formatTaskSummary(task) {
|
|
53
|
+
return {
|
|
54
|
+
id: task.id,
|
|
55
|
+
name: task.name,
|
|
56
|
+
status: task.status.status,
|
|
57
|
+
list: {
|
|
58
|
+
id: task.list.id,
|
|
59
|
+
name: task.list.name
|
|
60
|
+
},
|
|
61
|
+
due_date: task.due_date,
|
|
62
|
+
url: task.url,
|
|
63
|
+
priority: this.extractPriorityValue(task),
|
|
64
|
+
tags: task.tags.map(tag => ({
|
|
65
|
+
name: tag.name,
|
|
66
|
+
tag_bg: tag.tag_bg,
|
|
67
|
+
tag_fg: tag.tag_fg
|
|
68
|
+
}))
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Estimates token count for a task in JSON format
|
|
73
|
+
* @param task ClickUp task
|
|
74
|
+
* @returns Estimated token count
|
|
75
|
+
*/
|
|
76
|
+
estimateTaskTokens(task) {
|
|
77
|
+
return estimateTokensFromObject(task);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get filtered tasks across the entire team/workspace using tags and other filters
|
|
81
|
+
* @param filters Task filters to apply including tags, list/folder/space filtering
|
|
82
|
+
* @returns Either a DetailedTaskResponse or WorkspaceTasksResponse depending on detail_level
|
|
83
|
+
*/
|
|
84
|
+
async getWorkspaceTasks(filters = {}) {
|
|
85
|
+
try {
|
|
86
|
+
this.logOperation('getWorkspaceTasks', { filters });
|
|
87
|
+
const params = this.buildTaskFilterParams(filters);
|
|
88
|
+
const response = await this.makeRequest(async () => {
|
|
89
|
+
return await this.client.get(`/team/${this.teamId}/task`, {
|
|
90
|
+
params
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
const tasks = response.data.tasks;
|
|
94
|
+
const totalCount = tasks.length; // Note: This is just the current page count
|
|
95
|
+
const hasMore = totalCount === 100; // ClickUp returns max 100 tasks per page
|
|
96
|
+
const nextPage = (filters.page || 0) + 1;
|
|
97
|
+
// If the estimated token count exceeds 50,000 or detail_level is 'summary',
|
|
98
|
+
// return summary format for efficiency and to avoid hitting token limits
|
|
99
|
+
const TOKEN_LIMIT = 50000;
|
|
100
|
+
// Estimate tokens for the full response
|
|
101
|
+
let tokensExceedLimit = false;
|
|
102
|
+
if (filters.detail_level !== 'summary' && tasks.length > 0) {
|
|
103
|
+
// We only need to check token count if detailed was requested
|
|
104
|
+
// For summary requests, we always return summary format
|
|
105
|
+
// First check with a sample task - if one task exceeds the limit, we definitely need summary
|
|
106
|
+
const sampleTask = tasks[0];
|
|
107
|
+
// Check if all tasks would exceed the token limit
|
|
108
|
+
const estimatedTokensPerTask = this.estimateTaskTokens(sampleTask);
|
|
109
|
+
const estimatedTotalTokens = estimatedTokensPerTask * tasks.length;
|
|
110
|
+
// Add 10% overhead for the response wrapper
|
|
111
|
+
tokensExceedLimit = estimatedTotalTokens * 1.1 > TOKEN_LIMIT;
|
|
112
|
+
// Double-check with more precise estimation if we're close to the limit
|
|
113
|
+
if (!tokensExceedLimit && estimatedTotalTokens * 1.1 > TOKEN_LIMIT * 0.8) {
|
|
114
|
+
// More precise check - build a representative sample and extrapolate
|
|
115
|
+
tokensExceedLimit = wouldExceedTokenLimit({ tasks, total_count: totalCount, has_more: hasMore, next_page: nextPage }, TOKEN_LIMIT);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Determine if we should return summary or detailed based on request and token limit
|
|
119
|
+
const shouldUseSummary = filters.detail_level === 'summary' || tokensExceedLimit;
|
|
120
|
+
this.logOperation('getWorkspaceTasks', {
|
|
121
|
+
totalTasks: tasks.length,
|
|
122
|
+
estimatedTokens: tasks.reduce((count, task) => count + this.estimateTaskTokens(task), 0),
|
|
123
|
+
usingDetailedFormat: !shouldUseSummary,
|
|
124
|
+
requestedFormat: filters.detail_level || 'auto'
|
|
125
|
+
});
|
|
126
|
+
if (shouldUseSummary) {
|
|
127
|
+
return {
|
|
128
|
+
summaries: tasks.map(task => this.formatTaskSummary(task)),
|
|
129
|
+
total_count: totalCount,
|
|
130
|
+
has_more: hasMore,
|
|
131
|
+
next_page: nextPage
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
tasks,
|
|
136
|
+
total_count: totalCount,
|
|
137
|
+
has_more: hasMore,
|
|
138
|
+
next_page: nextPage
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
this.logOperation('getWorkspaceTasks', { error: error.message, status: error.response?.status });
|
|
143
|
+
throw this.handleError(error, 'Failed to get workspace tasks');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get task summaries for lightweight retrieval
|
|
148
|
+
* @param filters Task filters to apply
|
|
149
|
+
* @returns WorkspaceTasksResponse with task summaries
|
|
150
|
+
*/
|
|
151
|
+
async getTaskSummaries(filters = {}) {
|
|
152
|
+
return this.getWorkspaceTasks({ ...filters, detail_level: 'summary' });
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get detailed task data
|
|
156
|
+
* @param filters Task filters to apply
|
|
157
|
+
* @returns DetailedTaskResponse with full task data
|
|
158
|
+
*/
|
|
159
|
+
async getTaskDetails(filters = {}) {
|
|
160
|
+
return this.getWorkspaceTasks({ ...filters, detail_level: 'detailed' });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Unified method for finding tasks by ID or name with consistent handling of global lookup
|
|
164
|
+
*
|
|
165
|
+
* This method provides a single entry point for all task lookup operations:
|
|
166
|
+
* - Direct lookup by task ID (highest priority)
|
|
167
|
+
* - Lookup by task name within a specific list
|
|
168
|
+
* - Global lookup by task name across the entire workspace
|
|
169
|
+
*
|
|
170
|
+
* @param options Lookup options with the following parameters:
|
|
171
|
+
* - taskId: Optional task ID for direct lookup
|
|
172
|
+
* - customTaskId: Optional custom task ID for direct lookup
|
|
173
|
+
* - taskName: Optional task name to search for
|
|
174
|
+
* - listId: Optional list ID to scope the search
|
|
175
|
+
* - listName: Optional list name to scope the search
|
|
176
|
+
* - allowMultipleMatches: Whether to return all matches instead of throwing an error
|
|
177
|
+
* - useSmartDisambiguation: Whether to automatically select the most recently updated task
|
|
178
|
+
* - includeFullDetails: Whether to include full task details (true) or just task summaries (false)
|
|
179
|
+
* - includeListContext: Whether to include list/folder/space context with results
|
|
180
|
+
* @returns Either a single task or an array of tasks depending on options
|
|
181
|
+
* @throws Error if task cannot be found or if multiple matches are found when not allowed
|
|
182
|
+
*/
|
|
183
|
+
async findTasks({ taskId, customTaskId, taskName, listId, listName, allowMultipleMatches = false, useSmartDisambiguation = false, includeFullDetails = true, includeListContext = true }) {
|
|
184
|
+
try {
|
|
185
|
+
this.logOperation('findTasks', {
|
|
186
|
+
taskId,
|
|
187
|
+
customTaskId,
|
|
188
|
+
taskName,
|
|
189
|
+
listId,
|
|
190
|
+
listName,
|
|
191
|
+
allowMultipleMatches,
|
|
192
|
+
useSmartDisambiguation
|
|
193
|
+
});
|
|
194
|
+
// Case 1: Direct task ID lookup (highest priority)
|
|
195
|
+
if (taskId) {
|
|
196
|
+
// Check if it looks like a custom ID
|
|
197
|
+
if (taskId.includes('-') && /^[A-Z]+\-\d+$/.test(taskId)) {
|
|
198
|
+
this.logOperation('findTasks', { detectedCustomId: taskId });
|
|
199
|
+
try {
|
|
200
|
+
// Try to get it as a custom ID first
|
|
201
|
+
let resolvedListId;
|
|
202
|
+
if (listId) {
|
|
203
|
+
resolvedListId = listId;
|
|
204
|
+
}
|
|
205
|
+
else if (listName) {
|
|
206
|
+
const listInfo = await findListIDByName(this.workspaceService, listName);
|
|
207
|
+
if (listInfo) {
|
|
208
|
+
resolvedListId = listInfo.id;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const foundTask = await this.getTaskByCustomId(taskId, resolvedListId);
|
|
212
|
+
return foundTask;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// If it fails as a custom ID, try as a regular ID
|
|
216
|
+
this.logOperation('findTasks', {
|
|
217
|
+
message: `Failed to find task with custom ID "${taskId}", falling back to regular ID`,
|
|
218
|
+
error: error.message
|
|
219
|
+
});
|
|
220
|
+
return await this.getTask(taskId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Regular task ID
|
|
224
|
+
return await this.getTask(taskId);
|
|
225
|
+
}
|
|
226
|
+
// Case 2: Explicit custom task ID lookup
|
|
227
|
+
if (customTaskId) {
|
|
228
|
+
let resolvedListId;
|
|
229
|
+
if (listId) {
|
|
230
|
+
resolvedListId = listId;
|
|
231
|
+
}
|
|
232
|
+
else if (listName) {
|
|
233
|
+
const listInfo = await findListIDByName(this.workspaceService, listName);
|
|
234
|
+
if (listInfo) {
|
|
235
|
+
resolvedListId = listInfo.id;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return await this.getTaskByCustomId(customTaskId, resolvedListId);
|
|
239
|
+
}
|
|
240
|
+
// Case 3: Task name lookup (requires either list context or global lookup)
|
|
241
|
+
if (taskName) {
|
|
242
|
+
// Case 3a: Task name + list context - search in specific list
|
|
243
|
+
if (listId || listName) {
|
|
244
|
+
let resolvedListId;
|
|
245
|
+
if (listId) {
|
|
246
|
+
resolvedListId = listId;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const listInfo = await findListIDByName(this.workspaceService, listName);
|
|
250
|
+
if (!listInfo) {
|
|
251
|
+
throw new Error(`List "${listName}" not found`);
|
|
252
|
+
}
|
|
253
|
+
resolvedListId = listInfo.id;
|
|
254
|
+
}
|
|
255
|
+
const foundTask = await this.findTaskByName(resolvedListId, taskName);
|
|
256
|
+
if (!foundTask) {
|
|
257
|
+
throw new Error(`Task "${taskName}" not found in list`);
|
|
258
|
+
}
|
|
259
|
+
// If includeFullDetails is true and we need context not already in the task,
|
|
260
|
+
// get full details, otherwise return what we already have
|
|
261
|
+
if (includeFullDetails && (!foundTask.list || !foundTask.list.name || !foundTask.status)) {
|
|
262
|
+
return await this.getTask(foundTask.id);
|
|
263
|
+
}
|
|
264
|
+
return foundTask;
|
|
265
|
+
}
|
|
266
|
+
// Case 3b: Task name without list context - global lookup across workspace
|
|
267
|
+
// Get lightweight task summaries for efficient first-pass filtering
|
|
268
|
+
const response = await this.getTaskSummaries({});
|
|
269
|
+
if (!this.workspaceService) {
|
|
270
|
+
throw new Error("Workspace service required for global task lookup");
|
|
271
|
+
}
|
|
272
|
+
// Create an index to efficiently look up list context information
|
|
273
|
+
const hierarchy = await this.workspaceService.getWorkspaceHierarchy();
|
|
274
|
+
const listContextMap = new Map();
|
|
275
|
+
// Function to recursively build list context map
|
|
276
|
+
function buildListContextMap(nodes, spaceId, spaceName, folderId, folderName) {
|
|
277
|
+
for (const node of nodes) {
|
|
278
|
+
if (node.type === 'space') {
|
|
279
|
+
// Process space children
|
|
280
|
+
if (node.children) {
|
|
281
|
+
buildListContextMap(node.children, node.id, node.name);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else if (node.type === 'folder') {
|
|
285
|
+
// Process folder children
|
|
286
|
+
if (node.children) {
|
|
287
|
+
buildListContextMap(node.children, spaceId, spaceName, node.id, node.name);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (node.type === 'list') {
|
|
291
|
+
// Add list context to map
|
|
292
|
+
listContextMap.set(node.id, {
|
|
293
|
+
listId: node.id,
|
|
294
|
+
listName: node.name,
|
|
295
|
+
spaceId: spaceId,
|
|
296
|
+
spaceName: spaceName,
|
|
297
|
+
folderId,
|
|
298
|
+
folderName
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Build the context map
|
|
304
|
+
buildListContextMap(hierarchy.root.children);
|
|
305
|
+
// Find tasks that match the provided name
|
|
306
|
+
const initialMatches = [];
|
|
307
|
+
// Process task summaries to find initial matches
|
|
308
|
+
for (const taskSummary of response.summaries) {
|
|
309
|
+
if (isNameMatch(taskSummary.name, taskName)) {
|
|
310
|
+
// Get list context information
|
|
311
|
+
const listContext = listContextMap.get(taskSummary.list.id);
|
|
312
|
+
if (listContext) {
|
|
313
|
+
// Store task summary and context
|
|
314
|
+
initialMatches.push({
|
|
315
|
+
id: taskSummary.id,
|
|
316
|
+
task: taskSummary,
|
|
317
|
+
listContext
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Handle the no matches case
|
|
323
|
+
if (initialMatches.length === 0) {
|
|
324
|
+
throw new Error(`Task "${taskName}" not found in any list across your workspace. Please check the task name and try again.`);
|
|
325
|
+
}
|
|
326
|
+
// Handle the single match case - we can return early if we don't need full details
|
|
327
|
+
if (initialMatches.length === 1 && !useSmartDisambiguation && !includeFullDetails) {
|
|
328
|
+
const match = initialMatches[0];
|
|
329
|
+
if (includeListContext) {
|
|
330
|
+
return {
|
|
331
|
+
...match.task,
|
|
332
|
+
list: {
|
|
333
|
+
id: match.listContext.listId,
|
|
334
|
+
name: match.listContext.listName
|
|
335
|
+
},
|
|
336
|
+
folder: match.listContext.folderId ? {
|
|
337
|
+
id: match.listContext.folderId,
|
|
338
|
+
name: match.listContext.folderName
|
|
339
|
+
} : undefined,
|
|
340
|
+
space: {
|
|
341
|
+
id: match.listContext.spaceId,
|
|
342
|
+
name: match.listContext.spaceName
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return match.task;
|
|
347
|
+
}
|
|
348
|
+
// For multiple matches or when we need details, fetch full task info
|
|
349
|
+
const fullMatches = [];
|
|
350
|
+
try {
|
|
351
|
+
// Process in sequence for better reliability
|
|
352
|
+
for (const match of initialMatches) {
|
|
353
|
+
const fullTask = await this.getTask(match.id);
|
|
354
|
+
if (includeListContext) {
|
|
355
|
+
// Enhance task with context information
|
|
356
|
+
fullTask.list = {
|
|
357
|
+
...fullTask.list,
|
|
358
|
+
name: match.listContext.listName
|
|
359
|
+
};
|
|
360
|
+
if (match.listContext.folderId) {
|
|
361
|
+
fullTask.folder = {
|
|
362
|
+
id: match.listContext.folderId,
|
|
363
|
+
name: match.listContext.folderName
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
fullTask.space = {
|
|
367
|
+
id: match.listContext.spaceId,
|
|
368
|
+
name: match.listContext.spaceName
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
fullMatches.push(fullTask);
|
|
372
|
+
}
|
|
373
|
+
// Sort by update time for disambiguation
|
|
374
|
+
if (fullMatches.length > 1) {
|
|
375
|
+
fullMatches.sort((a, b) => {
|
|
376
|
+
const aDate = parseInt(a.date_updated || '0', 10);
|
|
377
|
+
const bDate = parseInt(b.date_updated || '0', 10);
|
|
378
|
+
return bDate - aDate; // Most recent first
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
this.logOperation('findTasks', {
|
|
384
|
+
error: error.message,
|
|
385
|
+
message: "Failed to get detailed task information"
|
|
386
|
+
});
|
|
387
|
+
// If detailed fetch fails, use the summaries with context info
|
|
388
|
+
// This fallback ensures we still return something useful
|
|
389
|
+
if (allowMultipleMatches) {
|
|
390
|
+
return initialMatches.map(match => ({
|
|
391
|
+
...match.task,
|
|
392
|
+
list: {
|
|
393
|
+
id: match.listContext.listId,
|
|
394
|
+
name: match.listContext.listName
|
|
395
|
+
},
|
|
396
|
+
folder: match.listContext.folderId ? {
|
|
397
|
+
id: match.listContext.folderId,
|
|
398
|
+
name: match.listContext.folderName
|
|
399
|
+
} : undefined,
|
|
400
|
+
space: {
|
|
401
|
+
id: match.listContext.spaceId,
|
|
402
|
+
name: match.listContext.spaceName
|
|
403
|
+
}
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// For single result, return the first match
|
|
408
|
+
const match = initialMatches[0];
|
|
409
|
+
return {
|
|
410
|
+
...match.task,
|
|
411
|
+
list: {
|
|
412
|
+
id: match.listContext.listId,
|
|
413
|
+
name: match.listContext.listName
|
|
414
|
+
},
|
|
415
|
+
folder: match.listContext.folderId ? {
|
|
416
|
+
id: match.listContext.folderId,
|
|
417
|
+
name: match.listContext.folderName
|
|
418
|
+
} : undefined,
|
|
419
|
+
space: {
|
|
420
|
+
id: match.listContext.spaceId,
|
|
421
|
+
name: match.listContext.spaceName
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Return results based on options
|
|
427
|
+
if (fullMatches.length === 1 || useSmartDisambiguation) {
|
|
428
|
+
return fullMatches[0]; // Return most recently updated if multiple and smart disambiguation enabled
|
|
429
|
+
}
|
|
430
|
+
else if (allowMultipleMatches) {
|
|
431
|
+
return fullMatches; // Return all matches
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
// Format error message for multiple matches
|
|
435
|
+
const matchesInfo = fullMatches.map(task => {
|
|
436
|
+
const listName = task.list?.name || "Unknown list";
|
|
437
|
+
const folderName = task.folder?.name;
|
|
438
|
+
const spaceName = task.space?.name || "Unknown space";
|
|
439
|
+
const updateTime = task.date_updated
|
|
440
|
+
? new Date(parseInt(task.date_updated, 10)).toLocaleString()
|
|
441
|
+
: "Unknown date";
|
|
442
|
+
const location = `list "${listName}"${folderName ? ` (folder: "${folderName}")` : ''} (space: "${spaceName}")`;
|
|
443
|
+
return `- "${task.name}" in ${location} - Updated ${updateTime}`;
|
|
444
|
+
}).join('\n');
|
|
445
|
+
throw new Error(`Multiple tasks found with name "${taskName}":\n${matchesInfo}\n\nPlease provide list context to disambiguate or set allowMultipleMatches to true.`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// No valid lookup parameters provided
|
|
449
|
+
throw new Error("At least one of taskId, customTaskId, or taskName must be provided");
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (error.message?.includes('Task "') && error.message?.includes('not found')) {
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
if (error.message?.includes('Multiple tasks found')) {
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
// Unexpected errors
|
|
459
|
+
throw this.handleError(error, `Error finding task: ${error.message}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* ClickUp Task Service
|
|
6
|
+
*
|
|
7
|
+
* Main entry point for the ClickUp Task Service.
|
|
8
|
+
* Combines all task-related functionality through inheritance:
|
|
9
|
+
* - Core operations (CRUD)
|
|
10
|
+
* - Search capabilities
|
|
11
|
+
* - File attachments
|
|
12
|
+
* - Comments
|
|
13
|
+
* - Tags
|
|
14
|
+
* - Custom fields
|
|
15
|
+
*/
|
|
16
|
+
import { TaskServiceCustomFields } from './task-custom-fields.js';
|
|
17
|
+
/**
|
|
18
|
+
* Complete TaskService combining all task-related functionality
|
|
19
|
+
*/
|
|
20
|
+
export class TaskService extends TaskServiceCustomFields {
|
|
21
|
+
constructor(apiKey, teamId, baseUrl, workspaceService) {
|
|
22
|
+
super(apiKey, teamId, baseUrl, workspaceService);
|
|
23
|
+
this.logOperation('constructor', { initialized: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* ClickUp Task Service - Tags Module
|
|
6
|
+
*
|
|
7
|
+
* Handles tag operations for ClickUp tasks, including:
|
|
8
|
+
* - Adding tags to a task
|
|
9
|
+
* - Removing tags from a task
|
|
10
|
+
*/
|
|
11
|
+
import { TaskServiceComments } from './task-comments.js';
|
|
12
|
+
/**
|
|
13
|
+
* Tags functionality for the TaskService
|
|
14
|
+
*/
|
|
15
|
+
export class TaskServiceTags extends TaskServiceComments {
|
|
16
|
+
/**
|
|
17
|
+
* Add a tag to a task
|
|
18
|
+
*
|
|
19
|
+
* @param taskId ID of the task
|
|
20
|
+
* @param tagName Name of the tag to add
|
|
21
|
+
* @returns Success response
|
|
22
|
+
*/
|
|
23
|
+
async addTagToTask(taskId, tagName) {
|
|
24
|
+
this.logOperation('addTagToTask', { taskId, tagName });
|
|
25
|
+
try {
|
|
26
|
+
const payload = {
|
|
27
|
+
tag_name: tagName,
|
|
28
|
+
};
|
|
29
|
+
await this.client.post(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`, payload);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw this.handleError(error, `Failed to add tag "${tagName}" to task`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Remove a tag from a task
|
|
38
|
+
*
|
|
39
|
+
* @param taskId ID of the task
|
|
40
|
+
* @param tagName Name of the tag to remove
|
|
41
|
+
* @returns Success response
|
|
42
|
+
*/
|
|
43
|
+
async removeTagFromTask(taskId, tagName) {
|
|
44
|
+
this.logOperation('removeTagFromTask', { taskId, tagName });
|
|
45
|
+
try {
|
|
46
|
+
await this.client.delete(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw this.handleError(error, `Failed to remove tag "${tagName}" from task`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get all tags for a task
|
|
55
|
+
*
|
|
56
|
+
* @param taskId ID of the task
|
|
57
|
+
* @returns Array of task tags
|
|
58
|
+
*/
|
|
59
|
+
async getTaskTags(taskId) {
|
|
60
|
+
this.logOperation('getTaskTags', { taskId });
|
|
61
|
+
try {
|
|
62
|
+
// We need to fetch the full task to get its tags
|
|
63
|
+
const task = await this.getTask(taskId);
|
|
64
|
+
return task.tags || [];
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
throw this.handleError(error, 'Failed to get task tags');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Update all tags for a task (replaces existing tags)
|
|
72
|
+
*
|
|
73
|
+
* @param taskId ID of the task
|
|
74
|
+
* @param tagNames Array of tag names to set
|
|
75
|
+
* @returns Success response
|
|
76
|
+
*/
|
|
77
|
+
async updateTaskTags(taskId, tagNames) {
|
|
78
|
+
this.logOperation('updateTaskTags', { taskId, tagNames });
|
|
79
|
+
try {
|
|
80
|
+
// First get existing tags
|
|
81
|
+
const existingTags = await this.getTaskTags(taskId);
|
|
82
|
+
const existingTagNames = existingTags.map(tag => tag.name);
|
|
83
|
+
// Remove tags that shouldn't be there
|
|
84
|
+
for (const tagName of existingTagNames) {
|
|
85
|
+
if (!tagNames.includes(tagName)) {
|
|
86
|
+
await this.removeTagFromTask(taskId, tagName);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Add new tags
|
|
90
|
+
for (const tagName of tagNames) {
|
|
91
|
+
if (!existingTagNames.includes(tagName)) {
|
|
92
|
+
await this.addTagToTask(taskId, tagName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw this.handleError(error, 'Failed to update task tags');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|