limitless-ai-mcp-server
Version:
MCP server for integrating Limitless AI Pendant recordings with AI assistants
238 lines • 10.3 kB
JavaScript
;
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