import { logger } from './logger.js';
// Error classes
export class HeliosError extends Error {
constructor(message, code = 'UNKNOWN_ERROR', statusCode = 500, originalError) {
super(message);
this.message = message;
this.code = code;
this.statusCode = statusCode;
this.originalError = originalError;
this.name = 'HeliosError';
}
}
export class NotFoundError extends HeliosError {
constructor(resource, id) {
super(`${resource}${id ? ` with ID ${id}` : ''} not found`, 'NOT_FOUND', 404);
}
}
export class UnauthorizedError extends HeliosError {
constructor(message = 'Unauthorized') {
super(message, 'UNAUTHORIZED', 401);
}
}
export class ValidationError extends HeliosError {
constructor(message, field) {
super(message, 'VALIDATION_ERROR', 400);
if (field) {
this.message = `${field}: ${message}`;
}
}
}
export class ApiClient {
constructor() {
this.currentUserId = null;
this.currentTenantId = null;
const baseUrl = process.env.HELIOS_API_URL;
const apiKey = process.env.HELIOS_API_KEY;
// Provide detailed error information
if (!baseUrl) {
logger.error('HELIOS_API_URL environment variable is not set');
throw new Error('Missing HELIOS_API_URL environment variable. This should be set in your MCP client configuration (e.g., Claude Desktop config).');
}
if (!apiKey) {
logger.error('HELIOS_API_KEY environment variable is not set');
throw new Error('Missing HELIOS_API_KEY environment variable. This should be set in your MCP client configuration (e.g., Claude Desktop config).');
}
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.apiKey = apiKey;
logger.info('Helios API client initialized', {
baseUrl: this.baseUrl,
keyPrefix: this.apiKey.substring(0, 16) + '...', // Log partial key for debugging
keyLength: this.apiKey.length,
envBaseUrl: process.env.HELIOS_API_URL,
envKeyLength: process.env.HELIOS_API_KEY?.length
});
}
/**
* Make authenticated API request
*/
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'X-MCP-Client': 'helios9-mcp-server',
...options.headers,
},
};
try {
const headers = config.headers;
logger.info(`API Request: ${config.method || 'GET'} ${url}`, {
hasAuth: !!headers?.['Authorization'],
authPrefix: headers?.['Authorization']?.substring(0, 20) + '...'
});
const response = await fetch(url, config);
logger.info(`API Response: ${response.status} ${response.statusText}`, {
url,
ok: response.ok
});
if (!response.ok) {
const errorText = await response.text();
let errorData;
try {
errorData = JSON.parse(errorText);
}
catch {
errorData = { message: errorText };
}
logger.error(`API Error: ${response.status} ${response.statusText}`, {
url,
error: errorData
});
switch (response.status) {
case 401:
throw new UnauthorizedError(errorData.message || 'Invalid API key');
case 404:
throw new NotFoundError('Resource');
case 400:
throw new ValidationError(errorData.message || 'Validation failed');
default:
throw new HeliosError(errorData.message || `API request failed: ${response.status}`, 'API_ERROR', response.status, errorData);
}
}
const data = await response.json();
return data;
}
catch (error) {
if (error instanceof HeliosError) {
throw error;
}
logger.error(`API Request failed: ${url}`, error);
throw new HeliosError(`Network error: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NETWORK_ERROR', 500, error);
}
}
/**
* Authenticate with API key (validates the key and gets user info)
*/
async authenticate() {
try {
const response = await this.request('/api/auth/validate', {
method: 'POST',
});
this.currentUserId = response.user.id;
this.currentTenantId = response.user.tenant_id || null;
logger.info(`API authenticated for user: ${response.user.email}, tenant: ${response.user.tenant_id || 'none'}`);
return response.user;
}
catch (error) {
logger.error('API authentication failed:', error);
throw error instanceof HeliosError ? error : new UnauthorizedError();
}
}
/**
* Get current user ID
*/
getCurrentUserId() {
if (!this.currentUserId) {
throw new UnauthorizedError('No authenticated user');
}
return this.currentUserId;
}
/**
* Get current tenant ID
*/
getCurrentTenantId() {
return this.currentTenantId;
}
/**
* Project operations
*/
async getProjects(filter, pagination, sort) {
const params = new URLSearchParams();
if (filter?.status)
params.append('status', filter.status);
if (filter?.search)
params.append('search', filter.search);
if (filter?.created_after)
params.append('created_after', filter.created_after);
if (filter?.created_before)
params.append('created_before', filter.created_before);
if (pagination?.limit)
params.append('limit', pagination.limit.toString());
if (pagination?.offset)
params.append('offset', pagination.offset.toString());
if (sort?.field)
params.append('sort_field', sort.field);
if (sort?.order)
params.append('sort_order', sort.order);
const queryString = params.toString();
const endpoint = `/api/mcp/projects${queryString ? `?${queryString}` : ''}`;
const response = await this.request(endpoint);
return response.projects;
}
async getProject(projectId) {
const response = await this.request(`/api/mcp/projects/${projectId}`);
return response.project;
}
async createProject(projectData) {
const response = await this.request('/api/mcp/projects', {
method: 'POST',
body: JSON.stringify(projectData),
});
logger.info(`Project created: ${response.project.name} (${response.project.id})`);
return response.project;
}
async updateProject(projectId, updates) {
const response = await this.request(`/api/mcp/projects/${projectId}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
return response.project;
}
/**
* Task operations
*/
async getTasks(filter, pagination, sort) {
const params = new URLSearchParams();
if (filter?.project_id)
params.append('project_id', filter.project_id);
if (filter?.status)
params.append('status', filter.status);
if (filter?.assignee_id)
params.append('assignee_id', filter.assignee_id);
if (filter?.search)
params.append('search', filter.search);
if (pagination?.limit)
params.append('limit', pagination.limit.toString());
if (pagination?.offset)
params.append('offset', pagination.offset.toString());
if (sort?.field)
params.append('sort_field', sort.field);
if (sort?.order)
params.append('sort_order', sort.order);
const queryString = params.toString();
const endpoint = `/api/mcp/tasks${queryString ? `?${queryString}` : ''}`;
const response = await this.request(endpoint);
return response.tasks;
}
async getTask(taskId) {
const response = await this.request(`/api/mcp/tasks/${taskId}`);
return response.task;
}
async createTask(taskData) {
const response = await this.request('/api/mcp/tasks', {
method: 'POST',
body: JSON.stringify(taskData),
});
return response.task;
}
async updateTask(taskId, updates) {
const response = await this.request(`/api/mcp/tasks/${taskId}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
return response.task;
}
/**
* Document operations
*/
async getDocuments(filter, pagination, sort) {
const params = new URLSearchParams();
if (filter?.project_id)
params.append('project_id', filter.project_id);
if (filter?.type)
params.append('document_type', filter.type);
if (filter?.search)
params.append('search', filter.search);
if (pagination?.limit)
params.append('limit', pagination.limit.toString());
if (pagination?.offset)
params.append('offset', pagination.offset.toString());
if (sort?.field)
params.append('sort_field', sort.field);
if (sort?.order)
params.append('sort_order', sort.order);
const queryString = params.toString();
const endpoint = `/api/mcp/documents${queryString ? `?${queryString}` : ''}`;
const response = await this.request(endpoint);
return response.documents;
}
async getDocument(documentId) {
const response = await this.request(`/api/mcp/documents/${documentId}`);
return response.document;
}
async createDocument(documentData) {
const response = await this.request('/api/mcp/documents', {
method: 'POST',
body: JSON.stringify(documentData),
});
return response.document;
}
async updateDocument(documentId, updates) {
const response = await this.request(`/api/mcp/documents/${documentId}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
return response.document;
}
/**
* Get comprehensive project context for AI agents
*/
async getProjectContext(projectId) {
const response = await this.request(`/api/mcp/projects/${projectId}/context`);
return response.context;
}
/**
* Initiative operations
*/
async getInitiatives(filter, pagination, sort) {
const params = new URLSearchParams();
if (filter?.project_id)
params.append('project_id', filter.project_id);
if (filter?.status)
params.append('status', filter.status);
if (filter?.priority)
params.append('priority', filter.priority);
if (filter?.search)
params.append('search', filter.search);
if (pagination?.limit)
params.append('limit', pagination.limit.toString());
if (pagination?.offset)
params.append('offset', pagination.offset.toString());
if (sort?.field)
params.append('sort_field', sort.field);
if (sort?.order)
params.append('sort_order', sort.order);
const queryString = params.toString();
const endpoint = `/api/mcp/initiatives${queryString ? `?${queryString}` : ''}`;
const response = await this.request(endpoint);
return response.initiatives;
}
async getInitiative(initiativeId) {
const response = await this.request(`/api/mcp/initiatives/${initiativeId}`);
return response.initiative;
}
async createInitiative(initiativeData) {
const response = await this.request('/api/mcp/initiatives', {
method: 'POST',
body: JSON.stringify(initiativeData),
});
logger.info(`Initiative created: ${response.initiative.name} (${response.initiative.id})`);
return response.initiative;
}
async updateInitiative(initiativeId, updates) {
const response = await this.request(`/api/mcp/initiatives/${initiativeId}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
return response.initiative;
}
async getInitiativeContext(initiativeId) {
const response = await this.request(`/api/mcp/initiatives/${initiativeId}/context`);
return response.context;
}
async getInitiativeInsights(initiativeId) {
const response = await this.request(`/api/mcp/initiatives/${initiativeId}/insights`);
return response.insights;
}
async searchWorkspace(query, filters, limit) {
const response = await this.request('/api/mcp/search', {
method: 'POST',
body: JSON.stringify({ query, filters, limit }),
});
return response;
}
async getEnhancedProjectContext(projectId) {
const response = await this.request(`/api/mcp/projects/${projectId}/context-enhanced`);
return response.context;
}
async getWorkspaceContext() {
const response = await this.request('/api/mcp/workspace/context');
return response.context;
}
/**
* Additional methods for MCP compatibility
*/
async updateTasksByProject(projectId, updates) {
await this.request(`/api/mcp/projects/${projectId}/tasks`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
}
// Placeholder methods for missing functionality
async createTaskDependency(dependency) {
logger.warn('Task dependencies not yet implemented in API');
return { id: 'placeholder', ...dependency };
}
async getTaskDependencies(taskId) {
logger.warn('Task dependencies not yet implemented in API');
return [];
}
async getProjectDependencies(projectId) {
logger.warn('Project dependencies not yet implemented in API');
return [];
}
async createWorkflowRule(rule) {
logger.warn('Workflow rules not yet implemented in API');
return { id: 'placeholder', ...rule };
}
async getWorkflowRules(filter) {
logger.warn('Workflow rules not yet implemented in API');
return [];
}
async getWorkflowRule(ruleId) {
logger.warn('Workflow rules not yet implemented in API');
return null;
}
async logWorkflowExecution(execution) {
logger.warn('Workflow execution logging not yet implemented in API');
return { id: 'placeholder', ...execution };
}
async createTriggerAutomation(automation) {
logger.warn('Trigger automations not yet implemented in API');
return { id: 'placeholder', ...automation };
}
async getWorkflowExecutions(filter) {
logger.warn('Workflow executions not yet implemented in API');
return [];
}
async createDocumentCollaboration(collaboration) {
logger.warn('Document collaborations not yet implemented in API');
return { id: 'placeholder', ...collaboration };
}
async getDocumentCollaborations(documentId) {
logger.warn('Document collaborations not yet implemented in API');
return [];
}
}
// Export singleton instance (lazy initialization)
let _apiClient = null;
export function getApiClient() {
if (!_apiClient) {
_apiClient = new ApiClient();
}
return _apiClient;
}
// For backward compatibility
export const apiClient = new Proxy({}, {
get(target, prop, receiver) {
return Reflect.get(getApiClient(), prop, receiver);
}
});
// Maintain backward compatibility by aliasing the service
export const supabaseService = apiClient;