UNPKG

cntx-ui

Version:

File context management tool with web UI and MCP server for AI development workflows - bundle project files for LLM consumption

979 lines (808 loc) 33.5 kB
/** * API Router for cntx-ui * Handles all HTTP API endpoints and request routing */ import { parse } from 'url'; export default class APIRouter { constructor(configManager, bundleManager, fileSystemManager, semanticAnalysisManager, vectorStore, activityManager) { this.configManager = configManager; this.bundleManager = bundleManager; this.fileSystemManager = fileSystemManager; this.semanticAnalysisManager = semanticAnalysisManager; this.vectorStore = vectorStore; this.activityManager = activityManager; } async handleRequest(req, res, url) { const method = req.method; const pathname = url.pathname; try { // Route to appropriate handler if (pathname === '/api/bundles' && method === 'GET') { return await this.handleGetBundles(req, res, url); } if (pathname === '/api/bundles' && method === 'POST') { return await this.handlePostBundles(req, res); } if (pathname.startsWith('/api/bundles/') && method === 'GET') { const bundleName = pathname.split('/')[3]; return await this.handleGetBundle(req, res, bundleName); } if (pathname.startsWith('/api/regenerate/') && (method === 'GET' || method === 'POST')) { const bundleName = pathname.split('/')[3]; return await this.handleRegenerateBundle(req, res, bundleName); } if (pathname === '/api/bundles-from-chunk' && method === 'POST') { return await this.handleCreateBundlesFromChunk(req, res); } if (pathname === '/api/bundle-visibility-stats' && method === 'GET') { return await this.handleBundleVisibilityStats(req, res); } if (pathname.startsWith('/api/bundle-categories/') && method === 'GET') { const bundleName = pathname.split('/')[3]; return await this.handleBundleCategories(req, res, bundleName); } if (pathname === '/api/config' && method === 'GET') { return await this.handleGetConfig(req, res); } if (pathname === '/api/config' && method === 'POST') { return await this.handlePostConfig(req, res); } if (pathname === '/api/files' && method === 'GET') { return await this.handleGetFiles(req, res); } if (pathname === '/api/cursor-rules' && method === 'GET') { return await this.handleGetCursorRules(req, res); } if (pathname === '/api/cursor-rules' && method === 'POST') { return await this.handlePostCursorRules(req, res); } if (pathname === '/api/cursor-rules/templates' && method === 'GET') { return await this.handleGetCursorRulesTemplates(req, res); } if (pathname === '/api/claude-md' && method === 'GET') { return await this.handleGetClaudeMd(req, res); } if (pathname === '/api/claude-md' && method === 'POST') { return await this.handlePostClaudeMd(req, res); } if (pathname === '/api/heuristics/config' && method === 'GET') { return await this.handleGetHeuristicsConfig(req, res); } if (pathname === '/api/heuristics/config' && method === 'PUT') { return await this.handlePutHeuristicsConfig(req, res); } if (pathname === '/api/test-pattern' && method === 'POST') { return await this.handleTestPattern(req, res); } if (pathname === '/api/hidden-files' && method === 'GET') { return await this.handleGetHiddenFiles(req, res); } if (pathname === '/api/hidden-files' && method === 'POST') { return await this.handlePostHiddenFiles(req, res); } if (pathname === '/api/files-with-visibility' && method === 'GET') { return await this.handleGetFilesWithVisibility(req, res, url); } if (pathname === '/api/ignore-patterns' && method === 'GET') { return await this.handleGetIgnorePatterns(req, res); } if (pathname === '/api/ignore-patterns' && method === 'POST') { return await this.handlePostIgnorePatterns(req, res); } if (pathname === '/api/reset-hidden-files' && method === 'POST') { return await this.handleResetHiddenFiles(req, res); } if (pathname === '/api/semantic-chunks' && method === 'GET') { return await this.handleGetSemanticChunks(req, res, url); } if (pathname === '/api/semantic-chunks/export' && method === 'POST') { return await this.handleExportSemanticChunk(req, res); } if (pathname === '/api/mcp-status' && method === 'GET') { return await this.handleGetMcpStatus(req, res); } if (pathname === '/api/cntxignore' && method === 'GET') { return await this.handleGetCntxignore(req, res); } if (pathname === '/api/cntxignore' && method === 'POST') { return await this.handlePostCntxignore(req, res); } if (pathname === '/api/gitignore' && method === 'GET') { return await this.handleGetGitignore(req, res); } if (pathname === '/api/gitignore' && method === 'POST') { return await this.handlePostGitignore(req, res); } if (pathname === '/api/status' && method === 'GET') { return await this.handleGetStatus(req, res); } if (pathname === '/api/vector-db/status' && method === 'GET') { return await this.handleGetVectorDbStatus(req, res); } if (pathname === '/api/vector-db/rebuild' && method === 'POST') { return await this.handlePostVectorDbRebuild(req, res); } if (pathname === '/api/vector-db/search' && method === 'POST') { return await this.handlePostVectorDbSearch(req, res); } if (pathname === '/api/vector-db/search-by-type' && method === 'POST') { return await this.handlePostVectorDbSearchByType(req, res); } if (pathname === '/api/vector-db/search-by-domain' && method === 'POST') { return await this.handlePostVectorDbSearchByDomain(req, res); } if (pathname === '/api/activities' && method === 'GET') { return await this.handleGetActivities(req, res); } if (pathname.startsWith('/api/activities/') && pathname.endsWith('/execute') && method === 'POST') { const activityId = pathname.split('/')[3]; return await this.handlePostActivityExecute(req, res, activityId); } if (pathname.startsWith('/api/activities/') && pathname.endsWith('/stop') && method === 'POST') { const activityId = pathname.split('/')[3]; return await this.handlePostActivityStop(req, res, activityId); } if (pathname === '/api/open-file' && method === 'POST') { return await this.handleOpenFile(req, res); } // If no route matches, return 404 res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'API endpoint not found' })); } catch (error) { console.error('API Error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } // === Bundle Operations === async handleGetBundles(req, res, url) { const bundles = this.configManager.getBundles(); const bundleData = Array.from(bundles.entries()).map(([name, bundle]) => ({ name, changed: bundle.changed, fileCount: bundle.files.length, contentPreview: bundle.content.substring(0, 200) + (bundle.content.length > 200 ? '...' : ''), files: bundle.files, patterns: bundle.patterns, size: bundle.size, generated: bundle.generated })); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(bundleData)); } async handlePostBundles(req, res) { try { const body = JSON.parse(await this.getRequestBody(req)); const { action, bundleName, fileName, fileNames } = body; if (!action || !bundleName) { return this.sendError(res, 400, 'Missing required fields: action and bundleName'); } const bundles = this.configManager.getBundles(); const bundle = bundles.get(bundleName); if (!bundle) { return this.sendError(res, 404, `Bundle "${bundleName}" not found`); } switch (action) { case 'add-file': if (!fileName) { return this.sendError(res, 400, 'Missing fileName for add-file action'); } // Ensure we're working with relative paths const relativeAddFileName = fileName.startsWith('/') ? require('path').relative(this.configManager.CWD, fileName) : fileName; if (!bundle.files.includes(relativeAddFileName)) { bundle.files.push(relativeAddFileName); bundle.changed = true; this.configManager.saveBundleStates(); } break; case 'remove-file': if (!fileName) { return this.sendError(res, 400, 'Missing fileName for remove-file action'); } // Ensure we're working with relative paths for both search and removal const relativeRemoveFileName = fileName.startsWith('/') ? require('path').relative(this.configManager.CWD, fileName) : fileName; const removeIndex = bundle.files.indexOf(relativeRemoveFileName); if (removeIndex > -1) { bundle.files.splice(removeIndex, 1); bundle.changed = true; this.configManager.saveBundleStates(); } break; case 'bulk-add-files': if (!fileNames || !Array.isArray(fileNames)) { return this.sendError(res, 400, 'Missing fileNames array for bulk-add-files action'); } fileNames.forEach(file => { // Ensure we're working with relative paths const relativeFile = file.startsWith('/') ? require('path').relative(this.configManager.CWD, file) : file; if (!bundle.files.includes(relativeFile)) { bundle.files.push(relativeFile); } }); bundle.changed = true; this.configManager.saveBundleStates(); break; case 'bulk-remove-files': if (!fileNames || !Array.isArray(fileNames)) { return this.sendError(res, 400, 'Missing fileNames array for bulk-remove-files action'); } fileNames.forEach(file => { // Ensure we're working with relative paths const relativeFile = file.startsWith('/') ? require('path').relative(this.configManager.CWD, file) : file; const index = bundle.files.indexOf(relativeFile); if (index > -1) { bundle.files.splice(index, 1); } }); bundle.changed = true; this.configManager.saveBundleStates(); break; default: return this.sendError(res, 400, `Unknown action: ${action}`); } this.sendResponse(res, 200, { success: true, message: `Action ${action} completed successfully` }); } catch (error) { console.error('handlePostBundles error:', error); this.sendError(res, 500, error.message); } } async handleGetBundle(req, res, bundleName) { const content = this.bundleManager.getBundleContent(bundleName); if (!content) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Bundle not found' })); return; } res.writeHead(200, { 'Content-Type': 'application/xml' }); res.end(content); } async handleRegenerateBundle(req, res, bundleName) { try { await this.bundleManager.regenerateBundle(bundleName); this.sendResponse(res, 200, { success: true, message: `Bundle ${bundleName} regenerated` }); } catch (error) { this.sendError(res, 500, error.message); } } async handleCreateBundlesFromChunk(req, res) { const body = await this.getRequestBody(req); const { chunkName, files } = JSON.parse(body); const bundleName = this.configManager.createBundleFromChunk(chunkName, files); await this.bundleManager.regenerateBundle(bundleName); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, bundleName })); } async handleBundleVisibilityStats(req, res) { const bundles = this.configManager.getBundles(); const stats = {}; for (const [bundleName] of bundles) { const files = this.bundleManager.getFileListWithVisibility(bundleName); stats[bundleName] = { total: files.length, included: files.filter(f => f.included).length, hidden: files.filter(f => f.hidden).length, matching: files.filter(f => f.matchesPattern).length }; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(stats)); } async handleBundleCategories(req, res, bundleName) { const files = this.bundleManager.getFileListWithVisibility(bundleName); const includedFiles = files.filter(f => f.included).map(f => f.fullPath); const categories = this.fileSystemManager.categorizeFiles(includedFiles); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(categories)); } // === Configuration Endpoints === async handleGetConfig(req, res) { const bundles = this.configManager.getBundles(); const config = { bundles: {}, editor: this.configManager.getEditor() }; bundles.forEach((bundle, name) => { config.bundles[name] = bundle.patterns; }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(config)); } async handlePostConfig(req, res) { const body = await this.getRequestBody(req); const config = JSON.parse(body); this.configManager.saveConfig(config); this.configManager.loadConfig(); await this.bundleManager.generateAllBundles(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } async handleGetCursorRules(req, res) { const content = this.configManager.loadCursorRules(); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(content); } async handlePostCursorRules(req, res) { const body = await this.getRequestBody(req); const { content } = JSON.parse(body); this.configManager.saveCursorRules(content); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } async handleGetCursorRulesTemplates(req, res) { const templates = { react: this.configManager.generateCursorRulesTemplate({ projectType: 'react', name: 'React Project' }), vue: this.configManager.generateCursorRulesTemplate({ projectType: 'vue', name: 'Vue Project' }), angular: this.configManager.generateCursorRulesTemplate({ projectType: 'angular', name: 'Angular Project' }), 'node-backend': this.configManager.generateCursorRulesTemplate({ projectType: 'node-backend', name: 'Node.js Backend' }), javascript: this.configManager.generateCursorRulesTemplate({ projectType: 'javascript', name: 'JavaScript Project' }) }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(templates)); } async handleGetClaudeMd(req, res) { const content = this.configManager.loadClaudeMd(); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(content); } async handlePostClaudeMd(req, res) { const body = await this.getRequestBody(req); const { content } = JSON.parse(body); this.configManager.saveClaudeMd(content); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } async handleGetHeuristicsConfig(req, res) { const config = this.configManager.loadHeuristicsConfig(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(config)); } async handlePutHeuristicsConfig(req, res) { const body = await this.getRequestBody(req); const config = JSON.parse(body); this.configManager.saveHeuristicsConfig(config); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } // === File Operations === async handleGetFiles(req, res) { const fileTree = this.fileSystemManager.getFileTree(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(fileTree)); } async handleGetFilesWithVisibility(req, res, url) { const bundleName = url.searchParams.get('bundle'); const files = this.bundleManager.getFileListWithVisibility(bundleName); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(files)); } async handleGetHiddenFiles(req, res) { const hiddenConfig = this.configManager.getHiddenFilesConfig(); const stats = { globalHidden: hiddenConfig.globalHidden.length, bundleSpecificTotal: Object.values(hiddenConfig.bundleSpecific).reduce((sum, arr) => sum + arr.length, 0), userIgnorePatterns: hiddenConfig.userIgnorePatterns.length, disabledSystemPatterns: hiddenConfig.disabledSystemPatterns.length, config: hiddenConfig }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(stats)); } async handlePostHiddenFiles(req, res) { const body = await this.getRequestBody(req); const data = JSON.parse(body); if (data.action === 'toggle') { this.configManager.toggleFileVisibility(data.filePath, data.bundleName, data.forceHide); } else if (data.action === 'bulk-toggle') { this.configManager.bulkToggleFileVisibility(data.filePaths, data.bundleName, data.forceHide); } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } async handleGetIgnorePatterns(req, res) { const patterns = { system: this.fileSystemManager.ignorePatterns.filter(p => !this.configManager.getHiddenFilesConfig().userIgnorePatterns.some(up => up.pattern === p)), user: this.configManager.getHiddenFilesConfig().userIgnorePatterns, disabled: this.configManager.getHiddenFilesConfig().disabledSystemPatterns }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(patterns)); } async handlePostIgnorePatterns(req, res) { const body = await this.getRequestBody(req); const data = JSON.parse(body); if (data.action === 'add') { this.configManager.addUserIgnorePattern(data.pattern); } else if (data.action === 'remove') { this.configManager.removeUserIgnorePattern(data.pattern); } else if (data.action === 'toggle-system') { this.configManager.toggleSystemIgnorePattern(data.pattern); } // Reload patterns and regenerate bundles this.configManager.loadIgnorePatterns(); this.fileSystemManager.setIgnorePatterns(this.configManager.getIgnorePatterns()); await this.bundleManager.generateAllBundles(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } async handleResetHiddenFiles(req, res) { const body = await this.getRequestBody(req); const { scope, bundleName } = JSON.parse(body); const hiddenConfig = this.configManager.getHiddenFilesConfig(); if (scope === 'global') { hiddenConfig.globalHidden = []; } else if (scope === 'bundle' && bundleName) { delete hiddenConfig.bundleSpecific[bundleName]; } else if (scope === 'all') { hiddenConfig.globalHidden = []; hiddenConfig.bundleSpecific = {}; } this.configManager.saveHiddenFilesConfig(); await this.bundleManager.generateAllBundles(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } async handleGetCntxignore(req, res) { try { const content = this.configManager.loadCntxignore(); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(content); } catch (error) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(''); } } async handlePostCntxignore(req, res) { const body = await this.getRequestBody(req); const { content } = JSON.parse(body); // Save content and reload patterns this.configManager.saveIgnoreFile ? this.configManager.saveIgnoreFile(content) : null; this.configManager.loadIgnorePatterns(); this.fileSystemManager.setIgnorePatterns(this.configManager.getIgnorePatterns()); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } async handleGetGitignore(req, res) { try { const content = this.configManager.loadGitignore(); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(content); } catch (error) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(''); } } async handlePostGitignore(req, res) { const body = await this.getRequestBody(req); const { content } = JSON.parse(body); this.configManager.saveGitignore(content); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } // === Semantic Analysis === async handleGetSemanticChunks(req, res, url) { const forceRefresh = url.searchParams.get('refresh') === 'true'; try { const analysis = forceRefresh ? await this.semanticAnalysisManager.refreshSemanticAnalysis() : await this.semanticAnalysisManager.getSemanticAnalysis(); const rawChunks = analysis?.chunks || []; // console.log('📊 Raw chunks sample:', rawChunks[0] ? { // name: rawChunks[0].name, // type: rawChunks[0].type, // subtype: rawChunks[0].subtype, // purpose: rawChunks[0].purpose, // hasCode: !!rawChunks[0].code // } : 'No chunks'); // Transform chunks to match VectorVisualization expectations const chunks = rawChunks.map(chunk => ({ id: chunk.name || chunk.id || `chunk-${Math.random()}`, name: chunk.name, code: chunk.code, semanticType: chunk.subtype || chunk.type || 'unknown', businessDomain: chunk.tags || [], technicalPatterns: chunk.tags || [], purpose: chunk.purpose || '', filePath: chunk.filePath, files: chunk.filePath ? [chunk.filePath] : [], size: chunk.size || 0, complexity: chunk.complexity || 0, startLine: chunk.startLine, isExported: chunk.isExported, isAsync: chunk.isAsync, bundles: chunk.bundles || [], embedding: chunk.embedding, // Also include nested metadata format that VectorVisualization expects metadata: { content: chunk.code || '', semanticType: chunk.subtype || chunk.type || 'unknown', businessDomain: chunk.tags || [], technicalPatterns: chunk.tags || [], purpose: chunk.purpose || '', files: chunk.filePath ? [chunk.filePath] : [], size: chunk.size || 0, complexity: chunk.complexity || 0 } })); // console.log('📊 Transformed chunks sample:', chunks[0] ? { // id: chunks[0].id, // semanticType: chunks[0].semanticType, // hasMetadata: !!chunks[0].metadata // } : 'No chunks'); this.sendResponse(res, 200, { summary: { totalFiles: analysis?.summary?.totalFiles || analysis?.fileCount || 0, totalFunctions: rawChunks.filter(c => c.type === 'function_chunk').length, totalChunks: rawChunks.length, averageChunkSize: rawChunks.length > 0 ? Math.round(rawChunks.reduce((sum, c) => sum + (c.size || 0), 0) / rawChunks.length) : 0 }, chunks: chunks, lastSemanticAnalysis: this.semanticAnalysisManager.lastSemanticAnalysis }); } catch (error) { this.sendError(res, 500, error.message); } } async handleExportSemanticChunk(req, res) { const body = await this.getRequestBody(req); const { chunkName } = JSON.parse(body); try { const content = await this.semanticAnalysisManager.exportSemanticChunk(chunkName); res.writeHead(200, { 'Content-Type': 'application/xml' }); res.end(content); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } // === Utilities === async handleTestPattern(req, res) { const body = await this.getRequestBody(req); const { pattern } = JSON.parse(body); const allFiles = this.fileSystemManager.getAllFiles(); const matchingFiles = allFiles.filter(file => this.fileSystemManager.matchesPattern(file, pattern) ).map(file => this.fileSystemManager.relativePath(file)); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ pattern, matchCount: matchingFiles.length, matches: matchingFiles.slice(0, 100) // Limit to first 100 matches })); } async handleGetMcpStatus(req, res) { const status = { enabled: this.mcpServerStarted || false, available: true, message: 'MCP server integration available' }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(status)); } async handleGetStatus(req, res) { const bundles = this.configManager.getBundles(); const bundleStats = Array.from(bundles.entries()).map(([name, bundle]) => ({ name, fileCount: bundle.files.length, size: bundle.size, changed: bundle.changed, generated: bundle.generated })); const status = { uptime: process.uptime(), memory: process.memoryUsage(), bundles: bundleStats, scanning: this.bundleManager._isScanning || false, totalFiles: this.fileSystemManager.getAllFiles().length, mcp: { enabled: this.mcpServerStarted || false, available: true } }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(status)); } // === Vector Database Operations === async handleGetVectorDbStatus(req, res) { try { // Initialize vector store if needed if (!this.vectorStore.embedder) { await this.vectorStore.init(); } const stats = await this.vectorStore.getStats(); this.sendResponse(res, 200, { stats }); } catch (error) { this.sendError(res, 500, error.message); } } async handlePostVectorDbRebuild(req, res) { try { await this.vectorStore.clear(); const analysis = await this.semanticAnalysisManager.getSemanticAnalysis(); if (analysis && analysis.chunks) { await this.vectorStore.storePrecomputedChunks(analysis.chunks); } const stats = await this.vectorStore.getStats(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, stats })); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } async handlePostVectorDbSearch(req, res) { try { const body = await this.getRequestBody(req); const { query, limit = 10 } = JSON.parse(body); // Initialize vector store if needed if (!this.vectorStore.embedder) { await this.vectorStore.init(); } const results = await this.vectorStore.findSimilar(query, { limit }); this.sendResponse(res, 200, results); } catch (error) { this.sendError(res, 500, error.message); } } async handlePostVectorDbSearchByType(req, res) { try { const body = await this.getRequestBody(req); const { type, limit = 10 } = JSON.parse(body); // Initialize vector store if needed if (!this.vectorStore.embedder) { await this.vectorStore.init(); } const results = await this.vectorStore.findByType(type, limit); this.sendResponse(res, 200, results); } catch (error) { this.sendError(res, 500, error.message); } } async handlePostVectorDbSearchByDomain(req, res) { try { const body = await this.getRequestBody(req); const { domain, limit = 10 } = JSON.parse(body); // Initialize vector store if needed if (!this.vectorStore.embedder) { await this.vectorStore.init(); } const results = await this.vectorStore.findByDomain(domain, limit); this.sendResponse(res, 200, results); } catch (error) { this.sendError(res, 500, error.message); } } // === Activities === async handleGetActivities(req, res) { try { console.log('API: /api/activities called'); const activities = await this.activityManager.loadActivities(); console.log('API: Loaded activities:', activities.length); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(activities)); } catch (error) { console.log('API: Error loading activities:', error.message); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } async handlePostActivityExecute(req, res, activityId) { try { const result = await this.activityManager.executeActivity(activityId); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } async handlePostActivityStop(req, res, activityId) { try { const result = await this.activityManager.stopActivity(activityId); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } async handleOpenFile(req, res) { try { const body = await this.getRequestBody(req); const { filePath, line, column } = JSON.parse(body); if (!filePath) { return this.sendError(res, 400, 'Missing filePath parameter'); } const { spawn } = await import('child_process'); const path = await import('path'); const fs = await import('fs'); const fullPath = path.resolve(this.configManager.CWD, filePath); if (!fs.existsSync(fullPath)) { return this.sendError(res, 404, 'File not found'); } let editorCommand = this.configManager.getEditor(); let command, args; // Helper to add line/column if supported const addLineColumn = (file, line, column) => { if (line && column) return `${file}:${line}:${column}`; if (line) return `${file}:${line}`; return file; }; if (editorCommand === 'system') { switch (process.platform) { case 'darwin': command = 'open'; args = [fullPath]; break; case 'win32': command = 'start'; args = [fullPath]; break; default: command = 'xdg-open'; args = [fullPath]; break; } } else if (editorCommand.startsWith('code')) { // VS Code: try direct file:line format command = 'code'; if (line) { args = [`${fullPath}:${line}`]; } else { args = [fullPath]; } } else if (editorCommand.startsWith('subl')) { // Sublime Text supports file:line[:column] command = 'subl'; args = [addLineColumn(fullPath, line, column)]; } else { // Custom editor: just append file path, ignore line/column command = editorCommand.split(' ')[0]; args = [...editorCommand.split(' ').slice(1), fullPath]; } const child = spawn(command, args, { detached: true, stdio: 'ignore', shell: false }); child.unref(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `File opened with ${command}` })); } catch (error) { console.error('Failed to open file:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to open file in editor. Please check your editor configuration.', details: error.message })); } } // === Utility Methods === sendResponse(res, statusCode, data, contentType = 'application/json') { res.setHeader('Content-Type', contentType); res.statusCode = statusCode; if (typeof data === 'string') { res.end(data); } else { res.end(JSON.stringify(data)); } } sendError(res, statusCode, message) { this.sendResponse(res, statusCode, { error: message }); } async getRequestBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', reject); }); } }