@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.
@@ -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
- const now = new Date();
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
- const today = new Date();
49
- today.setHours(23, 59, 59, 999);
50
- return today.getTime();
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 hours from now
101
- const minutesRegex = /(\d+)\s*minutes?\s*from\s*now/i;
102
- const hoursRegex = /(\d+)\s*hours?\s*from\s*now/i;
103
- const daysRegex = /(\d+)\s*days?\s*from\s*now/i;
104
- const weeksRegex = /(\d+)\s*weeks?\s*from\s*now/i;
105
- const monthsRegex = /(\d+)\s*months?\s*from\s*now/i;
106
- if (minutesRegex.test(lowerDate)) {
107
- const minutes = parseInt(lowerDate.match(minutesRegex)[1]);
108
- return getRelativeTimestamp(minutes);
109
- }
110
- if (hoursRegex.test(lowerDate)) {
111
- const hours = parseInt(lowerDate.match(hoursRegex)[1]);
112
- return getRelativeTimestamp(0, hours);
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
- if (monthsRegex.test(lowerDate)) {
123
- const months = parseInt(lowerDate.match(monthsRegex)[1]);
124
- return getRelativeTimestamp(0, 0, 0, 0, months);
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
- * Resolve a list ID from either a direct ID or list name
12
+ * Check if a task name matches search criteria
13
13
  *
14
- * @param listId Optional direct list ID
15
- * @param listName Optional list name to resolve
16
- * @param workspaceService Workspace service to use for lookup
17
- * @returns Resolved list ID
18
- * @throws Error if neither listId nor listName is provided, or if list name can't be resolved
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: `⎯ Support this project by sponsoring the developer at ${this.sponsorUrl}`
62
+ text: `♥ Support this project by sponsoring the developer at ${this.sponsorUrl}`
63
63
  });
64
64
  }
65
65
  return { content };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "build/index.js",