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
JavaScript
// 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