@taazkareem/clickup-mcp-server 0.6.1 → 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/index.js +1 -0
- 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/services/clickup/task.js +0 -701
|
@@ -30,6 +30,31 @@ export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0
|
|
|
30
30
|
now.setMonth(now.getMonth() + months);
|
|
31
31
|
return now.getTime();
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the start of today (midnight) in Unix milliseconds
|
|
35
|
+
* @returns Timestamp in milliseconds for start of current day
|
|
36
|
+
*/
|
|
37
|
+
export function getStartOfDay() {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
now.setHours(0, 0, 0, 0);
|
|
40
|
+
return now.getTime();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the end of today (23:59:59.999) in Unix milliseconds
|
|
44
|
+
* @returns Timestamp in milliseconds for end of current day
|
|
45
|
+
*/
|
|
46
|
+
export function getEndOfDay() {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
now.setHours(23, 59, 59, 999);
|
|
49
|
+
return now.getTime();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get the current time in Unix milliseconds
|
|
53
|
+
* @returns Current timestamp in milliseconds
|
|
54
|
+
*/
|
|
55
|
+
export function getCurrentTimestamp() {
|
|
56
|
+
return new Date().getTime();
|
|
57
|
+
}
|
|
33
58
|
/**
|
|
34
59
|
* Parse a due date string into a timestamp
|
|
35
60
|
* Supports ISO 8601 format or natural language like "tomorrow"
|
|
@@ -42,15 +67,36 @@ export function parseDueDate(dateString) {
|
|
|
42
67
|
return undefined;
|
|
43
68
|
try {
|
|
44
69
|
// Handle natural language dates
|
|
45
|
-
const lowerDate = dateString.toLowerCase();
|
|
46
|
-
|
|
70
|
+
const lowerDate = dateString.toLowerCase().trim();
|
|
71
|
+
// Handle "now" specifically
|
|
72
|
+
if (lowerDate === 'now') {
|
|
73
|
+
return getCurrentTimestamp();
|
|
74
|
+
}
|
|
75
|
+
// Handle "today" with different options
|
|
47
76
|
if (lowerDate === 'today') {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
77
|
+
return getEndOfDay();
|
|
78
|
+
}
|
|
79
|
+
if (lowerDate === 'today start' || lowerDate === 'start of today') {
|
|
80
|
+
return getStartOfDay();
|
|
81
|
+
}
|
|
82
|
+
if (lowerDate === 'today end' || lowerDate === 'end of today') {
|
|
83
|
+
return getEndOfDay();
|
|
84
|
+
}
|
|
85
|
+
// Handle "yesterday" and "tomorrow"
|
|
86
|
+
if (lowerDate === 'yesterday') {
|
|
87
|
+
const yesterday = new Date();
|
|
88
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
89
|
+
yesterday.setHours(23, 59, 59, 999);
|
|
90
|
+
return yesterday.getTime();
|
|
91
|
+
}
|
|
92
|
+
if (lowerDate === 'tomorrow') {
|
|
93
|
+
const tomorrow = new Date();
|
|
94
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
95
|
+
tomorrow.setHours(23, 59, 59, 999);
|
|
96
|
+
return tomorrow.getTime();
|
|
51
97
|
}
|
|
52
98
|
// Handle relative dates with specific times
|
|
53
|
-
const relativeTimeRegex = /(?:(\d+)\s*(minutes?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
|
|
99
|
+
const relativeTimeRegex = /(?:(\d+)\s*(minutes?|hours?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month|year))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
|
|
54
100
|
const match = lowerDate.match(relativeTimeRegex);
|
|
55
101
|
if (match) {
|
|
56
102
|
const date = new Date();
|
|
@@ -61,6 +107,9 @@ export function parseDueDate(dateString) {
|
|
|
61
107
|
if (unit.startsWith('minute')) {
|
|
62
108
|
date.setMinutes(date.getMinutes() + value);
|
|
63
109
|
}
|
|
110
|
+
else if (unit.startsWith('hour')) {
|
|
111
|
+
date.setHours(date.getHours() + value);
|
|
112
|
+
}
|
|
64
113
|
else if (unit.startsWith('day')) {
|
|
65
114
|
date.setDate(date.getDate() + value);
|
|
66
115
|
}
|
|
@@ -80,6 +129,9 @@ export function parseDueDate(dateString) {
|
|
|
80
129
|
else if (lowerDate.includes('next month')) {
|
|
81
130
|
date.setMonth(date.getMonth() + 1);
|
|
82
131
|
}
|
|
132
|
+
else if (lowerDate.includes('next year')) {
|
|
133
|
+
date.setFullYear(date.getFullYear() + 1);
|
|
134
|
+
}
|
|
83
135
|
// Set the time if specified
|
|
84
136
|
if (hours) {
|
|
85
137
|
let parsedHours = parseInt(hours);
|
|
@@ -97,31 +149,44 @@ export function parseDueDate(dateString) {
|
|
|
97
149
|
}
|
|
98
150
|
return date.getTime();
|
|
99
151
|
}
|
|
100
|
-
// Handle
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (daysRegex.test(lowerDate)) {
|
|
115
|
-
const days = parseInt(lowerDate.match(daysRegex)[1]);
|
|
116
|
-
return getRelativeTimestamp(0, 0, days);
|
|
117
|
-
}
|
|
118
|
-
if (weeksRegex.test(lowerDate)) {
|
|
119
|
-
const weeks = parseInt(lowerDate.match(weeksRegex)[1]);
|
|
120
|
-
return getRelativeTimestamp(0, 0, 0, weeks);
|
|
152
|
+
// Handle various relative formats
|
|
153
|
+
const relativeFormats = [
|
|
154
|
+
{ regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) },
|
|
155
|
+
{ regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) },
|
|
156
|
+
{ regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) },
|
|
157
|
+
{ regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) },
|
|
158
|
+
{ regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) }
|
|
159
|
+
];
|
|
160
|
+
for (const format of relativeFormats) {
|
|
161
|
+
if (format.regex.test(lowerDate)) {
|
|
162
|
+
const value = parseInt(lowerDate.match(format.regex)[1]);
|
|
163
|
+
return format.handler(value);
|
|
164
|
+
}
|
|
121
165
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
166
|
+
// Handle specific date formats
|
|
167
|
+
// Format: MM/DD/YYYY
|
|
168
|
+
const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s+(am|pm))?)?$/i;
|
|
169
|
+
const usDateMatch = lowerDate.match(usDateRegex);
|
|
170
|
+
if (usDateMatch) {
|
|
171
|
+
const [_, month, day, year, hours, minutes, meridian] = usDateMatch;
|
|
172
|
+
const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed
|
|
173
|
+
parseInt(day));
|
|
174
|
+
// Add time if specified
|
|
175
|
+
if (hours) {
|
|
176
|
+
let parsedHours = parseInt(hours);
|
|
177
|
+
const parsedMinutes = minutes ? parseInt(minutes) : 0;
|
|
178
|
+
// Convert to 24-hour format if meridian is specified
|
|
179
|
+
if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
|
|
180
|
+
parsedHours += 12;
|
|
181
|
+
if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
|
|
182
|
+
parsedHours = 0;
|
|
183
|
+
date.setHours(parsedHours, parsedMinutes, 0, 0);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Default to end of day if no time specified
|
|
187
|
+
date.setHours(23, 59, 59, 999);
|
|
188
|
+
}
|
|
189
|
+
return date.getTime();
|
|
125
190
|
}
|
|
126
191
|
// Try to parse as a date string
|
|
127
192
|
const date = new Date(dateString);
|
|
@@ -164,3 +229,57 @@ export function formatDueDate(timestamp) {
|
|
|
164
229
|
return undefined;
|
|
165
230
|
}
|
|
166
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Checks if a timestamp is for today
|
|
234
|
+
*
|
|
235
|
+
* @param timestamp Unix timestamp in milliseconds
|
|
236
|
+
* @returns Boolean indicating if the timestamp is for today
|
|
237
|
+
*/
|
|
238
|
+
export function isToday(timestamp) {
|
|
239
|
+
const date = new Date(timestamp);
|
|
240
|
+
const today = new Date();
|
|
241
|
+
return date.getDate() === today.getDate() &&
|
|
242
|
+
date.getMonth() === today.getMonth() &&
|
|
243
|
+
date.getFullYear() === today.getFullYear();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get timestamp range for today (start to end)
|
|
247
|
+
*
|
|
248
|
+
* @returns Object with start and end timestamps for today
|
|
249
|
+
*/
|
|
250
|
+
export function getTodayRange() {
|
|
251
|
+
return {
|
|
252
|
+
start: getStartOfDay(),
|
|
253
|
+
end: getEndOfDay()
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Format a date for display in errors and messages
|
|
258
|
+
* @param timestamp The timestamp to format
|
|
259
|
+
* @returns A human-readable relative time (e.g., "2 hours ago")
|
|
260
|
+
*/
|
|
261
|
+
export function formatRelativeTime(timestamp) {
|
|
262
|
+
if (!timestamp)
|
|
263
|
+
return 'Unknown';
|
|
264
|
+
const timestampNum = typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp;
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
const diffMs = now - timestampNum;
|
|
267
|
+
// Convert to appropriate time unit
|
|
268
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
269
|
+
if (diffSec < 60)
|
|
270
|
+
return `${diffSec} seconds ago`;
|
|
271
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
272
|
+
if (diffMin < 60)
|
|
273
|
+
return `${diffMin} minutes ago`;
|
|
274
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
275
|
+
if (diffHour < 24)
|
|
276
|
+
return `${diffHour} hours ago`;
|
|
277
|
+
const diffDays = Math.floor(diffHour / 24);
|
|
278
|
+
if (diffDays < 30)
|
|
279
|
+
return `${diffDays} days ago`;
|
|
280
|
+
const diffMonths = Math.floor(diffDays / 30);
|
|
281
|
+
if (diffMonths < 12)
|
|
282
|
+
return `${diffMonths} months ago`;
|
|
283
|
+
const diffYears = Math.floor(diffMonths / 12);
|
|
284
|
+
return `${diffYears} years ago`;
|
|
285
|
+
}
|
|
@@ -9,13 +9,40 @@
|
|
|
9
9
|
import { clickUpServices } from '../services/shared.js';
|
|
10
10
|
import { findListIDByName } from '../tools/list.js';
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Check if a task name matches search criteria
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
* Performs flexible case-insensitive and emoji-aware text matching
|
|
15
|
+
* Used by multiple components for consistent name matching behavior
|
|
16
|
+
*/
|
|
17
|
+
export function isNameMatch(taskName, searchTerm) {
|
|
18
|
+
const normalizedTask = taskName.toLowerCase().trim();
|
|
19
|
+
const normalizedSearch = searchTerm.toLowerCase().trim();
|
|
20
|
+
// Handle empty strings - don't match empty task names
|
|
21
|
+
if (normalizedTask === '')
|
|
22
|
+
return false;
|
|
23
|
+
if (normalizedSearch === '')
|
|
24
|
+
return false;
|
|
25
|
+
// Exact match check
|
|
26
|
+
if (normalizedTask === normalizedSearch)
|
|
27
|
+
return true;
|
|
28
|
+
// Substring match check
|
|
29
|
+
if (normalizedTask.includes(normalizedSearch) || normalizedSearch.includes(normalizedTask))
|
|
30
|
+
return true;
|
|
31
|
+
// Handle emoji characters in names
|
|
32
|
+
if (/[\p{Emoji}]/u.test(normalizedSearch) || /[\p{Emoji}]/u.test(normalizedTask)) {
|
|
33
|
+
const taskWithoutEmoji = normalizedTask.replace(/[\p{Emoji}]/gu, '').trim();
|
|
34
|
+
const searchWithoutEmoji = normalizedSearch.replace(/[\p{Emoji}]/gu, '').trim();
|
|
35
|
+
// Don't match if either becomes empty after emoji removal
|
|
36
|
+
if (taskWithoutEmoji === '' || searchWithoutEmoji === '')
|
|
37
|
+
return false;
|
|
38
|
+
return taskWithoutEmoji === searchWithoutEmoji ||
|
|
39
|
+
taskWithoutEmoji.includes(searchWithoutEmoji) ||
|
|
40
|
+
searchWithoutEmoji.includes(taskWithoutEmoji);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a list ID from either a direct ID or list name
|
|
19
46
|
*/
|
|
20
47
|
export async function resolveListId(listId, listName, workspaceService = clickUpServices.workspace) {
|
|
21
48
|
// If list ID is directly provided, use it
|
|
@@ -33,37 +60,3 @@ export async function resolveListId(listId, listName, workspaceService = clickUp
|
|
|
33
60
|
// If neither is provided, throw an error
|
|
34
61
|
throw new Error("Either listId or listName must be provided");
|
|
35
62
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Resolve a task ID from either a direct ID or task name + list info
|
|
38
|
-
*
|
|
39
|
-
* @param taskId Optional direct task ID
|
|
40
|
-
* @param taskName Optional task name to resolve
|
|
41
|
-
* @param listId Optional list ID for task lookup
|
|
42
|
-
* @param listName Optional list name for task lookup
|
|
43
|
-
* @param taskService Task service to use for lookup
|
|
44
|
-
* @returns Resolved task ID
|
|
45
|
-
* @throws Error if parameters are insufficient or task can't be found
|
|
46
|
-
*/
|
|
47
|
-
export async function resolveTaskId(taskId, taskName, listId, listName, taskService = clickUpServices.task) {
|
|
48
|
-
// If task ID is directly provided, use it
|
|
49
|
-
if (taskId) {
|
|
50
|
-
return taskId;
|
|
51
|
-
}
|
|
52
|
-
// If task name is provided, we need list info to find it
|
|
53
|
-
if (taskName) {
|
|
54
|
-
// We need either listId or listName to find a task by name
|
|
55
|
-
if (!listId && !listName) {
|
|
56
|
-
throw new Error(`List name or ID is required when using task name for task "${taskName}"`);
|
|
57
|
-
}
|
|
58
|
-
// Get list ID
|
|
59
|
-
const targetListId = await resolveListId(listId, listName);
|
|
60
|
-
// Find the task in the list
|
|
61
|
-
const foundTask = await taskService.findTaskByName(targetListId, taskName);
|
|
62
|
-
if (!foundTask) {
|
|
63
|
-
throw new Error(`Task "${taskName}" not found in list`);
|
|
64
|
-
}
|
|
65
|
-
return foundTask.id;
|
|
66
|
-
}
|
|
67
|
-
// If neither is provided, throw an error
|
|
68
|
-
throw new Error("Either taskId or taskName must be provided");
|
|
69
|
-
}
|
|
@@ -59,7 +59,7 @@ export class SponsorService {
|
|
|
59
59
|
if (this.isEnabled && includeSponsorMessage) {
|
|
60
60
|
content.push({
|
|
61
61
|
type: "text",
|
|
62
|
-
text:
|
|
62
|
+
text: `♥ Support this project by sponsoring the developer at ${this.sponsorUrl}`
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
return { content };
|
package/package.json
CHANGED