UNPKG

@taazkareem/clickup-mcp-server

Version:

ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol

343 lines (342 loc) 14.2 kB
/** * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com> * SPDX-License-Identifier: MIT * * Date Utility Functions * * This module provides utilities for handling dates, timestamps, and due date parsing. */ import { Logger } from '../logger.js'; // Create a logger instance for date utilities const logger = new Logger('DateUtils'); /** * Get a timestamp for a relative time * * @param minutes Minutes from now * @param hours Hours from now * @param days Days from now * @param weeks Weeks from now * @param months Months from now * @returns Timestamp in milliseconds */ export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0, months = 0) { const now = new Date(); if (minutes) now.setMinutes(now.getMinutes() + minutes); if (hours) now.setHours(now.getHours() + hours); if (days) now.setDate(now.getDate() + days); if (weeks) now.setDate(now.getDate() + (weeks * 7)); if (months) now.setMonth(now.getMonth() + months); return now.getTime(); } /** * Get the start of today (midnight) in Unix milliseconds * @returns Timestamp in milliseconds for start of current day */ function getStartOfDay() { const now = new Date(); now.setHours(0, 0, 0, 0); return now.getTime(); } /** * Get the end of today (23:59:59.999) in Unix milliseconds * @returns Timestamp in milliseconds for end of current day */ function getEndOfDay() { const now = new Date(); now.setHours(23, 59, 59, 999); return now.getTime(); } /** * Get the current time in Unix milliseconds * @returns Current timestamp in milliseconds */ function getCurrentTimestamp() { return new Date().getTime(); } /** * Parse a due date string into a timestamp * Supports ISO 8601 format or natural language like "tomorrow" * * @param dateString Date string to parse * @returns Timestamp in milliseconds or undefined if parsing fails */ export function parseDueDate(dateString) { if (!dateString) return undefined; try { // First, try to parse as a direct timestamp const numericValue = Number(dateString); if (!isNaN(numericValue) && numericValue > 0) { // If it's a reasonable timestamp (after year 2000), use it if (numericValue > 946684800000) { // Jan 1, 2000 return numericValue; } } // Handle natural language dates const lowerDate = dateString.toLowerCase().trim(); // Handle "now" specifically if (lowerDate === 'now') { return getCurrentTimestamp(); } // Handle "today" with different options if (lowerDate === 'today') { return getEndOfDay(); } if (lowerDate === 'today start' || lowerDate === 'start of today') { return getStartOfDay(); } if (lowerDate === 'today end' || lowerDate === 'end of today') { return getEndOfDay(); } // Handle "yesterday" and "tomorrow" if (lowerDate === 'yesterday') { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); yesterday.setHours(23, 59, 59, 999); return yesterday.getTime(); } if (lowerDate === 'tomorrow') { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(23, 59, 59, 999); return tomorrow.getTime(); } // Handle day names (Monday, Tuesday, etc.) - find next occurrence const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const dayMatch = lowerDate.match(/\b(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/); if (dayMatch) { const targetDayName = dayMatch[1]; const targetDayIndex = dayNames.indexOf(targetDayName); const today = new Date(); const currentDayIndex = today.getDay(); // Calculate days until target day let daysUntilTarget = targetDayIndex - currentDayIndex; if (daysUntilTarget <= 0) { daysUntilTarget += 7; // Next week } // Handle "next" prefix explicitly if (lowerDate.includes('next ')) { daysUntilTarget += 7; } const targetDate = new Date(today); targetDate.setDate(today.getDate() + daysUntilTarget); // Extract time if specified (e.g., "Friday at 3pm", "Saturday 2:30pm") const timeMatch = lowerDate.match(/(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i); if (timeMatch) { let hours = parseInt(timeMatch[1]); const minutes = timeMatch[2] ? parseInt(timeMatch[2]) : 0; const meridian = timeMatch[3]?.toLowerCase(); // Convert to 24-hour format if (meridian === 'pm' && hours < 12) hours += 12; if (meridian === 'am' && hours === 12) hours = 0; targetDate.setHours(hours, minutes, 0, 0); } else { // Default to end of day if no time specified targetDate.setHours(23, 59, 59, 999); } return targetDate.getTime(); } // Handle relative dates with specific times 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; const match = lowerDate.match(relativeTimeRegex); if (match) { const date = new Date(); const [_, amount, unit, hours, minutes, meridian] = match; // Calculate the future date if (amount && unit) { const value = parseInt(amount); if (unit.startsWith('minute')) { date.setMinutes(date.getMinutes() + value); } else if (unit.startsWith('hour')) { date.setHours(date.getHours() + value); } else if (unit.startsWith('day')) { date.setDate(date.getDate() + value); } else if (unit.startsWith('week')) { date.setDate(date.getDate() + (value * 7)); } else if (unit.startsWith('month')) { date.setMonth(date.getMonth() + value); } } else if (lowerDate.startsWith('tomorrow')) { date.setDate(date.getDate() + 1); } else if (lowerDate.includes('next week')) { date.setDate(date.getDate() + 7); } else if (lowerDate.includes('next month')) { date.setMonth(date.getMonth() + 1); } else if (lowerDate.includes('next year')) { date.setFullYear(date.getFullYear() + 1); } // Set the time if specified if (hours) { let parsedHours = parseInt(hours); const parsedMinutes = minutes ? parseInt(minutes) : 0; // Convert to 24-hour format if meridian is specified if (meridian?.toLowerCase() === 'pm' && parsedHours < 12) parsedHours += 12; if (meridian?.toLowerCase() === 'am' && parsedHours === 12) parsedHours = 0; date.setHours(parsedHours, parsedMinutes, 0, 0); } else { // Default to end of day if no time specified date.setHours(23, 59, 59, 999); } return date.getTime(); } // Handle various relative formats const relativeFormats = [ { regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) }, { regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) }, { regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) }, { regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) }, { regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) } ]; for (const format of relativeFormats) { if (format.regex.test(lowerDate)) { const value = parseInt(lowerDate.match(format.regex)[1]); return format.handler(value); } } // Handle specific date formats // Format: MM/DD/YYYY const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s+(am|pm))?)?$/i; const usDateMatch = lowerDate.match(usDateRegex); if (usDateMatch) { const [_, month, day, year, hours, minutes, meridian] = usDateMatch; const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed parseInt(day)); // Add time if specified if (hours) { let parsedHours = parseInt(hours); const parsedMinutes = minutes ? parseInt(minutes) : 0; // Convert to 24-hour format if meridian is specified if (meridian?.toLowerCase() === 'pm' && parsedHours < 12) parsedHours += 12; if (meridian?.toLowerCase() === 'am' && parsedHours === 12) parsedHours = 0; date.setHours(parsedHours, parsedMinutes, 0, 0); } else { // Default to end of day if no time specified date.setHours(23, 59, 59, 999); } return date.getTime(); } // Enhanced fallback: Try JavaScript's native Date constructor with various formats // This handles many natural language formats like "Saturday at 3pm EST", "next Friday", etc. const nativeDate = new Date(dateString); if (!isNaN(nativeDate.getTime())) { // Check if the parsed date is reasonable (not too far in the past or future) const now = Date.now(); const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000); const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000); if (nativeDate.getTime() > oneYearAgo && nativeDate.getTime() < tenYearsFromNow) { return nativeDate.getTime(); } } // Try some common variations and transformations const variations = [ dateString.replace(/\s+at\s+/i, ' '), // "Saturday at 3pm" -> "Saturday 3pm" dateString.replace(/\s+EST|EDT|PST|PDT|CST|CDT|MST|MDT/i, ''), // Remove timezone dateString.replace(/next\s+/i, ''), // "next Friday" -> "Friday" dateString.replace(/this\s+/i, ''), // "this Friday" -> "Friday" ]; for (const variation of variations) { const varDate = new Date(variation); if (!isNaN(varDate.getTime())) { const now = Date.now(); const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000); const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000); if (varDate.getTime() > oneYearAgo && varDate.getTime() < tenYearsFromNow) { // If the parsed date is in the past, assume they meant next occurrence if (varDate.getTime() < now) { // Add 7 days if it's a day of the week if (dateString.match(/monday|tuesday|wednesday|thursday|friday|saturday|sunday/i)) { varDate.setDate(varDate.getDate() + 7); } } return varDate.getTime(); } } } // If all parsing fails, return undefined return undefined; } catch (error) { logger.warn(`Failed to parse due date: ${dateString}`, error); throw new Error(`Invalid date format: ${dateString}`); } } /** * Format a due date timestamp into a human-readable string * * @param timestamp Unix timestamp in milliseconds * @returns Formatted date string or undefined if timestamp is invalid */ export function formatDueDate(timestamp) { if (!timestamp) return undefined; try { const date = new Date(timestamp); if (isNaN(date.getTime())) return undefined; // Format: "March 10, 2025 at 10:56 PM" return date.toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).replace(' at', ','); } catch (error) { logger.warn(`Failed to format due date: ${timestamp}`, error); throw new Error(`Invalid timestamp: ${timestamp}`); } } /** * Format a date for display in errors and messages * @param timestamp The timestamp to format * @returns A human-readable relative time (e.g., "2 hours ago") */ export function formatRelativeTime(timestamp) { if (!timestamp) return 'Unknown'; const timestampNum = typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp; const now = Date.now(); const diffMs = now - timestampNum; // Convert to appropriate time unit const diffSec = Math.floor(diffMs / 1000); if (diffSec < 60) return `${diffSec} seconds ago`; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return `${diffMin} minutes ago`; const diffHour = Math.floor(diffMin / 60); if (diffHour < 24) return `${diffHour} hours ago`; const diffDays = Math.floor(diffHour / 24); if (diffDays < 30) return `${diffDays} days ago`; const diffMonths = Math.floor(diffDays / 30); if (diffMonths < 12) return `${diffMonths} months ago`; const diffYears = Math.floor(diffMonths / 12); return `${diffYears} years ago`; }