UNPKG

199bio-mcp-limitless-server

Version:

Scientifically Validated Speech Vitality Index for Limitless Pendant. Empirically validated metrics from 2,500+ conversation segments with transparent reliability assessment. Peer-reviewed methodology for clinical research applications.

200 lines 8.58 kB
// Native fetch is available in Node.js >= 18 const LIMITLESS_API_URL = process.env.LIMITLESS_API_URL || "https://api.limitless.ai"; const API_TIMEOUT_MS = 120000; // 120 seconds timeout for API calls export class LimitlessApiError extends Error { status; responseBody; constructor(message, status, responseBody) { super(message); this.status = status; this.responseBody = responseBody; this.name = "LimitlessApiError"; } } // Helper to get the system's default IANA timezone name with fallback function getDefaultTimezone() { try { // Primary method: Intl API const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; if (tz && tz !== 'UTC') { return tz; } } catch (e) { // Intl API failed, continue to fallback } try { // Fallback: Use Date().toString() parsing (less reliable but better than nothing) const dateStr = new Date().toString(); const tzMatch = dateStr.match(/\(([^)]+)\)$/); if (tzMatch) { // Common timezone abbreviations to IANA mapping const abbrevMap = { 'EST': 'America/New_York', 'EDT': 'America/New_York', 'CST': 'America/Chicago', 'CDT': 'America/Chicago', 'MST': 'America/Denver', 'MDT': 'America/Denver', 'PST': 'America/Los_Angeles', 'PDT': 'America/Los_Angeles', 'GMT': 'Europe/London', 'BST': 'Europe/London', }; const abbrev = tzMatch[1].split(' ').map(w => w[0]).join(''); return abbrevMap[abbrev] || undefined; } } catch (e) { // Fallback failed too } // If all else fails, return undefined (API will use its default) return undefined; } async function makeApiRequest(apiKey, endpoint, params) { if (!apiKey) { // This error happens before connection, so logging might be okay, but let's throw directly. throw new LimitlessApiError("Limitless API key is missing. Please set LIMITLESS_API_KEY environment variable.", 401); } const url = new URL(`${LIMITLESS_API_URL}/${endpoint}`); const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined) { queryParams.set(key, String(value)); } } url.search = queryParams.toString(); const requestUrl = url.toString(); // Cannot log here reliably for stdio // console.error(`[Limitless Client] Requesting: ${requestUrl}`); let response; // Uses global Response type now const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS); try { response = await fetch(requestUrl, { headers: { "X-API-Key": apiKey, "Accept": "application/json", }, signal: controller.signal // Pass abort signal }); clearTimeout(timeoutId); // Clear timeout if fetch completes if (!response.ok) { // Read body as text first to avoid "body used already" const errorText = await response.text(); let errorBody; try { errorBody = JSON.parse(errorText); } catch (e) { errorBody = errorText; } // Provide more specific error messages based on status code let errorMessage = `Limitless API Error: ${response.status} ${response.statusText}`; if (response.status === 401) { errorMessage = "Authentication failed: Invalid or missing API key"; } else if (response.status === 404) { errorMessage = endpoint.includes('/v1/lifelogs/') ? `Lifelog not found with ID: ${endpoint.split('/').pop()}` : "Resource not found"; } else if (response.status === 429) { errorMessage = "Rate limit exceeded: Too many requests"; } else if (response.status >= 500) { errorMessage = `Server error (${response.status}): Limitless API is experiencing issues`; } // Include request details for debugging if (errorBody && typeof errorBody === 'object' && errorBody.message) { errorMessage += ` - ${errorBody.message}`; } throw new LimitlessApiError(errorMessage, response.status, errorBody); } // Only parse JSON if response is ok return await response.json(); } catch (error) { clearTimeout(timeoutId); // Clear timeout on error too if (error.name === 'AbortError') { // Cannot log here reliably for stdio // console.error(`[Limitless Client] Timeout error for ${requestUrl}`); throw new LimitlessApiError(`Limitless API request timed out after ${API_TIMEOUT_MS}ms`, 504); } // Don't log generic network errors here either, just re-throw if (error instanceof LimitlessApiError) { throw error; } throw new LimitlessApiError(`Network error calling Limitless API: ${error instanceof Error ? error.message : String(error)}`); } } export async function getLifelogs(apiKey, options = {}) { const allLifelogs = []; let currentCursor = options.cursor; const limit = options.limit; const batchSize = options.batch_size || 10; // Now configurable with default const defaultTimezone = getDefaultTimezone(); const originalOptions = { includeMarkdown: options.includeMarkdown ?? true, includeHeadings: options.includeHeadings ?? true, date: options.date, start: options.start, end: options.end, direction: options.direction ?? 'desc', timezone: options.timezone ?? defaultTimezone, isStarred: options.isStarred, }; // Cannot log here reliably for stdio // if (originalOptions.timezone === undefined && defaultTimezone === undefined) { ... } let page = 0; while (true) { page++; const remainingNeeded = limit !== undefined ? limit - allLifelogs.length : Infinity; if (remainingNeeded <= 0 && limit !== undefined) break; const fetchLimit = Math.min(batchSize, remainingNeeded === Infinity ? batchSize : remainingNeeded); const params = { limit: fetchLimit, includeMarkdown: originalOptions.includeMarkdown, includeHeadings: originalOptions.includeHeadings, date: originalOptions.date, start: originalOptions.start, end: originalOptions.end, direction: originalOptions.direction, timezone: originalOptions.timezone, cursor: currentCursor, isStarred: originalOptions.isStarred, }; if (!params.timezone) delete params.timezone; if (params.isStarred === undefined) delete params.isStarred; // Cannot log here reliably for stdio // console.error(`[Limitless Client] Fetching page ${page} ...`); const response = await makeApiRequest(apiKey, "v1/lifelogs", params); const lifelogs = response.data?.lifelogs ?? []; // Cannot log here reliably for stdio // console.error(`[Limitless Client] Received ${lifelogs.length} logs from page ${page}.`); allLifelogs.push(...lifelogs); const nextCursor = response.meta?.lifelogs?.nextCursor; if (!nextCursor || lifelogs.length < fetchLimit || (limit !== undefined && allLifelogs.length >= limit)) { break; } currentCursor = nextCursor; } return limit !== undefined ? allLifelogs.slice(0, limit) : allLifelogs; } export async function getLifelogById(apiKey, lifelogId, options = {}) { // Cannot log here reliably for stdio // console.error(`[Limitless Client] Requesting lifelog by ID: ${lifelogId}`); const params = { includeMarkdown: options.includeMarkdown ?? true, includeHeadings: options.includeHeadings ?? true, }; const response = await makeApiRequest(apiKey, `v1/lifelogs/${lifelogId}`, params); if (!response.data?.lifelog) { throw new LimitlessApiError(`Lifelog with ID ${lifelogId} not found`, 404); } return response.data.lifelog; } //# sourceMappingURL=limitless-client.js.map