@taazkareem/clickup-mcp-server 0.8.3 → 0.8.5

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.
@@ -58,9 +58,207 @@ function getEndOfDay() {
58
58
  function getCurrentTimestamp() {
59
59
  return new Date().getTime();
60
60
  }
61
+ /**
62
+ * Smart preprocessing layer for date strings
63
+ * Normalizes input, handles common variations, and prepares for regex patterns
64
+ *
65
+ * @param input Raw date string input
66
+ * @returns Preprocessed and normalized date string
67
+ */
68
+ function preprocessDateString(input) {
69
+ if (!input)
70
+ return input;
71
+ let processed = input.toLowerCase().trim();
72
+ // Normalize common variations and typos
73
+ const normalizations = [
74
+ // Handle "a" and "an" as "1" FIRST (before other patterns)
75
+ [/\ba\s+(day|week|month|year)\s+ago\b/g, '1 $1 ago'],
76
+ [/\ba\s+(day|week|month|year)\s+from\s+now\b/g, '1 $1 from now'],
77
+ [/\ba\s+(day|week|month|year)\s+later\b/g, '1 $1 later'],
78
+ [/\ban\s+(hour|day|week|month|year)\s+ago\b/g, '1 $1 ago'],
79
+ [/\ban\s+(hour|day|week|month|year)\s+from\s+now\b/g, '1 $1 from now'],
80
+ [/\ban\s+(hour|day|week|month|year)\s+later\b/g, '1 $1 later'],
81
+ [/\bin\s+a\s+(day|week|month|year)\b/g, 'in 1 $1'],
82
+ [/\bin\s+an\s+(hour|day|week|month|year)\b/g, 'in 1 $1'],
83
+ // Handle common typos and variations
84
+ [/\btommorow\b/g, 'tomorrow'],
85
+ [/\byesterady\b/g, 'yesterday'],
86
+ [/\btomorrow\s*mornin[g]?\b/g, 'tomorrow 9am'],
87
+ [/\byesterday\s*mornin[g]?\b/g, 'yesterday 9am'],
88
+ [/\btomorrow\s*evenin[g]?\b/g, 'tomorrow 6pm'],
89
+ [/\byesterday\s*evenin[g]?\b/g, 'yesterday 6pm'],
90
+ [/\btomorrow\s*night\b/g, 'tomorrow 9pm'],
91
+ [/\byesterday\s*night\b/g, 'yesterday 9pm'],
92
+ // Normalize time expressions
93
+ [/\b(\d{1,2})\s*:\s*(\d{2})\s*(a\.?m\.?|p\.?m\.?)\b/g, '$1:$2$3'],
94
+ [/\b(\d{1,2})\s*(a\.?m\.?|p\.?m\.?)\b/g, '$1$2'],
95
+ [/\ba\.?m\.?\b/g, 'am'],
96
+ [/\bp\.?m\.?\b/g, 'pm'],
97
+ // Normalize "at" usage and additional time connectors
98
+ [/\s+at\s+/g, ' '],
99
+ [/\s+@\s+/g, ' '],
100
+ [/\s+around\s+/g, ' '],
101
+ [/\s+by\s+/g, ' '],
102
+ [/\s+on\s+/g, ' '],
103
+ // Handle "day after tomorrow" and "day before yesterday" + additional variations
104
+ [/\bday\s+after\s+tomorrow\b/g, '+2 days'],
105
+ [/\bday\s+before\s+yesterday\b/g, '-2 days'],
106
+ [/\bovermorrow\b/g, '+2 days'], // Formal term for "day after tomorrow"
107
+ [/\bereyesterday\b/g, '-2 days'], // Formal term for "day before yesterday"
108
+ // Handle "next/last" with time units
109
+ [/\bnext\s+(\d+)\s+days?\b/g, '+$1 days'],
110
+ [/\bnext\s+(\d+)\s+weeks?\b/g, '+$1 weeks'],
111
+ [/\blast\s+(\d+)\s+days?\b/g, '-$1 days'],
112
+ [/\blast\s+(\d+)\s+weeks?\b/g, '-$1 weeks'],
113
+ // Normalize relative expressions - comprehensive natural language support
114
+ [/\bin\s+(\d+)\s+days?\b/g, '+$1 days'],
115
+ [/\b(\d+)\s+days?\s+ago\b/g, '-$1 days'],
116
+ [/\bin\s+(\d+)\s+weeks?\b/g, '+$1 weeks'],
117
+ [/\b(\d+)\s+weeks?\s+ago\b/g, '-$1 weeks'],
118
+ [/\b(\d+)\s+weeks?\s+from\s+now\b/g, '+$1 weeks'],
119
+ [/\b(\d+)\s+days?\s+from\s+now\b/g, '+$1 days'],
120
+ // Additional natural language variations
121
+ [/\b(\d+)\s+days?\s+later\b/g, '+$1 days'],
122
+ [/\b(\d+)\s+weeks?\s+later\b/g, '+$1 weeks'],
123
+ [/\bafter\s+(\d+)\s+days?\b/g, '+$1 days'],
124
+ [/\bafter\s+(\d+)\s+weeks?\b/g, '+$1 weeks'],
125
+ [/\b(\d+)\s+days?\s+ahead\b/g, '+$1 days'],
126
+ [/\b(\d+)\s+weeks?\s+ahead\b/g, '+$1 weeks'],
127
+ [/\b(\d+)\s+days?\s+forward\b/g, '+$1 days'],
128
+ [/\b(\d+)\s+weeks?\s+forward\b/g, '+$1 weeks'],
129
+ // Past variations
130
+ [/\b(\d+)\s+days?\s+back\b/g, '-$1 days'],
131
+ [/\b(\d+)\s+weeks?\s+back\b/g, '-$1 weeks'],
132
+ [/\b(\d+)\s+days?\s+before\b/g, '-$1 days'],
133
+ [/\b(\d+)\s+weeks?\s+before\b/g, '-$1 weeks'],
134
+ [/\b(\d+)\s+days?\s+earlier\b/g, '-$1 days'],
135
+ [/\b(\d+)\s+weeks?\s+earlier\b/g, '-$1 weeks'],
136
+ // Extended time units - months and years
137
+ [/\bin\s+(\d+)\s+months?\b/g, '+$1 months'],
138
+ [/\b(\d+)\s+months?\s+from\s+now\b/g, '+$1 months'],
139
+ [/\b(\d+)\s+months?\s+later\b/g, '+$1 months'],
140
+ [/\bafter\s+(\d+)\s+months?\b/g, '+$1 months'],
141
+ [/\b(\d+)\s+months?\s+ago\b/g, '-$1 months'],
142
+ [/\b(\d+)\s+months?\s+back\b/g, '-$1 months'],
143
+ [/\b(\d+)\s+months?\s+earlier\b/g, '-$1 months'],
144
+ [/\bin\s+(\d+)\s+years?\b/g, '+$1 years'],
145
+ [/\b(\d+)\s+years?\s+from\s+now\b/g, '+$1 years'],
146
+ [/\b(\d+)\s+years?\s+later\b/g, '+$1 years'],
147
+ [/\bafter\s+(\d+)\s+years?\b/g, '+$1 years'],
148
+ [/\b(\d+)\s+years?\s+ago\b/g, '-$1 years'],
149
+ [/\b(\d+)\s+years?\s+back\b/g, '-$1 years'],
150
+ [/\b(\d+)\s+years?\s+earlier\b/g, '-$1 years'],
151
+ // Handle "this" and "next" prefixes more consistently
152
+ [/\bthis\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/g, '$1'],
153
+ [/\bnext\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/g, 'next $1'],
154
+ // Normalize timezone abbreviations (remove them for now)
155
+ [/\s+(est|edt|pst|pdt|cst|cdt|mst|mdt)\b/g, ''],
156
+ // Clean up extra whitespace
157
+ [/\s+/g, ' '],
158
+ ];
159
+ // Apply all normalizations
160
+ for (const [pattern, replacement] of normalizations) {
161
+ processed = processed.replace(pattern, replacement);
162
+ }
163
+ return processed.trim();
164
+ }
165
+ /**
166
+ * Helper function to parse time components and convert to 24-hour format
167
+ * Reduces code duplication across different date parsing patterns
168
+ */
169
+ function parseTimeComponents(hours, minutes, meridian) {
170
+ let parsedHours = parseInt(hours);
171
+ const parsedMinutes = minutes ? parseInt(minutes) : 0;
172
+ // Convert to 24-hour format if meridian is specified
173
+ if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
174
+ parsedHours += 12;
175
+ if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
176
+ parsedHours = 0;
177
+ return { hours: parsedHours, minutes: parsedMinutes };
178
+ }
179
+ /**
180
+ * Helper function to set time on a date object with default fallback
181
+ */
182
+ function setTimeOnDate(date, hours, minutes, meridian) {
183
+ if (hours) {
184
+ const { hours: parsedHours, minutes: parsedMinutes } = parseTimeComponents(hours, minutes, meridian);
185
+ date.setHours(parsedHours, parsedMinutes, 0, 0);
186
+ }
187
+ else {
188
+ // Default to end of day if no time specified
189
+ date.setHours(23, 59, 59, 999);
190
+ }
191
+ }
192
+ /**
193
+ * Consolidated date patterns with enhanced flexibility
194
+ */
195
+ function getDatePatterns() {
196
+ return [
197
+ // Relative day expressions with optional time
198
+ {
199
+ name: 'relative_days',
200
+ pattern: /^([+-]?\d+)\s+days?(?:\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/,
201
+ handler: (match) => {
202
+ const days = parseInt(match[1]);
203
+ const date = new Date();
204
+ date.setDate(date.getDate() + days);
205
+ setTimeOnDate(date, match[2], match[3], match[4]);
206
+ return date;
207
+ }
208
+ },
209
+ // Relative week expressions with optional time
210
+ {
211
+ name: 'relative_weeks',
212
+ pattern: /^([+-]?\d+)\s+weeks?(?:\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/,
213
+ handler: (match) => {
214
+ const weeks = parseInt(match[1]);
215
+ const date = new Date();
216
+ date.setDate(date.getDate() + (weeks * 7));
217
+ setTimeOnDate(date, match[2], match[3], match[4]);
218
+ return date;
219
+ }
220
+ },
221
+ // Relative month expressions with optional time
222
+ {
223
+ name: 'relative_months',
224
+ pattern: /^([+-]?\d+)\s+months?(?:\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/,
225
+ handler: (match) => {
226
+ const months = parseInt(match[1]);
227
+ const date = new Date();
228
+ date.setMonth(date.getMonth() + months);
229
+ setTimeOnDate(date, match[2], match[3], match[4]);
230
+ return date;
231
+ }
232
+ },
233
+ // Relative year expressions with optional time
234
+ {
235
+ name: 'relative_years',
236
+ pattern: /^([+-]?\d+)\s+years?(?:\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/,
237
+ handler: (match) => {
238
+ const years = parseInt(match[1]);
239
+ const date = new Date();
240
+ date.setFullYear(date.getFullYear() + years);
241
+ setTimeOnDate(date, match[2], match[3], match[4]);
242
+ return date;
243
+ }
244
+ },
245
+ // Yesterday/Tomorrow with enhanced time support
246
+ {
247
+ name: 'yesterday_tomorrow',
248
+ pattern: /^(yesterday|tomorrow)(?:\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/,
249
+ handler: (match) => {
250
+ const isYesterday = match[1] === 'yesterday';
251
+ const date = new Date();
252
+ date.setDate(date.getDate() + (isYesterday ? -1 : 1));
253
+ setTimeOnDate(date, match[2], match[3], match[4]);
254
+ return date;
255
+ }
256
+ }
257
+ ];
258
+ }
61
259
  /**
62
260
  * Parse a due date string into a timestamp
63
- * Supports ISO 8601 format or natural language like "tomorrow"
261
+ * Enhanced with smart preprocessing and consolidated patterns
64
262
  *
65
263
  * @param dateString Date string to parse
66
264
  * @returns Timestamp in milliseconds or undefined if parsing fails
@@ -73,12 +271,27 @@ export function parseDueDate(dateString) {
73
271
  const numericValue = Number(dateString);
74
272
  if (!isNaN(numericValue) && numericValue > 0) {
75
273
  // If it's a reasonable timestamp (after year 2000), use it
76
- if (numericValue > 946684800000) { // Jan 1, 2000
274
+ if (numericValue >= 946684800000) { // Jan 1, 2000 (inclusive)
77
275
  return numericValue;
78
276
  }
79
277
  }
80
- // Handle natural language dates
81
- const lowerDate = dateString.toLowerCase().trim();
278
+ // Apply smart preprocessing
279
+ const preprocessed = preprocessDateString(dateString);
280
+ logger.debug(`Preprocessed date: "${dateString}" -> "${preprocessed}"`);
281
+ // Handle natural language dates with preprocessed input
282
+ const lowerDate = preprocessed;
283
+ // Try enhanced pattern matching first
284
+ const patterns = getDatePatterns();
285
+ for (const pattern of patterns) {
286
+ const match = lowerDate.match(pattern.pattern);
287
+ if (match) {
288
+ const result = pattern.handler(match);
289
+ if (result && !isNaN(result.getTime())) {
290
+ logger.debug(`Matched pattern "${pattern.name}" for: ${lowerDate}`);
291
+ return result.getTime();
292
+ }
293
+ }
294
+ }
82
295
  // Handle "now" specifically
83
296
  if (lowerDate === 'now') {
84
297
  return getCurrentTimestamp();
@@ -93,19 +306,7 @@ export function parseDueDate(dateString) {
93
306
  if (lowerDate === 'today end' || lowerDate === 'end of today') {
94
307
  return getEndOfDay();
95
308
  }
96
- // Handle "yesterday" and "tomorrow"
97
- if (lowerDate === 'yesterday') {
98
- const yesterday = new Date();
99
- yesterday.setDate(yesterday.getDate() - 1);
100
- yesterday.setHours(23, 59, 59, 999);
101
- return yesterday.getTime();
102
- }
103
- if (lowerDate === 'tomorrow') {
104
- const tomorrow = new Date();
105
- tomorrow.setDate(tomorrow.getDate() + 1);
106
- tomorrow.setHours(23, 59, 59, 999);
107
- return tomorrow.getTime();
108
- }
309
+ // Note: Yesterday/tomorrow patterns are now handled by enhanced patterns above
109
310
  // Handle day names (Monday, Tuesday, etc.) - find next occurrence
110
311
  const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
111
312
  const dayMatch = lowerDate.match(/\b(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/);
@@ -127,161 +328,160 @@ export function parseDueDate(dateString) {
127
328
  targetDate.setDate(today.getDate() + daysUntilTarget);
128
329
  // Extract time if specified (e.g., "Friday at 3pm", "Saturday 2:30pm")
129
330
  const timeMatch = lowerDate.match(/(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
130
- if (timeMatch) {
131
- let hours = parseInt(timeMatch[1]);
132
- const minutes = timeMatch[2] ? parseInt(timeMatch[2]) : 0;
133
- const meridian = timeMatch[3]?.toLowerCase();
134
- // Convert to 24-hour format
135
- if (meridian === 'pm' && hours < 12)
136
- hours += 12;
137
- if (meridian === 'am' && hours === 12)
138
- hours = 0;
139
- targetDate.setHours(hours, minutes, 0, 0);
140
- }
141
- else {
142
- // Default to end of day if no time specified
143
- targetDate.setHours(23, 59, 59, 999);
144
- }
331
+ setTimeOnDate(targetDate, timeMatch?.[1], timeMatch?.[2], timeMatch?.[3]);
145
332
  return targetDate.getTime();
146
333
  }
147
- // Handle relative dates with specific times
148
- 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;
149
- const match = lowerDate.match(relativeTimeRegex);
150
- if (match) {
151
- const date = new Date();
152
- const [_, amount, unit, hours, minutes, meridian] = match;
153
- // Calculate the future date
154
- if (amount && unit) {
155
- const value = parseInt(amount);
156
- if (unit.startsWith('minute')) {
157
- date.setMinutes(date.getMinutes() + value);
158
- }
159
- else if (unit.startsWith('hour')) {
160
- date.setHours(date.getHours() + value);
161
- }
162
- else if (unit.startsWith('day')) {
163
- date.setDate(date.getDate() + value);
164
- }
165
- else if (unit.startsWith('week')) {
166
- date.setDate(date.getDate() + (value * 7));
167
- }
168
- else if (unit.startsWith('month')) {
169
- date.setMonth(date.getMonth() + value);
170
- }
171
- }
172
- else if (lowerDate.startsWith('tomorrow')) {
173
- date.setDate(date.getDate() + 1);
174
- }
175
- else if (lowerDate.includes('next week')) {
176
- date.setDate(date.getDate() + 7);
177
- }
178
- else if (lowerDate.includes('next month')) {
179
- date.setMonth(date.getMonth() + 1);
180
- }
181
- else if (lowerDate.includes('next year')) {
182
- date.setFullYear(date.getFullYear() + 1);
183
- }
184
- // Set the time if specified
185
- if (hours) {
186
- let parsedHours = parseInt(hours);
187
- const parsedMinutes = minutes ? parseInt(minutes) : 0;
188
- // Convert to 24-hour format if meridian is specified
189
- if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
190
- parsedHours += 12;
191
- if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
192
- parsedHours = 0;
193
- date.setHours(parsedHours, parsedMinutes, 0, 0);
194
- }
195
- else {
196
- // Default to end of day if no time specified
197
- date.setHours(23, 59, 59, 999);
198
- }
199
- return date.getTime();
200
- }
201
- // Handle various relative formats
202
- const relativeFormats = [
334
+ // Note: Relative date patterns are now handled by enhanced patterns above
335
+ // Legacy support for "X from now" patterns
336
+ const legacyRelativeFormats = [
203
337
  { regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) },
204
338
  { regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) },
205
339
  { regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) },
206
340
  { regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) },
207
341
  { regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) }
208
342
  ];
209
- for (const format of relativeFormats) {
343
+ for (const format of legacyRelativeFormats) {
210
344
  if (format.regex.test(lowerDate)) {
211
345
  const value = parseInt(lowerDate.match(format.regex)[1]);
212
346
  return format.handler(value);
213
347
  }
214
348
  }
215
349
  // Handle specific date formats
216
- // Format: MM/DD/YYYY
217
- const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s+(am|pm))?)?$/i;
350
+ // Format: MM/DD/YYYY with enhanced time support (handles both "5pm" and "5 pm")
351
+ const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)?)?$/i;
218
352
  const usDateMatch = lowerDate.match(usDateRegex);
219
353
  if (usDateMatch) {
220
354
  const [_, month, day, year, hours, minutes, meridian] = usDateMatch;
221
355
  const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed
222
356
  parseInt(day));
223
357
  // Add time if specified
224
- if (hours) {
225
- let parsedHours = parseInt(hours);
226
- const parsedMinutes = minutes ? parseInt(minutes) : 0;
227
- // Convert to 24-hour format if meridian is specified
228
- if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
229
- parsedHours += 12;
230
- if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
231
- parsedHours = 0;
232
- date.setHours(parsedHours, parsedMinutes, 0, 0);
233
- }
234
- else {
235
- // Default to end of day if no time specified
236
- date.setHours(23, 59, 59, 999);
237
- }
358
+ setTimeOnDate(date, hours, minutes, meridian);
238
359
  return date.getTime();
239
360
  }
240
- // Enhanced fallback: Try JavaScript's native Date constructor with various formats
241
- // This handles many natural language formats like "Saturday at 3pm EST", "next Friday", etc.
242
- const nativeDate = new Date(dateString);
243
- if (!isNaN(nativeDate.getTime())) {
244
- // Check if the parsed date is reasonable (not too far in the past or future)
245
- const now = Date.now();
246
- const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000);
247
- const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000);
248
- if (nativeDate.getTime() > oneYearAgo && nativeDate.getTime() < tenYearsFromNow) {
249
- return nativeDate.getTime();
250
- }
361
+ // Handle MM/DD format without year (assume current year)
362
+ const usDateNoYearRegex = /^(\d{1,2})\/(\d{1,2})(?:\s+(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)?)?$/i;
363
+ const usDateNoYearMatch = lowerDate.match(usDateNoYearRegex);
364
+ if (usDateNoYearMatch) {
365
+ const [_, month, day, hours, minutes, meridian] = usDateNoYearMatch;
366
+ const currentYear = new Date().getFullYear();
367
+ const date = new Date(currentYear, parseInt(month) - 1, // JS months are 0-indexed
368
+ parseInt(day));
369
+ // Add time if specified
370
+ setTimeOnDate(date, hours, minutes, meridian);
371
+ return date.getTime();
251
372
  }
252
- // Try some common variations and transformations
253
- const variations = [
254
- dateString.replace(/\s+at\s+/i, ' '), // "Saturday at 3pm" -> "Saturday 3pm"
255
- dateString.replace(/\s+EST|EDT|PST|PDT|CST|CDT|MST|MDT/i, ''), // Remove timezone
256
- dateString.replace(/next\s+/i, ''), // "next Friday" -> "Friday"
257
- dateString.replace(/this\s+/i, ''), // "this Friday" -> "Friday"
258
- ];
259
- for (const variation of variations) {
260
- const varDate = new Date(variation);
261
- if (!isNaN(varDate.getTime())) {
262
- const now = Date.now();
263
- const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000);
264
- const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000);
265
- if (varDate.getTime() > oneYearAgo && varDate.getTime() < tenYearsFromNow) {
266
- // If the parsed date is in the past, assume they meant next occurrence
267
- if (varDate.getTime() < now) {
268
- // Add 7 days if it's a day of the week
269
- if (dateString.match(/monday|tuesday|wednesday|thursday|friday|saturday|sunday/i)) {
270
- varDate.setDate(varDate.getDate() + 7);
271
- }
272
- }
273
- return varDate.getTime();
274
- }
373
+ // Handle text month formats (e.g., "march 10 2025 6:30pm")
374
+ const textMonthRegex = /^(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2})\s+(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)?)?$/i;
375
+ const textMonthMatch = lowerDate.match(textMonthRegex);
376
+ if (textMonthMatch) {
377
+ const [_, monthName, day, year, hours, minutes, meridian] = textMonthMatch;
378
+ const monthNames = ['january', 'february', 'march', 'april', 'may', 'june',
379
+ 'july', 'august', 'september', 'october', 'november', 'december'];
380
+ const monthIndex = monthNames.indexOf(monthName.toLowerCase());
381
+ if (monthIndex !== -1) {
382
+ const date = new Date(parseInt(year), monthIndex, parseInt(day));
383
+ // Add time if specified
384
+ setTimeOnDate(date, hours, minutes, meridian);
385
+ return date.getTime();
275
386
  }
276
387
  }
277
- // If all parsing fails, return undefined
278
- return undefined;
388
+ // Enhanced fallback chain with better validation and error handling
389
+ return enhancedFallbackParsing(dateString, preprocessed);
279
390
  }
280
391
  catch (error) {
281
392
  logger.warn(`Failed to parse due date: ${dateString}`, error);
282
393
  throw new Error(`Invalid date format: ${dateString}`);
283
394
  }
284
395
  }
396
+ /**
397
+ * Enhanced fallback parsing with multiple strategies
398
+ *
399
+ * @param originalInput Original date string
400
+ * @param preprocessedInput Preprocessed date string
401
+ * @returns Timestamp in milliseconds or undefined
402
+ */
403
+ function enhancedFallbackParsing(originalInput, preprocessedInput) {
404
+ const now = Date.now();
405
+ const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000);
406
+ const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000);
407
+ /**
408
+ * Validate if a date is reasonable
409
+ */
410
+ function isReasonableDate(date) {
411
+ const time = date.getTime();
412
+ return !isNaN(time) && time > oneYearAgo && time < tenYearsFromNow;
413
+ }
414
+ /**
415
+ * Try parsing with automatic future adjustment for past dates
416
+ */
417
+ function tryParseWithFutureAdjustment(input) {
418
+ const date = new Date(input);
419
+ if (!isReasonableDate(date))
420
+ return null;
421
+ // If the parsed date is in the past and looks like a day of the week, assume next occurrence
422
+ if (date.getTime() < now && input.match(/monday|tuesday|wednesday|thursday|friday|saturday|sunday/i)) {
423
+ date.setDate(date.getDate() + 7);
424
+ }
425
+ return isReasonableDate(date) ? date : null;
426
+ }
427
+ // Strategy 1: Try preprocessed input with native Date constructor
428
+ let result = tryParseWithFutureAdjustment(preprocessedInput);
429
+ if (result) {
430
+ logger.debug(`Fallback strategy 1 succeeded for: ${preprocessedInput}`);
431
+ return result.getTime();
432
+ }
433
+ // Strategy 2: Try original input with native Date constructor
434
+ result = tryParseWithFutureAdjustment(originalInput);
435
+ if (result) {
436
+ logger.debug(`Fallback strategy 2 succeeded for: ${originalInput}`);
437
+ return result.getTime();
438
+ }
439
+ // Strategy 3: Try common variations and transformations
440
+ const variations = [
441
+ // Remove common words that might confuse the parser
442
+ originalInput.replace(/\s+at\s+/gi, ' '),
443
+ originalInput.replace(/\s+(est|edt|pst|pdt|cst|cdt|mst|mdt)\b/gi, ''),
444
+ originalInput.replace(/\bnext\s+/gi, ''),
445
+ originalInput.replace(/\bthis\s+/gi, ''),
446
+ originalInput.replace(/\bon\s+/gi, ''),
447
+ // Try with different separators
448
+ originalInput.replace(/[-\/]/g, '/'),
449
+ originalInput.replace(/[-\/]/g, '-'),
450
+ // Try adding current year if it looks like a date without year
451
+ (() => {
452
+ const currentYear = new Date().getFullYear();
453
+ if (originalInput.match(/^\d{1,2}[\/\-]\d{1,2}$/)) {
454
+ return `${originalInput}/${currentYear}`;
455
+ }
456
+ return originalInput;
457
+ })(),
458
+ ];
459
+ for (const variation of variations) {
460
+ if (variation === originalInput)
461
+ continue; // Skip if no change
462
+ result = tryParseWithFutureAdjustment(variation);
463
+ if (result) {
464
+ logger.debug(`Fallback strategy 3 succeeded with variation: ${variation}`);
465
+ return result.getTime();
466
+ }
467
+ }
468
+ // Strategy 4: Last resort - try ISO format variations
469
+ const isoVariations = [
470
+ originalInput.replace(/(\d{4})-(\d{1,2})-(\d{1,2})/, '$1-$2-$3T23:59:59'),
471
+ originalInput.replace(/(\d{1,2})\/(\d{1,2})\/(\d{4})/, '$3-$1-$2'),
472
+ ];
473
+ for (const isoVariation of isoVariations) {
474
+ if (isoVariation === originalInput)
475
+ continue;
476
+ const date = new Date(isoVariation);
477
+ if (isReasonableDate(date)) {
478
+ logger.debug(`Fallback strategy 4 succeeded with ISO variation: ${isoVariation}`);
479
+ return date.getTime();
480
+ }
481
+ }
482
+ logger.debug(`All fallback strategies failed for: ${originalInput}`);
483
+ return undefined;
484
+ }
285
485
  /**
286
486
  * Format a due date timestamp into a human-readable string
287
487
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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",