/**
* API Service for Semantic Memory Workbench
* Handles communication with MCP HTTP server endpoints
*/
export class ApiService {
constructor(baseUrl = '/api') {
// In test environment, use absolute URL to avoid fetch parsing errors
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {
this.baseUrl = baseUrl.startsWith('http') ? baseUrl : `http://localhost:3000${baseUrl}`;
} else {
this.baseUrl = baseUrl;
}
// Session management for MCP server
this.sessionId = null;
this.defaultHeaders = {
'Content-Type': 'application/json'
};
}
/**
* Make HTTP request with error handling and session management
*/
async makeRequest(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
// Include session ID in headers if available
const headers = { ...this.defaultHeaders };
if (this.sessionId) {
headers['mcp-session-id'] = this.sessionId;
}
const config = {
headers: { ...headers, ...options.headers },
...options
};
try {
const response = await fetch(url, config);
// Extract and store session ID from response for future requests
const responseSessionId = response.headers.get('mcp-session-id');
if (responseSessionId && responseSessionId !== this.sessionId) {
console.log(`🔗 [WORKBENCH] Session ID updated: ${this.sessionId} → ${responseSessionId}`);
this.sessionId = responseSessionId;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error(`API Error [${endpoint}]:`, error);
throw error;
}
}
// ===== SIMPLE VERBS API METHODS =====
/**
* TELL - Store knowledge and concepts
* @param {Object} params - Tell parameters
* @param {string} params.content - Content to store
* @param {string} params.type - Type: 'concept' | 'interaction' | 'document'
* @param {boolean} params.lazy - Whether to store without immediate processing
* @param {Object} params.metadata - Optional metadata
* @returns {Promise<Object>} Tell result
*/
async tell({ content, type = 'interaction', lazy = false, metadata = {} }) {
return this.makeRequest('/tell', {
method: 'POST',
body: JSON.stringify({ content, type, lazy, metadata })
});
}
/**
* UPLOAD DOCUMENT - Upload and process document file
* @param {Object} params - Upload parameters
* @param {string} params.fileUrl - Data URL of the file
* @param {string} params.filename - Original filename
* @param {string} params.mediaType - MIME type of the file
* @param {string} params.documentType - Document type (pdf, text, markdown)
* @param {Object} params.metadata - Additional metadata
*/
async uploadDocument({ fileUrl, filename, mediaType, documentType, metadata = {} }) {
return this.makeRequest('/upload-document', {
method: 'POST',
body: JSON.stringify({
fileUrl,
filename,
mediaType,
documentType,
metadata
})
});
}
/**
* ASK - Query stored knowledge
* @param {Object} params - Ask parameters
* @param {string} params.question - Question to ask
* @param {string} params.mode - Mode: 'basic' | 'standard' | 'comprehensive'
* @param {boolean} params.useContext - Whether to use context
* @param {boolean} params.useHyDE - Whether to use HyDE enhancement
* @param {boolean} params.useWikipedia - Whether to use Wikipedia enhancement
* @param {boolean} params.useWikidata - Whether to use Wikidata enhancement
* @returns {Promise<Object>} Ask result with answer and related content
*/
async ask({ question, mode = 'standard', useContext = true, useHyDE = false, useWikipedia = false, useWikidata = false, threshold }) {
return this.makeRequest('/ask', {
method: 'POST',
body: JSON.stringify({
question,
mode,
useContext,
useHyDE,
useWikipedia,
useWikidata,
threshold
})
});
}
/**
* CHAT - Interactive chat with slash command support and LLM inference
* @param {Object} params - Chat parameters
* @param {string} params.message - User message or command
* @param {Object} params.context - Optional conversation context
* @returns {Promise<Object>} Chat result with response content and routing information
*/
async chat({ message, context = {} }) {
return this.makeRequest('/chat', {
method: 'POST',
body: JSON.stringify({ message, context })
});
}
/**
* ENHANCED CHAT - Enhanced query with HyDE, Wikipedia, and Wikidata
* @param {Object} params - Enhanced chat parameters
* @param {string} params.query - Query to search with enhancements
* @param {boolean} params.useHyDE - Whether to use HyDE enhancement
* @param {boolean} params.useWikipedia - Whether to use Wikipedia enhancement
* @param {boolean} params.useWikidata - Whether to use Wikidata enhancement
* @returns {Promise<Object>} Enhanced chat result
*/
async enhancedChat({ query, useHyDE = false, useWikipedia = false, useWikidata = false }) {
return this.makeRequest('/chat/enhanced', {
method: 'POST',
body: JSON.stringify({ query, useHyDE, useWikipedia, useWikidata })
});
}
/**
* AUGMENT - Run operations on content
* @param {Object} params - Augment parameters
* @param {string} params.target - Target content to analyze
* @param {string} params.operation - Operation: 'auto' | 'concepts' | 'attributes' | 'relationships'
* @param {Object} params.options - Additional options
* @returns {Promise<Object>} Augment result with extracted concepts/attributes
*/
async augment({ target, operation = 'auto', options = {} }) {
return this.makeRequest('/augment', {
method: 'POST',
body: JSON.stringify({ target, operation, options })
});
}
/**
* ZOOM - Set abstraction level for navigation
* @param {Object} params - Zoom parameters
* @param {string} params.level - Abstraction level: 'entity' | 'unit' | 'text' | 'community' | 'corpus'
* @param {string} params.query - Optional query to update
* @returns {Promise<Object>} Zoom result with updated state
*/
async zoom({ level = 'entity', query }) {
return this.makeRequest('/zoom', {
method: 'POST',
body: JSON.stringify({ level, query })
});
}
/**
* PAN - Set domain filters for navigation
* @param {Object} params - Pan parameters
* @param {string[]} params.domains - Subject domains to filter by
* @param {string[]} params.keywords - Keywords to filter by
* @param {string[]} params.entities - Entity names to filter by
* @param {Object} params.temporal - Temporal filtering parameters
* @param {string} params.query - Optional query to update
* @returns {Promise<Object>} Pan result with updated filters
*/
async pan({ domains, keywords, entities, temporal, query }) {
const panParams = {};
if (domains) panParams.domains = Array.isArray(domains) ? domains : domains.split(',').map(s => s.trim());
if (keywords) panParams.keywords = Array.isArray(keywords) ? keywords : keywords.split(',').map(s => s.trim());
if (entities) panParams.entities = Array.isArray(entities) ? entities : entities.split(',').map(s => s.trim());
if (temporal) panParams.temporal = temporal;
if (query) panParams.query = query;
return this.makeRequest('/pan', {
method: 'POST',
body: JSON.stringify(panParams)
});
}
/**
* TILT - Set view filter/representation style
* @param {Object} params - Tilt parameters
* @param {string} params.style - Representation style: 'keywords' | 'embedding' | 'graph' | 'temporal'
* @param {string} params.query - Optional query to update
* @returns {Promise<Object>} Tilt result with updated view
*/
async tilt({ style = 'keywords', query }) {
return this.makeRequest('/tilt', {
method: 'POST',
body: JSON.stringify({ style, query })
});
}
// ===== STATE AND STATUS METHODS =====
/**
* Get current ZPT navigation state
* @returns {Promise<Object>} Current state object
*/
async getState() {
return this.makeRequest('/state', {
method: 'GET'
});
}
/**
* Check server health status
* @returns {Promise<Object>} Health status
*/
async getHealth() {
return this.makeRequest('/health', {
method: 'GET'
});
}
// ===== INSPECT METHODS =====
/**
* INSPECT SESSION - Get session state and cache information
* @param {boolean} details - Include detailed information
* @returns {Promise<Object>} Session inspection data
*/
async inspectSession(details = true) {
return this.makeRequest('/inspect', {
method: 'POST',
body: JSON.stringify({ what: 'session', details })
});
}
/**
* INSPECT CONCEPTS - Get concepts and embeddings information
* @param {boolean} details - Include detailed information
* @returns {Promise<Object>} Concepts inspection data
*/
async inspectConcepts(details = true) {
return this.makeRequest('/inspect', {
method: 'POST',
body: JSON.stringify({ what: 'concepts', details })
});
}
/**
* INSPECT ALL DATA - Get complete system state information
* @param {boolean} details - Include detailed information
* @returns {Promise<Object>} Complete inspection data
*/
async inspectAllData(details = true) {
return this.makeRequest('/inspect', {
method: 'POST',
body: JSON.stringify({ what: 'all', details })
});
}
// ===== UTILITY METHODS =====
/**
* Test connection to API server
* @returns {Promise<boolean>} True if connected
*/
async testConnection() {
try {
await this.getHealth();
return true;
} catch (error) {
console.warn('API connection test failed:', error.message);
return false;
}
}
/**
* Get formatted error message from API response
* @param {Error} error - Error object
* @returns {string} User-friendly error message
*/
getErrorMessage(error) {
if (error.message) {
// Extract meaningful error from common patterns
if (error.message.includes('fetch')) {
return 'Unable to connect to server. Please check if the service is running.';
}
if (error.message.includes('404')) {
return 'API endpoint not found. Please check server configuration.';
}
if (error.message.includes('500')) {
return 'Server error occurred. Please try again or check server logs.';
}
return error.message;
}
return 'Unknown error occurred';
}
/**
* Create request options with timeout
* @param {Object} options - Base options
* @param {number} timeoutMs - Timeout in milliseconds
* @returns {Object} Options with timeout
*/
withTimeout(options = {}, timeoutMs = 30000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
return {
...options,
signal: controller.signal,
// Clean up timeout if request completes
finally: () => clearTimeout(timeoutId)
};
}
// ===== MEMORY MANAGEMENT METHODS =====
/**
* REMEMBER - Store content in specific memory domain with importance weighting
* @param {Object} params - Remember parameters
* @param {string} params.content - Content to remember
* @param {string} params.domain - Memory domain: 'user' | 'project' | 'session' | 'instruction'
* @param {string} params.domainId - Domain identifier (optional)
* @param {number} params.importance - Importance weighting (0-1)
* @param {Object} params.metadata - Additional metadata including tags and category
* @returns {Promise<Object>} Remember result
*/
async remember({ content, domain = 'user', domainId, importance = 0.5, metadata = {} }) {
return this.makeRequest('/remember', {
method: 'POST',
body: JSON.stringify({ content, domain, domainId, importance, metadata })
});
}
/**
* FORGET - Fade memory visibility using navigation rather than deletion
* @param {Object} params - Forget parameters
* @param {string} params.target - Target memory identifier or domain
* @param {string} params.strategy - Forgetting strategy: 'fade' | 'context_switch' | 'temporal_decay'
* @param {number} params.fadeFactor - Fade factor for visibility reduction (0-1)
* @returns {Promise<Object>} Forget result
*/
async forget({ target, strategy = 'fade', fadeFactor = 0.1 }) {
return this.makeRequest('/forget', {
method: 'POST',
body: JSON.stringify({ target, strategy, fadeFactor })
});
}
/**
* RECALL - Retrieve memories based on query and domain filters with relevance scoring
* @param {Object} params - Recall parameters
* @param {string} params.query - Search query for memories
* @param {Array<string>} params.domains - Domain filters
* @param {Object} params.timeRange - Temporal filters {start, end}
* @param {number} params.relevanceThreshold - Minimum relevance score (0-1)
* @param {number} params.maxResults - Maximum results to return (1-100)
* @returns {Promise<Object>} Recall result with memories
*/
async recall({ query, domains, timeRange, relevanceThreshold = 0.1, maxResults = 10 }) {
return this.makeRequest('/recall', {
method: 'POST',
body: JSON.stringify({ query, domains, timeRange, relevanceThreshold, maxResults })
});
}
/**
* PROJECT_CONTEXT - Manage project-specific memory domains
* @param {Object} params - Project context parameters
* @param {string} params.projectId - Project identifier
* @param {string} params.action - Project action: 'create' | 'switch' | 'list' | 'archive'
* @param {Object} params.metadata - Project metadata including name, description, and technologies
* @returns {Promise<Object>} Project context result
*/
async project_context({ projectId, action = 'switch', metadata = {} }) {
return this.makeRequest('/project_context', {
method: 'POST',
body: JSON.stringify({ projectId, action, metadata })
});
}
/**
* FADE_MEMORY - Gradually reduce memory visibility for smooth context transitions
* @param {Object} params - Fade memory parameters
* @param {string} params.domain - Domain to fade
* @param {number} params.fadeFactor - Fade factor (0-1)
* @param {string} params.transition - Transition type: 'smooth' | 'immediate'
* @param {boolean} params.preserveInstructions - Whether to preserve instruction memories
* @returns {Promise<Object>} Fade memory result
*/
async fade_memory({ domain, fadeFactor = 0.1, transition = 'smooth', preserveInstructions = true }) {
return this.makeRequest('/fade_memory', {
method: 'POST',
body: JSON.stringify({ domain, fadeFactor, transition, preserveInstructions })
});
}
// ===== ZPT NAVIGATION METHODS =====
/**
* ZPT NAVIGATE - Execute navigation with zoom/pan/tilt parameters
* @param {Object} params - Navigation parameters
* @param {string} params.query - Navigation query
* @param {string} params.zoom - Zoom level (entity, unit, text, community, corpus)
* @param {Object} params.pan - Pan filters {domains, keywords}
* @param {string} params.tilt - Tilt style (keywords, embedding, graph, temporal)
* @returns {Promise<Object>} Navigation results
*/
async zptNavigate({ query, zoom = 'entity', pan = {}, tilt = 'keywords' }) {
return this.makeRequest('/zpt/navigate', {
method: 'POST',
body: JSON.stringify({
query,
zoom,
pan,
tilt
})
});
}
/**
* Batch multiple API calls
* @param {Array} requests - Array of {method, params} objects
* @returns {Promise<Array>} Array of results
*/
async batch(requests) {
const promises = requests.map(({ method, params }) => {
if (typeof this[method] === 'function') {
return this[method](params).catch(error => ({ error: error.message }));
}
return Promise.resolve({ error: `Unknown method: ${method}` });
});
return Promise.all(promises);
}
}
// Create and export singleton instance
export const apiService = new ApiService();
// Export class for testing or custom instances
export default ApiService;