UNPKG

limitless-mcp

Version:

MCP server for Limitless API - Connect your Pendant data to Claude and other LLMs

521 lines (520 loc) 26.4 kB
import { z } from "zod"; /** * A plugin that provides natural language time reference parsing */ export class TimeParserPlugin { constructor() { this.name = "time-parser"; this.description = "Parses natural language time references into specific date ranges"; this.version = "1.0.0"; this.config = {}; } async initialize(server, config) { this.server = server; this.config = config; // Register tool for parsing natural language time references server.tool("parse_time_reference", { timeReference: z.string().describe("Natural language time reference (e.g., 'yesterday', 'last week')"), timezone: z.string().optional().describe("IANA timezone specifier (e.g., 'America/New_York')"), referenceDate: z.string().optional().describe("Reference date in YYYY-MM-DD format (defaults to today)") }, async ({ timeReference, timezone = "UTC", referenceDate }) => { try { // Parse the reference date or use current date let refDate; if (referenceDate) { refDate = new Date(referenceDate); } else { refDate = new Date(); } // Make sure the reference date is valid if (isNaN(refDate.getTime())) { return { content: [{ type: "text", text: `Invalid reference date: ${referenceDate}` }] }; } // Apply timezone if provided if (timezone) { try { // Format the date with the specified timezone const options = { timeZone: timezone }; const formatter = new Intl.DateTimeFormat('en-US', options); const tzDate = new Date(formatter.format(refDate)); // If this doesn't throw, timezone is valid refDate.toLocaleString('en-US', { timeZone: timezone }); } catch (error) { return { content: [{ type: "text", text: `Invalid timezone: ${timezone}` }] }; } } // Parse the time reference const result = this.parseTimeReference(timeReference, refDate, timezone); if (!result.success || !result.start || !result.end) { return { content: [{ type: "text", text: result.error || "Unable to parse time reference." }] }; } // Format the date range nicely const { start, end } = result; const formatOptions = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZone: timezone }; const startFormatted = start.toLocaleString('en-US', formatOptions); const endFormatted = end.toLocaleString('en-US', formatOptions); // Format for API consumption (ISO format) const startISO = start.toISOString(); const endISO = end.toISOString(); return { content: [{ type: "text", text: `# Parsed Time Reference: "${timeReference}"\n\n` + `Reference Date: ${refDate.toLocaleDateString('en-US', { timeZone: timezone })}\n` + `Timezone: ${timezone}\n\n` + `## Results\n\n` + `- **Start**: ${startFormatted}\n` + `- **End**: ${endFormatted}\n\n` + `## ISO Formatted (for API use)\n\n` + `- **Start**: \`${startISO}\`\n` + `- **End**: \`${endISO}\`\n\n` + `## Search Parameters\n\n` + `\`\`\`json\n` + `{\n` + ` "start": "${startISO}",\n` + ` "end": "${endISO}",\n` + ` "timezone": "${timezone}"\n` + `}\n` + `\`\`\`` }] }; } catch (error) { console.error(`Error parsing time reference:`, error); return { content: [{ type: "text", text: `Error parsing time reference: ${error}` }] }; } }); // Register tool for search with natural language time server.tool("search_with_time", { query: z.string().describe("Search query text"), timeReference: z.string().describe("Natural language time reference (e.g., 'yesterday', 'last week')"), timezone: z.string().optional().describe("IANA timezone specifier"), limit: z.number().optional(), includeContent: z.boolean().default(false).describe("Whether to include content in results") }, async ({ query, timeReference, timezone = "UTC", limit = 10, includeContent }) => { try { // Parse the time reference const refDate = new Date(); const timeResult = this.parseTimeReference(timeReference, refDate, timezone); if (!timeResult.success || !timeResult.start || !timeResult.end) { return { content: [{ type: "text", text: timeResult.error || "Unable to parse time reference." }] }; } // Format the date range for display const { start, end } = timeResult; // The actual search call would happen here // In a real implementation, this would call the Limitless API return { content: [{ type: "text", text: `# Search Results for "${query}" during ${timeReference}\n\n` + `Time period: ${start.toLocaleString()} to ${end.toLocaleString()}\n\n` + `The search would be performed with these parameters:\n\n` + `\`\`\`json\n` + `{\n` + ` "query": "${query}",\n` + ` "start": "${start.toISOString()}",\n` + ` "end": "${end.toISOString()}",\n` + ` "timezone": "${timezone}",\n` + ` "limit": ${limit},\n` + ` "includeContent": ${includeContent}\n` + `}\n` + `\`\`\`\n\n` + `Note: This is a placeholder response. The actual search functionality would call the Limitless API.` }] }; } catch (error) { console.error(`Error searching with time reference:`, error); return { content: [{ type: "text", text: `Error searching with time reference: ${error}` }] }; } }); // Enhance existing list_lifelogs and search_lifelogs tools to accept natural language time this.enhanceExistingTools(server); } // Add natural language time capabilities to existing tools enhanceExistingTools(server) { // This is a placeholder for the implementation // In a real implementation, we would monkey-patch the existing tools // or register new tools with the same names but enhanced functionality console.error("Natural language time enhancements would be applied to existing tools"); } // Parse a natural language time reference into a date range parseTimeReference(reference, refDate = new Date(), timezone = "UTC") { // Normalize the reference const normalizedRef = reference.toLowerCase().trim(); // For creating dates in the specified timezone const createDate = (year, month, day, hour = 0, minute = 0) => { const date = new Date(Date.UTC(year, month, day, hour, minute)); // Apply timezone adjustment const offset = this.getTimezoneOffset(timezone, date); date.setTime(date.getTime() - offset); return date; }; // Get reference date components const refYear = refDate.getFullYear(); const refMonth = refDate.getMonth(); const refDay = refDate.getDate(); const refDayOfWeek = refDate.getDay(); // 0 = Sunday, 6 = Saturday // Handle different time references try { // Today if (normalizedRef === "today") { const start = createDate(refYear, refMonth, refDay, 0, 0); const end = createDate(refYear, refMonth, refDay, 23, 59); return { success: true, start, end }; } // Yesterday if (normalizedRef === "yesterday") { const yesterday = new Date(refDate); yesterday.setDate(refDate.getDate() - 1); const yesterdayYear = yesterday.getFullYear(); const yesterdayMonth = yesterday.getMonth(); const yesterdayDay = yesterday.getDate(); const start = createDate(yesterdayYear, yesterdayMonth, yesterdayDay, 0, 0); const end = createDate(yesterdayYear, yesterdayMonth, yesterdayDay, 23, 59); return { success: true, start, end }; } // Tomorrow if (normalizedRef === "tomorrow") { const tomorrow = new Date(refDate); tomorrow.setDate(refDate.getDate() + 1); const tomorrowYear = tomorrow.getFullYear(); const tomorrowMonth = tomorrow.getMonth(); const tomorrowDay = tomorrow.getDate(); const start = createDate(tomorrowYear, tomorrowMonth, tomorrowDay, 0, 0); const end = createDate(tomorrowYear, tomorrowMonth, tomorrowDay, 23, 59); return { success: true, start, end }; } // This week if (normalizedRef === "this week") { // Get the start of the week (Sunday) const daysToSunday = refDayOfWeek; const startDay = new Date(refDate); startDay.setDate(refDay - daysToSunday); // Get the end of the week (Saturday) const daysToSaturday = 6 - refDayOfWeek; const endDay = new Date(refDate); endDay.setDate(refDay + daysToSaturday); const start = createDate(startDay.getFullYear(), startDay.getMonth(), startDay.getDate(), 0, 0); const end = createDate(endDay.getFullYear(), endDay.getMonth(), endDay.getDate(), 23, 59); return { success: true, start, end }; } // Last week if (normalizedRef === "last week") { // Get the start of last week (Sunday) const daysToLastSunday = refDayOfWeek + 7; const startDay = new Date(refDate); startDay.setDate(refDay - daysToLastSunday); // Get the end of last week (Saturday) const daysToLastSaturday = 6 - refDayOfWeek + 7; const endDay = new Date(refDate); endDay.setDate(refDay - daysToLastSaturday); const start = createDate(startDay.getFullYear(), startDay.getMonth(), startDay.getDate(), 0, 0); const end = createDate(endDay.getFullYear(), endDay.getMonth(), endDay.getDate(), 23, 59); return { success: true, start, end }; } // Next week if (normalizedRef === "next week") { // Get the start of next week (Sunday) const daysToNextSunday = 7 - refDayOfWeek; const startDay = new Date(refDate); startDay.setDate(refDay + daysToNextSunday); // Get the end of next week (Saturday) const daysToNextSaturday = 13 - refDayOfWeek; const endDay = new Date(refDate); endDay.setDate(refDay + daysToNextSaturday); const start = createDate(startDay.getFullYear(), startDay.getMonth(), startDay.getDate(), 0, 0); const end = createDate(endDay.getFullYear(), endDay.getMonth(), endDay.getDate(), 23, 59); return { success: true, start, end }; } // This month if (normalizedRef === "this month") { const start = createDate(refYear, refMonth, 1, 0, 0); // Get the last day of the month const lastDay = new Date(refYear, refMonth + 1, 0).getDate(); const end = createDate(refYear, refMonth, lastDay, 23, 59); return { success: true, start, end }; } // Last month if (normalizedRef === "last month") { // Get the first day of last month const lastMonthDate = new Date(refDate); lastMonthDate.setMonth(refMonth - 1); const lastMonthYear = lastMonthDate.getFullYear(); const lastMonth = lastMonthDate.getMonth(); const start = createDate(lastMonthYear, lastMonth, 1, 0, 0); // Get the last day of last month const lastDay = new Date(lastMonthYear, lastMonth + 1, 0).getDate(); const end = createDate(lastMonthYear, lastMonth, lastDay, 23, 59); return { success: true, start, end }; } // Next month if (normalizedRef === "next month") { // Get the first day of next month const nextMonthDate = new Date(refDate); nextMonthDate.setMonth(refMonth + 1); const nextMonthYear = nextMonthDate.getFullYear(); const nextMonth = nextMonthDate.getMonth(); const start = createDate(nextMonthYear, nextMonth, 1, 0, 0); // Get the last day of next month const lastDay = new Date(nextMonthYear, nextMonth + 1, 0).getDate(); const end = createDate(nextMonthYear, nextMonth, lastDay, 23, 59); return { success: true, start, end }; } // Last X days const lastDaysMatch = normalizedRef.match(/^last\s+(\d+)\s+days?$/); if (lastDaysMatch) { const days = parseInt(lastDaysMatch[1], 10); if (isNaN(days) || days <= 0) { return { success: false, error: "Invalid number of days. Please specify a positive number." }; } // Get the start date (X days ago) const startDay = new Date(refDate); startDay.setDate(refDay - days); // End date is yesterday (since "last X days" typically excludes today) const endDay = new Date(refDate); endDay.setDate(refDay - 1); const start = createDate(startDay.getFullYear(), startDay.getMonth(), startDay.getDate(), 0, 0); const end = createDate(endDay.getFullYear(), endDay.getMonth(), endDay.getDate(), 23, 59); return { success: true, start, end }; } // Next X days const nextDaysMatch = normalizedRef.match(/^next\s+(\d+)\s+days?$/); if (nextDaysMatch) { const days = parseInt(nextDaysMatch[1], 10); if (isNaN(days) || days <= 0) { return { success: false, error: "Invalid number of days. Please specify a positive number." }; } // Start date is tomorrow (since "next X days" typically excludes today) const startDay = new Date(refDate); startDay.setDate(refDay + 1); // Get the end date (X days from tomorrow) const endDay = new Date(refDate); endDay.setDate(refDay + days); const start = createDate(startDay.getFullYear(), startDay.getMonth(), startDay.getDate(), 0, 0); const end = createDate(endDay.getFullYear(), endDay.getMonth(), endDay.getDate(), 23, 59); return { success: true, start, end }; } // Last X weeks const lastWeeksMatch = normalizedRef.match(/^last\s+(\d+)\s+weeks?$/); if (lastWeeksMatch) { const weeks = parseInt(lastWeeksMatch[1], 10); if (isNaN(weeks) || weeks <= 0) { return { success: false, error: "Invalid number of weeks. Please specify a positive number." }; } // Get the start date (X weeks ago from the start of this week) const daysToSunday = refDayOfWeek; const thisWeekSunday = new Date(refDate); thisWeekSunday.setDate(refDay - daysToSunday); const startDay = new Date(thisWeekSunday); startDay.setDate(thisWeekSunday.getDate() - (7 * weeks)); // End date is the Saturday before this week const endDay = new Date(thisWeekSunday); endDay.setDate(thisWeekSunday.getDate() - 1); const start = createDate(startDay.getFullYear(), startDay.getMonth(), startDay.getDate(), 0, 0); const end = createDate(endDay.getFullYear(), endDay.getMonth(), endDay.getDate(), 23, 59); return { success: true, start, end }; } // Specific day of week const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; const dayOfWeekIndex = dayNames.indexOf(normalizedRef); if (dayOfWeekIndex !== -1) { // Find the most recent occurrence of this day const dayDiff = (refDayOfWeek - dayOfWeekIndex + 7) % 7; const dayDate = new Date(refDate); if (dayDiff === 0) { // It's today, so use today dayDate.setDate(refDay); } else { // Use the most recent past occurrence dayDate.setDate(refDay - dayDiff); } const start = createDate(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), 0, 0); const end = createDate(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), 23, 59); return { success: true, start, end }; } // Last [day of week] for (const dayName of dayNames) { if (normalizedRef === `last ${dayName}`) { const targetDayIndex = dayNames.indexOf(dayName); // Calculate days difference to the previous occurrence let dayDiff = (refDayOfWeek - targetDayIndex + 7) % 7; // If today is the target day, we need to go back a full week if (dayDiff === 0) { dayDiff = 7; } const dayDate = new Date(refDate); dayDate.setDate(refDay - dayDiff); const start = createDate(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), 0, 0); const end = createDate(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), 23, 59); return { success: true, start, end }; } } // Handle more complex cases like "3 days ago" and "2 weeks ago" // X days ago const daysAgoMatch = normalizedRef.match(/^(\d+)\s+days?\s+ago$/); if (daysAgoMatch) { const daysAgo = parseInt(daysAgoMatch[1], 10); if (isNaN(daysAgo) || daysAgo <= 0) { return { success: false, error: "Invalid number of days. Please specify a positive number." }; } const dayDate = new Date(refDate); dayDate.setDate(refDay - daysAgo); const start = createDate(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), 0, 0); const end = createDate(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), 23, 59); return { success: true, start, end }; } // X weeks ago const weeksAgoMatch = normalizedRef.match(/^(\d+)\s+weeks?\s+ago$/); if (weeksAgoMatch) { const weeksAgo = parseInt(weeksAgoMatch[1], 10); if (isNaN(weeksAgo) || weeksAgo <= 0) { return { success: false, error: "Invalid number of weeks. Please specify a positive number." }; } // Get the date X weeks ago const weeksDate = new Date(refDate); weeksDate.setDate(refDay - (7 * weeksAgo)); // Calculate the start and end of that week const weekDayOfWeek = weeksDate.getDay(); // Start of the week (Sunday) const startDay = new Date(weeksDate); startDay.setDate(weeksDate.getDate() - weekDayOfWeek); // End of the week (Saturday) const endDay = new Date(weeksDate); endDay.setDate(weeksDate.getDate() + (6 - weekDayOfWeek)); const start = createDate(startDay.getFullYear(), startDay.getMonth(), startDay.getDate(), 0, 0); const end = createDate(endDay.getFullYear(), endDay.getMonth(), endDay.getDate(), 23, 59); return { success: true, start, end }; } // Relative date ranges like "from X to Y" const rangeMatch = normalizedRef.match(/^from\s+(.+)\s+to\s+(.+)$/); if (rangeMatch) { const fromRef = rangeMatch[1].trim(); const toRef = rangeMatch[2].trim(); // Parse the individual references const fromResult = this.parseTimeReference(fromRef, refDate, timezone); const toResult = this.parseTimeReference(toRef, refDate, timezone); if (!fromResult.success || !fromResult.start) { return { success: false, error: `Could not parse the start date: ${fromRef}` }; } if (!toResult.success || !toResult.end) { return { success: false, error: `Could not parse the end date: ${toRef}` }; } return { success: true, start: fromResult.start, end: toResult.end }; } // If we couldn't match any pattern return { success: false, error: `Could not parse time reference: "${reference}"` }; } catch (error) { return { success: false, error: `Error parsing time reference: ${error}` }; } } // Helper to get timezone offset in milliseconds getTimezoneOffset(timezone, date) { try { // Get the time in the specified timezone const options = { timeZone: timezone, year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false }; // Use the formatter to get date parts in the target timezone const formatter = new Intl.DateTimeFormat('en-US', options); const parts = formatter.formatToParts(date); // Extract the date components const timeParts = {}; parts.forEach(part => { if (part.type !== 'literal') { timeParts[part.type] = parseInt(part.value, 10); } }); // Create a date in UTC with these components const targetDate = Date.UTC(timeParts.year, timeParts.month - 1, // JavaScript months are 0-indexed timeParts.day, timeParts.hour, timeParts.minute, timeParts.second); // Calculate the offset return date.getTime() - targetDate; } catch (error) { console.error(`Error calculating timezone offset:`, error); return 0; // Default to no offset } } }