@alanse/clickup-multi-mcp-server 1.0.0

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.
Files changed (56) hide show
  1. package/Dockerfile +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +470 -0
  4. package/build/config.js +237 -0
  5. package/build/index.js +87 -0
  6. package/build/logger.js +163 -0
  7. package/build/middleware/security.js +231 -0
  8. package/build/server.js +288 -0
  9. package/build/services/clickup/base.js +432 -0
  10. package/build/services/clickup/bulk.js +180 -0
  11. package/build/services/clickup/document.js +159 -0
  12. package/build/services/clickup/folder.js +136 -0
  13. package/build/services/clickup/index.js +76 -0
  14. package/build/services/clickup/list.js +191 -0
  15. package/build/services/clickup/tag.js +239 -0
  16. package/build/services/clickup/task/index.js +32 -0
  17. package/build/services/clickup/task/task-attachments.js +105 -0
  18. package/build/services/clickup/task/task-comments.js +114 -0
  19. package/build/services/clickup/task/task-core.js +604 -0
  20. package/build/services/clickup/task/task-custom-fields.js +107 -0
  21. package/build/services/clickup/task/task-search.js +986 -0
  22. package/build/services/clickup/task/task-service.js +104 -0
  23. package/build/services/clickup/task/task-tags.js +113 -0
  24. package/build/services/clickup/time.js +244 -0
  25. package/build/services/clickup/types.js +33 -0
  26. package/build/services/clickup/workspace.js +397 -0
  27. package/build/services/shared.js +61 -0
  28. package/build/sse_server.js +277 -0
  29. package/build/tools/documents.js +489 -0
  30. package/build/tools/folder.js +331 -0
  31. package/build/tools/index.js +16 -0
  32. package/build/tools/list.js +428 -0
  33. package/build/tools/member.js +106 -0
  34. package/build/tools/tag.js +833 -0
  35. package/build/tools/task/attachments.js +357 -0
  36. package/build/tools/task/attachments.types.js +9 -0
  37. package/build/tools/task/bulk-operations.js +338 -0
  38. package/build/tools/task/handlers.js +919 -0
  39. package/build/tools/task/index.js +30 -0
  40. package/build/tools/task/main.js +233 -0
  41. package/build/tools/task/single-operations.js +469 -0
  42. package/build/tools/task/time-tracking.js +575 -0
  43. package/build/tools/task/utilities.js +310 -0
  44. package/build/tools/task/workspace-operations.js +258 -0
  45. package/build/tools/tool-enhancer.js +37 -0
  46. package/build/tools/utils.js +12 -0
  47. package/build/tools/workspace-helper.js +44 -0
  48. package/build/tools/workspace.js +73 -0
  49. package/build/utils/color-processor.js +183 -0
  50. package/build/utils/concurrency-utils.js +248 -0
  51. package/build/utils/date-utils.js +542 -0
  52. package/build/utils/resolver-utils.js +135 -0
  53. package/build/utils/sponsor-service.js +93 -0
  54. package/build/utils/token-utils.js +49 -0
  55. package/package.json +77 -0
  56. package/smithery.yaml +23 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Date Utility Functions
6
+ *
7
+ * This module provides utilities for handling dates, timestamps, and due date parsing.
8
+ */
9
+ import { Logger } from '../logger.js';
10
+ // Create a logger instance for date utilities
11
+ const logger = new Logger('DateUtils');
12
+ /**
13
+ * Get a timestamp for a relative time
14
+ *
15
+ * @param minutes Minutes from now
16
+ * @param hours Hours from now
17
+ * @param days Days from now
18
+ * @param weeks Weeks from now
19
+ * @param months Months from now
20
+ * @returns Timestamp in milliseconds
21
+ */
22
+ export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0, months = 0) {
23
+ const now = new Date();
24
+ if (minutes)
25
+ now.setMinutes(now.getMinutes() + minutes);
26
+ if (hours)
27
+ now.setHours(now.getHours() + hours);
28
+ if (days)
29
+ now.setDate(now.getDate() + days);
30
+ if (weeks)
31
+ now.setDate(now.getDate() + (weeks * 7));
32
+ if (months)
33
+ now.setMonth(now.getMonth() + months);
34
+ return now.getTime();
35
+ }
36
+ /**
37
+ * Get the start of today (midnight) in Unix milliseconds
38
+ * @returns Timestamp in milliseconds for start of current day
39
+ */
40
+ function getStartOfDay() {
41
+ const now = new Date();
42
+ now.setHours(0, 0, 0, 0);
43
+ return now.getTime();
44
+ }
45
+ /**
46
+ * Get the end of today (23:59:59.999) in Unix milliseconds
47
+ * @returns Timestamp in milliseconds for end of current day
48
+ */
49
+ function getEndOfDay() {
50
+ const now = new Date();
51
+ now.setHours(23, 59, 59, 999);
52
+ return now.getTime();
53
+ }
54
+ /**
55
+ * Get the current time in Unix milliseconds
56
+ * @returns Current timestamp in milliseconds
57
+ */
58
+ function getCurrentTimestamp() {
59
+ return new Date().getTime();
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
+ }
259
+ /**
260
+ * Parse a due date string into a timestamp
261
+ * Enhanced with smart preprocessing and consolidated patterns
262
+ *
263
+ * @param dateString Date string to parse
264
+ * @returns Timestamp in milliseconds or undefined if parsing fails
265
+ */
266
+ export function parseDueDate(dateString) {
267
+ if (!dateString)
268
+ return undefined;
269
+ try {
270
+ // First, try to parse as a direct timestamp
271
+ const numericValue = Number(dateString);
272
+ if (!isNaN(numericValue) && numericValue > 0) {
273
+ // If it's a reasonable timestamp (after year 2000), use it
274
+ if (numericValue >= 946684800000) { // Jan 1, 2000 (inclusive)
275
+ return numericValue;
276
+ }
277
+ }
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
+ }
295
+ // Handle "now" specifically
296
+ if (lowerDate === 'now') {
297
+ return getCurrentTimestamp();
298
+ }
299
+ // Handle "today" with different options
300
+ if (lowerDate === 'today') {
301
+ return getEndOfDay();
302
+ }
303
+ if (lowerDate === 'today start' || lowerDate === 'start of today') {
304
+ return getStartOfDay();
305
+ }
306
+ if (lowerDate === 'today end' || lowerDate === 'end of today') {
307
+ return getEndOfDay();
308
+ }
309
+ // Note: Yesterday/tomorrow patterns are now handled by enhanced patterns above
310
+ // Handle day names (Monday, Tuesday, etc.) - find next occurrence
311
+ const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
312
+ const dayMatch = lowerDate.match(/\b(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/);
313
+ if (dayMatch) {
314
+ const targetDayName = dayMatch[1];
315
+ const targetDayIndex = dayNames.indexOf(targetDayName);
316
+ const today = new Date();
317
+ const currentDayIndex = today.getDay();
318
+ // Calculate days until target day
319
+ let daysUntilTarget = targetDayIndex - currentDayIndex;
320
+ if (daysUntilTarget <= 0) {
321
+ daysUntilTarget += 7; // Next week
322
+ }
323
+ // Handle "next" prefix explicitly
324
+ if (lowerDate.includes('next ')) {
325
+ daysUntilTarget += 7;
326
+ }
327
+ const targetDate = new Date(today);
328
+ targetDate.setDate(today.getDate() + daysUntilTarget);
329
+ // Extract time if specified (e.g., "Friday at 3pm", "Saturday 2:30pm")
330
+ const timeMatch = lowerDate.match(/(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
331
+ setTimeOnDate(targetDate, timeMatch?.[1], timeMatch?.[2], timeMatch?.[3]);
332
+ return targetDate.getTime();
333
+ }
334
+ // Note: Relative date patterns are now handled by enhanced patterns above
335
+ // Legacy support for "X from now" patterns
336
+ const legacyRelativeFormats = [
337
+ { regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) },
338
+ { regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) },
339
+ { regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) },
340
+ { regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) },
341
+ { regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) }
342
+ ];
343
+ for (const format of legacyRelativeFormats) {
344
+ if (format.regex.test(lowerDate)) {
345
+ const value = parseInt(lowerDate.match(format.regex)[1]);
346
+ return format.handler(value);
347
+ }
348
+ }
349
+ // Handle specific date formats
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;
352
+ const usDateMatch = lowerDate.match(usDateRegex);
353
+ if (usDateMatch) {
354
+ const [_, month, day, year, hours, minutes, meridian] = usDateMatch;
355
+ const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed
356
+ parseInt(day));
357
+ // Add time if specified
358
+ setTimeOnDate(date, hours, minutes, meridian);
359
+ return date.getTime();
360
+ }
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();
372
+ }
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();
386
+ }
387
+ }
388
+ // Enhanced fallback chain with better validation and error handling
389
+ return enhancedFallbackParsing(dateString, preprocessed);
390
+ }
391
+ catch (error) {
392
+ logger.warn(`Failed to parse due date: ${dateString}`, error);
393
+ throw new Error(`Invalid date format: ${dateString}`);
394
+ }
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
+ }
485
+ /**
486
+ * Format a due date timestamp into a human-readable string
487
+ *
488
+ * @param timestamp Unix timestamp in milliseconds
489
+ * @returns Formatted date string or undefined if timestamp is invalid
490
+ */
491
+ export function formatDueDate(timestamp) {
492
+ if (!timestamp)
493
+ return undefined;
494
+ try {
495
+ const date = new Date(timestamp);
496
+ if (isNaN(date.getTime()))
497
+ return undefined;
498
+ // Format: "March 10, 2025 at 10:56 PM"
499
+ return date.toLocaleString('en-US', {
500
+ year: 'numeric',
501
+ month: 'long',
502
+ day: 'numeric',
503
+ hour: 'numeric',
504
+ minute: '2-digit',
505
+ hour12: true
506
+ }).replace(' at', ',');
507
+ }
508
+ catch (error) {
509
+ logger.warn(`Failed to format due date: ${timestamp}`, error);
510
+ throw new Error(`Invalid timestamp: ${timestamp}`);
511
+ }
512
+ }
513
+ /**
514
+ * Format a date for display in errors and messages
515
+ * @param timestamp The timestamp to format
516
+ * @returns A human-readable relative time (e.g., "2 hours ago")
517
+ */
518
+ export function formatRelativeTime(timestamp) {
519
+ if (!timestamp)
520
+ return 'Unknown';
521
+ const timestampNum = typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp;
522
+ const now = Date.now();
523
+ const diffMs = now - timestampNum;
524
+ // Convert to appropriate time unit
525
+ const diffSec = Math.floor(diffMs / 1000);
526
+ if (diffSec < 60)
527
+ return `${diffSec} seconds ago`;
528
+ const diffMin = Math.floor(diffSec / 60);
529
+ if (diffMin < 60)
530
+ return `${diffMin} minutes ago`;
531
+ const diffHour = Math.floor(diffMin / 60);
532
+ if (diffHour < 24)
533
+ return `${diffHour} hours ago`;
534
+ const diffDays = Math.floor(diffHour / 24);
535
+ if (diffDays < 30)
536
+ return `${diffDays} days ago`;
537
+ const diffMonths = Math.floor(diffDays / 30);
538
+ if (diffMonths < 12)
539
+ return `${diffMonths} months ago`;
540
+ const diffYears = Math.floor(diffMonths / 12);
541
+ return `${diffYears} years ago`;
542
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Resolver Utility Functions
6
+ *
7
+ * This module provides utilities for resolving entity IDs from names or other identifiers.
8
+ */
9
+ import { clickUpServices } from '../services/shared.js';
10
+ import { findListIDByName } from '../tools/list.js';
11
+ /**
12
+ * Check if a name matches another name using a variety of matching strategies
13
+ * Returns a structured result with match quality information rather than just a boolean
14
+ *
15
+ * @param actualName The actual name to check
16
+ * @param searchName The name being searched for
17
+ * @returns A structured result with match details
18
+ */
19
+ export function isNameMatch(actualName, searchName) {
20
+ if (!actualName || !searchName) {
21
+ return { isMatch: false, score: 0, exactMatch: false, reason: 'One of the names is empty' };
22
+ }
23
+ // Remove any extra whitespace
24
+ const normalizedActualName = actualName.trim();
25
+ const normalizedSearchName = searchName.trim();
26
+ // Handle empty names after normalization
27
+ if (normalizedActualName === '') {
28
+ return { isMatch: false, score: 0, exactMatch: false, reason: 'Actual name is empty' };
29
+ }
30
+ if (normalizedSearchName === '') {
31
+ return { isMatch: false, score: 0, exactMatch: false, reason: 'Search name is empty' };
32
+ }
33
+ // 1. Exact match (highest quality)
34
+ if (normalizedActualName === normalizedSearchName) {
35
+ return {
36
+ isMatch: true,
37
+ score: 100,
38
+ exactMatch: true,
39
+ reason: 'Exact match'
40
+ };
41
+ }
42
+ // 2. Case-insensitive exact match (high quality)
43
+ if (normalizedActualName.toLowerCase() === normalizedSearchName.toLowerCase()) {
44
+ return {
45
+ isMatch: true,
46
+ score: 90,
47
+ exactMatch: true,
48
+ reason: 'Case-insensitive exact match'
49
+ };
50
+ }
51
+ // 3. Match after removing emojis (moderate quality)
52
+ const actualNameWithoutEmoji = normalizedActualName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim();
53
+ const searchNameWithoutEmoji = normalizedSearchName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim();
54
+ if (actualNameWithoutEmoji === searchNameWithoutEmoji) {
55
+ return {
56
+ isMatch: true,
57
+ score: 80,
58
+ exactMatch: false,
59
+ reason: 'Exact match after removing emojis'
60
+ };
61
+ }
62
+ if (actualNameWithoutEmoji.toLowerCase() === searchNameWithoutEmoji.toLowerCase()) {
63
+ return {
64
+ isMatch: true,
65
+ score: 70,
66
+ exactMatch: false,
67
+ reason: 'Case-insensitive match after removing emojis'
68
+ };
69
+ }
70
+ // 4. Substring matches (lower quality)
71
+ const lowerActual = normalizedActualName.toLowerCase();
72
+ const lowerSearch = normalizedSearchName.toLowerCase();
73
+ // Full substring (term completely contained)
74
+ if (lowerActual.includes(lowerSearch)) {
75
+ return {
76
+ isMatch: true,
77
+ score: 60,
78
+ exactMatch: false,
79
+ reason: 'Search term found as substring in actual name'
80
+ };
81
+ }
82
+ if (lowerSearch.includes(lowerActual)) {
83
+ return {
84
+ isMatch: true,
85
+ score: 50,
86
+ exactMatch: false,
87
+ reason: 'Actual name found as substring in search term'
88
+ };
89
+ }
90
+ // 5. Fuzzy emoji-less matches (lowest quality)
91
+ const lowerActualNoEmoji = actualNameWithoutEmoji.toLowerCase();
92
+ const lowerSearchNoEmoji = searchNameWithoutEmoji.toLowerCase();
93
+ if (lowerActualNoEmoji.includes(lowerSearchNoEmoji)) {
94
+ return {
95
+ isMatch: true,
96
+ score: 40,
97
+ exactMatch: false,
98
+ reason: 'Search term (without emoji) found as substring in actual name'
99
+ };
100
+ }
101
+ if (lowerSearchNoEmoji.includes(lowerActualNoEmoji)) {
102
+ return {
103
+ isMatch: true,
104
+ score: 30,
105
+ exactMatch: false,
106
+ reason: 'Actual name (without emoji) found as substring in search term'
107
+ };
108
+ }
109
+ // No match found
110
+ return {
111
+ isMatch: false,
112
+ score: 0,
113
+ exactMatch: false,
114
+ reason: 'No match found with any matching strategy'
115
+ };
116
+ }
117
+ /**
118
+ * Resolve a list ID from either a direct ID or list name
119
+ */
120
+ export async function resolveListId(listId, listName, workspaceService = clickUpServices.workspace) {
121
+ // If list ID is directly provided, use it
122
+ if (listId) {
123
+ return listId;
124
+ }
125
+ // If list name is provided, find the corresponding ID
126
+ if (listName) {
127
+ const listInfo = await findListIDByName(workspaceService, listName);
128
+ if (!listInfo) {
129
+ throw new Error(`List "${listName}" not found`);
130
+ }
131
+ return listInfo.id;
132
+ }
133
+ // If neither is provided, throw an error
134
+ throw new Error("Either listId or listName must be provided");
135
+ }