UNPKG

limitless-ai-mcp-server

Version:

MCP server for integrating Limitless AI Pendant recordings with AI assistants

238 lines 10.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LimitlessClient = exports.LimitlessAPIError = void 0; const logger_1 = require("../utils/logger"); const retry_1 = require("../utils/retry"); const date_1 = require("../utils/date"); const cache_1 = require("./cache"); const DEFAULT_BASE_URL = 'https://api.limitless.ai/v1'; const DEFAULT_TIMEOUT = 120000; // 120 seconds const DEFAULT_RETRY_ATTEMPTS = 3; const DEFAULT_RETRY_DELAY = 1000; class LimitlessAPIError extends Error { statusCode; code; details; constructor(message, statusCode, code, details) { super(message); this.statusCode = statusCode; this.code = code; this.details = details; this.name = 'LimitlessAPIError'; } } exports.LimitlessAPIError = LimitlessAPIError; class LimitlessClient { apiKey; baseUrl; timeout; retryAttempts; retryDelay; constructor(config) { if (!config.apiKey) { throw new Error('API key is required'); } this.apiKey = config.apiKey; this.baseUrl = config.baseUrl || DEFAULT_BASE_URL; this.timeout = config.timeout || DEFAULT_TIMEOUT; this.retryAttempts = config.retryAttempts || DEFAULT_RETRY_ATTEMPTS; this.retryDelay = config.retryDelay || DEFAULT_RETRY_DELAY; } async makeRequest(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await (0, retry_1.retry)(async () => { const res = await fetch(url, { ...options, headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json', ...options.headers, }, signal: controller.signal, }); if (!res.ok) { const errorData = (await res.json().catch(() => ({ message: res.statusText }))); throw new LimitlessAPIError(errorData.message || `HTTP ${res.status}`, res.status, errorData.code, errorData); } return res; }, { attempts: this.retryAttempts, delay: this.retryDelay, shouldRetry: (error) => { if (error instanceof LimitlessAPIError) { // Retry on 5xx errors or specific 4xx errors return error.statusCode ? error.statusCode >= 500 || error.statusCode === 429 : false; } return true; // Retry on network errors }, }); const data = await response.json(); return data; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw new LimitlessAPIError('Request timeout', 408, 'TIMEOUT'); } if (error instanceof Error) { throw error; } throw new Error(String(error)); } finally { clearTimeout(timeoutId); } } async getLifelogById(id, options = {}) { logger_1.logger.debug(`Fetching lifelog with ID: ${id}`); // Check cache first const cacheKey = (0, cache_1.buildLifelogCacheKey)(id); const cached = cache_1.lifelogCache.get(cacheKey); if (cached) { logger_1.logger.debug(`Lifelog ${id} retrieved from cache`); return cached; } const params = new URLSearchParams(); if (options.includeMarkdown !== undefined) { params.append('includeMarkdown', String(options.includeMarkdown)); } if (options.includeHeadings !== undefined) { params.append('includeHeadings', String(options.includeHeadings)); } const queryString = params.toString(); const endpoint = `/lifelogs/${id}${queryString ? `?${queryString}` : ''}`; const response = await this.makeRequest(endpoint); if (response.error) { throw new LimitlessAPIError(response.error.message, undefined, response.error.code); } // Cache the result cache_1.lifelogCache.set(cacheKey, response.data); return response.data; } async listLifelogsByDate(date, options = {}) { const formattedDate = (0, date_1.formatDate)(date); logger_1.logger.debug(`Listing lifelogs for date: ${formattedDate}`); // Check cache first const cacheKey = (0, cache_1.buildDateCacheKey)(formattedDate, options); const cached = cache_1.lifelogArrayCache.get(cacheKey); if (cached) { logger_1.logger.debug(`Lifelogs for ${formattedDate} retrieved from cache`); return cached; } const params = this.buildQueryParams(options); params.append('date', formattedDate); const result = await this.fetchAllLifelogs('/lifelogs', params, options.limit); // Cache the result cache_1.lifelogArrayCache.set(cacheKey, result); return result; } async listLifelogsByRange(options) { const { start, end, ...listOptions } = options; const formattedStart = (0, date_1.formatDate)(start); const formattedEnd = (0, date_1.formatDate)(end); logger_1.logger.debug(`Listing lifelogs from ${formattedStart} to ${formattedEnd}`); const params = this.buildQueryParams(listOptions); params.append('start', formattedStart); params.append('end', formattedEnd); return this.fetchAllLifelogs('/lifelogs', params, listOptions.limit); } async listRecentLifelogs(options = {}) { const limit = options.limit || 10; logger_1.logger.debug(`Listing ${limit} recent lifelogs`); // Check cache first const cacheKey = (0, cache_1.buildRecentCacheKey)(options); const cached = cache_1.lifelogArrayCache.get(cacheKey); if (cached) { logger_1.logger.debug(`Recent lifelogs retrieved from cache`); return cached; } const params = this.buildQueryParams(options); params.append('recent', 'true'); const result = await this.fetchAllLifelogs('/lifelogs', params, limit); // Cache the result cache_1.lifelogArrayCache.set(cacheKey, result); return result; } async searchLifelogs(options) { const { searchTerm, fetchLimit = 20, ...listOptions } = options; logger_1.logger.debug(`Searching for "${searchTerm}" in recent ${fetchLimit} lifelogs`); // Check search cache first const cacheKey = (0, cache_1.buildSearchCacheKey)(searchTerm, options); const cached = cache_1.searchCache.get(cacheKey); if (cached) { logger_1.logger.debug(`Search results for "${searchTerm}" retrieved from cache`); return cached; } // First fetch recent lifelogs const recentLogs = await this.listRecentLifelogs({ ...listOptions, limit: fetchLimit, includeMarkdown: true, }); // Search within the fetched logs const searchLower = searchTerm.toLowerCase(); const results = recentLogs.filter((log) => { const titleMatch = log.title?.toLowerCase().includes(searchLower); const markdownMatch = log.markdown?.toLowerCase().includes(searchLower); // Search in contents const contentsMatch = log.contents?.some((content) => content.content.toLowerCase().includes(searchLower)); return titleMatch || markdownMatch || contentsMatch; }); // Apply limit if specified const finalResults = listOptions.limit ? results.slice(0, listOptions.limit) : results; // Cache the search results cache_1.searchCache.set(cacheKey, finalResults); return finalResults; } buildQueryParams(options) { const params = new URLSearchParams(); if (options.timezone) params.append('timezone', options.timezone); if (options.direction) params.append('direction', options.direction); if (options.includeMarkdown !== undefined) { params.append('includeMarkdown', String(options.includeMarkdown)); } if (options.includeHeadings !== undefined) { params.append('includeHeadings', String(options.includeHeadings)); } return params; } async fetchAllLifelogs(endpoint, params, limit) { const results = []; let nextCursor; const pageSize = 100; // API max page size do { if (nextCursor) { params.set('cursor', nextCursor); } params.set('limit', String(Math.min(pageSize, (limit || pageSize) - results.length))); const queryString = params.toString(); const fullEndpoint = `${endpoint}${queryString ? `?${queryString}` : ''}`; const response = await this.makeRequest(fullEndpoint); if (response.error) { throw new LimitlessAPIError(response.error.message, undefined, response.error.code); } // Handle both array response and object with lifelogs array if (Array.isArray(response.data)) { results.push(...response.data); } else if (response.data && 'lifelogs' in response.data) { results.push(...response.data.lifelogs); } else { throw new Error('Unexpected API response format'); } nextCursor = response.pagination?.nextCursor; // Stop if we've reached the limit or there's no more data if ((limit && results.length >= limit) || !response.pagination?.hasMore) { break; } } while (nextCursor); return limit ? results.slice(0, limit) : results; } } exports.LimitlessClient = LimitlessClient; //# sourceMappingURL=limitless-client.js.map