@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.
- package/README.md +31 -1
- package/build/config.js +30 -0
- package/build/middleware/security.js +231 -0
- package/build/server.js +1 -1
- package/build/services/clickup/task/task-core.js +69 -5
- package/build/sse_server.js +172 -8
- package/build/tools/member.js +2 -4
- package/build/tools/task/bulk-operations.js +7 -7
- package/build/tools/task/handlers.js +66 -15
- package/build/tools/task/single-operations.js +11 -11
- package/build/tools/task/time-tracking.js +61 -170
- package/build/tools/task/utilities.js +56 -22
- package/build/utils/date-utils.js +341 -141
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
|
274
|
+
if (numericValue >= 946684800000) { // Jan 1, 2000 (inclusive)
|
|
77
275
|
return numericValue;
|
|
78
276
|
}
|
|
79
277
|
}
|
|
80
|
-
//
|
|
81
|
-
const
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
149
|
-
const
|
|
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
|
|
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}))
|
|
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
|
-
|
|
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
|
-
//
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
//
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
//
|
|
278
|
-
return
|
|
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