Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
379 lines
10 KiB
JavaScript
379 lines
10 KiB
JavaScript
/**
|
|
* Claude Service - Chat interface with Claude AI and MCP Integration
|
|
*
|
|
* Provides:
|
|
* - Direct Claude chat
|
|
* - MCP tool execution
|
|
* - Project context integration
|
|
* - Integration management (Figma, Jira, Confluence)
|
|
*/
|
|
|
|
import api from '../core/api.js';
|
|
import logger from '../core/logger.js';
|
|
|
|
class ClaudeService {
|
|
constructor() {
|
|
this.conversationHistory = [];
|
|
this.maxHistory = 50;
|
|
this.currentProjectId = null;
|
|
this.userId = 1; // Default user ID (TODO: implement proper auth)
|
|
this.mcpTools = null;
|
|
this.mcpStatus = null;
|
|
|
|
// Load persisted chat history
|
|
this.loadHistory();
|
|
}
|
|
|
|
/**
|
|
* Get storage key for current user
|
|
* @private
|
|
*/
|
|
_getStorageKey() {
|
|
return `ds-chat-history-user-${this.userId}`;
|
|
}
|
|
|
|
/**
|
|
* Load chat history from localStorage
|
|
* @private
|
|
*/
|
|
loadHistory() {
|
|
const key = this._getStorageKey();
|
|
try {
|
|
const stored = localStorage.getItem(key);
|
|
if (stored) {
|
|
this.conversationHistory = JSON.parse(stored);
|
|
logger.info('Claude', `Loaded ${this.conversationHistory.length} messages from history`);
|
|
}
|
|
} catch (error) {
|
|
logger.warn('Claude', 'Failed to load chat history, resetting', error);
|
|
localStorage.removeItem(key);
|
|
this.conversationHistory = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save chat history to localStorage with quota management
|
|
* @private
|
|
*/
|
|
saveHistory() {
|
|
const key = this._getStorageKey();
|
|
try {
|
|
// Keep only last 50 messages to respect localStorage limits
|
|
const historyToSave = this.conversationHistory.slice(-50);
|
|
localStorage.setItem(key, JSON.stringify(historyToSave));
|
|
} catch (error) {
|
|
logger.warn('Claude', 'Failed to save chat history', error);
|
|
if (error.name === 'QuotaExceededError') {
|
|
// Emergency trim: cut in half and retry
|
|
this.conversationHistory = this.conversationHistory.slice(-25);
|
|
try {
|
|
localStorage.setItem(key, JSON.stringify(this.conversationHistory));
|
|
logger.info('Claude', 'History trimmed to 25 messages due to quota');
|
|
} catch (retryError) {
|
|
logger.error('Claude', 'Failed to save history even after trim', retryError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set current project context
|
|
*/
|
|
setProject(projectId) {
|
|
this.currentProjectId = projectId;
|
|
logger.info('Claude', 'Project context set', { projectId });
|
|
}
|
|
|
|
/**
|
|
* Send message to Claude with MCP context
|
|
*/
|
|
async chat(message, context = {}) {
|
|
logger.info('Claude', 'Sending message', { message: message.substring(0, 50) + '...' });
|
|
|
|
// Add project context if available
|
|
if (this.currentProjectId && !context.projectId) {
|
|
context.projectId = this.currentProjectId;
|
|
}
|
|
|
|
// Add to history
|
|
this.conversationHistory.push({
|
|
role: 'user',
|
|
content: message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
try {
|
|
// Call backend Claude endpoint with MCP context
|
|
const response = await api.post('/claude/chat', {
|
|
message,
|
|
context: {
|
|
...context,
|
|
mcp_enabled: true,
|
|
available_tools: this.mcpTools ? Object.keys(this.mcpTools).flatMap(k => this.mcpTools[k].map(t => t.name)) : []
|
|
},
|
|
history: this.conversationHistory.slice(-10) // Send last 10 messages
|
|
});
|
|
|
|
// Add response to history
|
|
this.conversationHistory.push({
|
|
role: 'assistant',
|
|
content: response.response,
|
|
timestamp: new Date().toISOString(),
|
|
tools_used: response.tools_used || []
|
|
});
|
|
|
|
// Keep history manageable
|
|
if (this.conversationHistory.length > this.maxHistory) {
|
|
this.conversationHistory = this.conversationHistory.slice(-this.maxHistory);
|
|
}
|
|
|
|
// Persist updated history
|
|
this.saveHistory();
|
|
|
|
logger.info('Claude', 'Received response', { length: response.response.length });
|
|
|
|
return response.response;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Chat failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get conversation history
|
|
*/
|
|
getHistory() {
|
|
return this.conversationHistory;
|
|
}
|
|
|
|
/**
|
|
* Clear history
|
|
*/
|
|
clearHistory() {
|
|
this.conversationHistory = [];
|
|
this.saveHistory(); // Persist empty state
|
|
logger.info('Claude', 'Conversation history cleared');
|
|
}
|
|
|
|
/**
|
|
* Export conversation
|
|
*/
|
|
exportConversation() {
|
|
const data = JSON.stringify(this.conversationHistory, null, 2);
|
|
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(data);
|
|
const exportFileDefaultName = `claude-conversation-${Date.now()}.json`;
|
|
|
|
const linkElement = document.createElement('a');
|
|
linkElement.setAttribute('href', dataUri);
|
|
linkElement.setAttribute('download', exportFileDefaultName);
|
|
linkElement.click();
|
|
}
|
|
|
|
// === MCP Integration Methods ===
|
|
|
|
/**
|
|
* Get MCP server status
|
|
*/
|
|
async getMcpStatus() {
|
|
try {
|
|
const status = await api.get('/mcp/status');
|
|
this.mcpStatus = status;
|
|
logger.info('Claude', 'MCP status fetched', status);
|
|
return status;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Failed to fetch MCP status', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all available MCP tools
|
|
*/
|
|
async getMcpTools() {
|
|
try {
|
|
const tools = await api.get('/mcp/tools');
|
|
this.mcpTools = tools.tools;
|
|
logger.info('Claude', 'MCP tools fetched', { count: tools.total_count });
|
|
return tools;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Failed to fetch MCP tools', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute an MCP tool directly
|
|
*/
|
|
async executeMcpTool(toolName, args = {}) {
|
|
if (this.currentProjectId && !args.project_id) {
|
|
args.project_id = this.currentProjectId;
|
|
}
|
|
|
|
try {
|
|
logger.info('Claude', 'Executing MCP tool', { toolName, args });
|
|
const result = await api.post(`/mcp/tools/${toolName}/execute?user_id=${this.userId}`, args);
|
|
logger.info('Claude', 'MCP tool executed', { toolName, success: !result.error });
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('Claude', 'MCP tool execution failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// === Integration Management ===
|
|
|
|
/**
|
|
* Get all integration types and their health status
|
|
*/
|
|
async getIntegrations() {
|
|
try {
|
|
const integrations = await api.get('/mcp/integrations');
|
|
logger.info('Claude', 'Integrations fetched', { count: integrations.integrations.length });
|
|
return integrations.integrations;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Failed to fetch integrations', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get project-specific integrations
|
|
*/
|
|
async getProjectIntegrations(projectId = null) {
|
|
const pid = projectId || this.currentProjectId;
|
|
if (!pid) {
|
|
throw new Error('No project ID specified');
|
|
}
|
|
|
|
try {
|
|
const integrations = await api.get(`/projects/${pid}/integrations?user_id=${this.userId}`);
|
|
return integrations.integrations;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Failed to fetch project integrations', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure an integration for a project
|
|
*/
|
|
async configureIntegration(integrationType, config, projectId = null) {
|
|
const pid = projectId || this.currentProjectId;
|
|
if (!pid) {
|
|
throw new Error('No project ID specified');
|
|
}
|
|
|
|
try {
|
|
const result = await api.post(`/projects/${pid}/integrations?user_id=${this.userId}`, {
|
|
integration_type: integrationType,
|
|
config,
|
|
enabled: true
|
|
});
|
|
logger.info('Claude', 'Integration configured', { integrationType, projectId: pid });
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Failed to configure integration', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an integration
|
|
*/
|
|
async updateIntegration(integrationType, updates, projectId = null) {
|
|
const pid = projectId || this.currentProjectId;
|
|
if (!pid) {
|
|
throw new Error('No project ID specified');
|
|
}
|
|
|
|
try {
|
|
const result = await api.put(
|
|
`/projects/${pid}/integrations/${integrationType}?user_id=${this.userId}`,
|
|
updates
|
|
);
|
|
logger.info('Claude', 'Integration updated', { integrationType, projectId: pid });
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Failed to update integration', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an integration
|
|
*/
|
|
async deleteIntegration(integrationType, projectId = null) {
|
|
const pid = projectId || this.currentProjectId;
|
|
if (!pid) {
|
|
throw new Error('No project ID specified');
|
|
}
|
|
|
|
try {
|
|
const result = await api.delete(
|
|
`/projects/${pid}/integrations/${integrationType}?user_id=${this.userId}`
|
|
);
|
|
logger.info('Claude', 'Integration deleted', { integrationType, projectId: pid });
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('Claude', 'Failed to delete integration', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle integration enabled/disabled
|
|
*/
|
|
async toggleIntegration(integrationType, enabled, projectId = null) {
|
|
return this.updateIntegration(integrationType, { enabled }, projectId);
|
|
}
|
|
|
|
// === Project Context Tools ===
|
|
|
|
/**
|
|
* Get project summary via MCP
|
|
*/
|
|
async getProjectSummary(projectId = null) {
|
|
return this.executeMcpTool('dss_get_project_summary', {
|
|
project_id: projectId || this.currentProjectId,
|
|
include_components: false
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get project health via MCP
|
|
*/
|
|
async getProjectHealth(projectId = null) {
|
|
return this.executeMcpTool('dss_get_project_health', {
|
|
project_id: projectId || this.currentProjectId
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List project components via MCP
|
|
*/
|
|
async listComponents(projectId = null, filter = null) {
|
|
const args = { project_id: projectId || this.currentProjectId };
|
|
if (filter) {
|
|
args.filter_name = filter;
|
|
}
|
|
return this.executeMcpTool('dss_list_components', args);
|
|
}
|
|
|
|
/**
|
|
* Get design tokens via MCP
|
|
*/
|
|
async getDesignTokens(projectId = null, category = null) {
|
|
const args = { project_id: projectId || this.currentProjectId };
|
|
if (category) {
|
|
args.token_category = category;
|
|
}
|
|
return this.executeMcpTool('dss_get_design_tokens', args);
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
const claudeService = new ClaudeService();
|
|
|
|
export default claudeService;
|
|
export { ClaudeService };
|