UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

612 lines (604 loc) • 24.7 kB
/** * Embedded Knowledge Layer * * Loads knowledge content from the embedded git submodule (embedded-knowledge/). * This is the base layer (Layer 0) that provides the core BC knowledge content. */ import { fileURLToPath } from 'url'; import { readFile, stat, access } from 'fs/promises'; import { join, basename, dirname } from 'path'; import { existsSync } from 'fs'; import * as yaml from 'yaml'; import glob from 'fast-glob'; import { AtomicTopicFrontmatterSchema } from '../types/bc-knowledge.js'; import { LayerPriority } from '../types/layer-types.js'; import { BaseKnowledgeLayer } from './base-layer.js'; export class EmbeddedKnowledgeLayer extends BaseKnowledgeLayer { embeddedPath; // Support for MultiContentKnowledgeLayer interface supported_content_types = ['topics', 'specialists', 'methodologies']; constructor(embeddedPath = (() => { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); return join(__dirname, '../../embedded-knowledge'); })()) { super('embedded', LayerPriority.EMBEDDED, true); this.embeddedPath = embeddedPath; } /** * Initialize the embedded layer by loading topics and indexes from submodule */ async initialize() { if (this.initialized && this.loadResult) { return this.loadResult; } const startTime = Date.now(); try { console.error(`Initializing ${this.name} layer from ${this.embeddedPath}...`); // Early validation: Check if embedded knowledge directory exists and has content if (!existsSync(this.embeddedPath)) { throw new Error(` 🚨 BC Code Intelligence MCP Server Setup Issue PROBLEM: Embedded knowledge directory not found PATH: ${this.embeddedPath} LIKELY CAUSE: The git submodule 'embedded-knowledge' was not initialized when this package was built/installed. SOLUTIONS: šŸ“¦ For NPM users: Update to the latest version with: npm update bc-code-intelligence-mcp šŸ”§ For developers: Run: git submodule init && git submodule update šŸ¢ For package maintainers: Ensure submodules are initialized before npm publish The embedded-knowledge directory should contain BC expertise (domains/ or topics/, specialists/, methodologies/) required for the MCP server to function. `.trim()); } // Check if directory has the expected structure (domains/ or topics/ are both valid) const hasDomains = existsSync(join(this.embeddedPath, 'domains')); const hasTopics = existsSync(join(this.embeddedPath, 'topics')); const hasSpecialists = existsSync(join(this.embeddedPath, 'specialists')); const hasMethodologies = existsSync(join(this.embeddedPath, 'methodologies')); const missingDirs = []; if (!hasDomains && !hasTopics) missingDirs.push('domains/ or topics/'); if (!hasSpecialists) missingDirs.push('specialists'); if (!hasMethodologies) missingDirs.push('methodologies'); if (missingDirs.length > 0) { throw new Error(` 🚨 BC Code Intelligence MCP Server Setup Issue PROBLEM: Incomplete embedded knowledge structure PATH: ${this.embeddedPath} MISSING: ${missingDirs.join(', ')} LIKELY CAUSE: The embedded-knowledge submodule is present but incomplete or corrupted. SOLUTIONS: šŸ“¦ For NPM users: Update to the latest version with: npm update bc-code-intelligence-mcp šŸ”§ For developers: Run: git submodule update --remote --force šŸ¢ For package maintainers: Verify submodule content before npm publish Expected structure: (domains/ or topics/), specialists/, methodologies/ directories with BC expertise content. `.trim()); } // Load topics, specialists, and indexes in parallel const [topicsLoaded, specialistsLoaded, indexesLoaded] = await Promise.all([ this.loadTopics(), this.loadSpecialists(), this.loadIndexes() ]); const loadTimeMs = Date.now() - startTime; this.initialized = true; this.loadResult = this.createLoadResult(topicsLoaded, indexesLoaded, loadTimeMs); console.error(`āœ… ${this.name} layer loaded: ${topicsLoaded} topics, ${specialistsLoaded} specialists, ${indexesLoaded} indexes (${loadTimeMs}ms)`); return this.loadResult; } catch (error) { const loadTimeMs = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); this.loadResult = this.createErrorResult(errorMessage, loadTimeMs); console.error(`āŒ Failed to initialize ${this.name} layer: ${errorMessage}`); return this.loadResult; } } /** * Load all atomic topics from embedded knowledge (supports both domains/ and topics/) */ async loadTopics() { // Support both domains/ and topics/ directory structures const domainsPath = join(this.embeddedPath, 'domains'); const topicsPath = join(this.embeddedPath, 'topics'); // Determine which directory exists const useDomainsPath = existsSync(domainsPath); const useTopicsPath = existsSync(topicsPath); const basePath = useDomainsPath ? domainsPath : (useTopicsPath ? topicsPath : domainsPath); const dirType = useDomainsPath ? 'domains' : (useTopicsPath ? 'topics' : 'domains (not found)'); // Diagnostic logging for path resolution across platforms console.error(`šŸ“‚ Loading topics from: ${basePath}`); console.error(` Platform: ${process.platform}`); console.error(` Directory exists: ${existsSync(basePath)} (using ${dirType})`); // Use glob to find all markdown files, excluding samples // Normalize path separators for cross-platform compatibility let pattern = join(basePath, '**', '*.md'); // fast-glob expects forward slashes on all platforms pattern = pattern.replace(/\\/g, '/'); // Handle Windows drive letter normalization (e.g., /c/ to C:/) if (process.platform === 'win32' && pattern.startsWith('/c/')) { pattern = 'C:' + pattern.substring(2); } console.error(`šŸ” Using glob pattern: ${pattern}`); try { const topicFiles = await glob(pattern, { ignore: ['**/samples/**'], // Ignore sample files absolute: true, // Return absolute paths onlyFiles: true // Only return files, not directories }); console.error(` Found ${topicFiles.length} topic files`); if (topicFiles.length === 0) { console.error(`āš ļø Warning: No topic files found. Check if embedded-knowledge is properly populated.`); } let loadedCount = 0; for (const filePath of topicFiles) { try { const topic = await this.loadAtomicTopic(filePath); if (topic && this.validateTopic(topic)) { this.topics.set(topic.id, topic); loadedCount++; } else { console.error(`Invalid topic structure in ${filePath}`); } } catch (error) { console.error(`Failed to load topic ${filePath}:`, error instanceof Error ? error.message : String(error)); } } return loadedCount; } catch (error) { console.error(`āŒ Fatal error loading topics from ${domainsPath}:`, error); console.error(` Error type: ${error instanceof Error ? error.constructor.name : typeof error}`); console.error(` Error message: ${error instanceof Error ? error.message : String(error)}`); if (error instanceof Error && error.stack) { console.error(` Stack trace: ${error.stack}`); } throw error; // Re-throw to be caught by initialize() } } /** * Load indexes from embedded knowledge */ async loadIndexes() { const indexesPath = join(this.embeddedPath, 'indexes'); try { // Load tag indexes await this.loadTagIndexes(indexesPath); // Load other indexes await this.loadDomainCatalog(indexesPath); await this.loadTopicRelationships(indexesPath); await this.loadBCVersionMatrix(indexesPath); return this.indexes.size; } catch (error) { console.error(`Failed to load indexes from ${indexesPath}:`, error instanceof Error ? error.message : String(error)); return 0; } } /** * Load a single atomic topic from a markdown file */ async loadAtomicTopic(filePath) { const content = await readFile(filePath, 'utf-8'); const stats = await stat(filePath); // Normalize line endings const normalizedContent = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // Extract YAML frontmatter const frontmatterMatch = normalizedContent.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/); if (!frontmatterMatch) { return null; } const [, frontmatterContent, markdownContent] = frontmatterMatch; // Parse and validate frontmatter const frontmatterData = yaml.parse(frontmatterContent || ''); const frontmatter = AtomicTopicFrontmatterSchema.parse(frontmatterData); // Generate topic ID from file path const topicId = this.normalizeTopicId(filePath, this.embeddedPath); // Load companion sample file if exists let samples; if (frontmatter.samples) { const samplesPath = join(filePath, '..', frontmatter.samples); try { const sampleContent = await readFile(samplesPath, 'utf-8'); samples = { filePath: samplesPath, content: sampleContent }; } catch { // Sample file doesn't exist, which is OK } } return { id: topicId, title: frontmatter.title || topicId.replace(/-/g, ' '), // Use frontmatter title or derive from ID filePath, frontmatter, content: markdownContent?.trim() || '', wordCount: markdownContent?.split(/\s+/).length || 0, lastModified: stats.mtime, samples: samples || undefined }; } /** * Load tag indexes from JSON files */ async loadTagIndexes(indexesPath) { const tagsPath = join(indexesPath, 'tags'); try { const tagFiles = await glob('*.json', { cwd: tagsPath }); for (const tagFile of tagFiles) { const tagName = basename(tagFile, '.json'); const filePath = join(tagsPath, tagFile); try { const content = await readFile(filePath, 'utf-8'); const cleanContent = content.replace(/^\uFEFF/, ''); // Remove BOM const topics = JSON.parse(cleanContent); this.indexes.set(`tag:${tagName}`, { tag: tagName, topics, count: topics.length }); } catch (error) { console.error(`Failed to load tag index ${tagFile}:`, error instanceof Error ? error.message : String(error)); } } } catch (error) { // Tags directory might not exist, which is OK console.error(`No tags directory found at ${tagsPath}`); } } /** * Load domain catalog */ async loadDomainCatalog(indexesPath) { const catalogPath = join(indexesPath, 'domain-catalog.json'); try { const content = await readFile(catalogPath, 'utf-8'); const cleanContent = content.replace(/^\uFEFF/, ''); const catalog = JSON.parse(cleanContent); this.indexes.set('domain-catalog', catalog); } catch (error) { console.error(`Failed to load domain catalog:`, error instanceof Error ? error.message : String(error)); } } /** * Load topic relationships */ async loadTopicRelationships(indexesPath) { const relationshipsPath = join(indexesPath, 'topic-relationships.json'); try { const content = await readFile(relationshipsPath, 'utf-8'); const cleanContent = content.replace(/^\uFEFF/, ''); const relationships = JSON.parse(cleanContent); this.indexes.set('topic-relationships', relationships); } catch (error) { console.error(`Failed to load topic relationships:`, error instanceof Error ? error.message : String(error)); } } /** * Load BC version matrix */ async loadBCVersionMatrix(indexesPath) { const matrixPath = join(indexesPath, 'bc-version-matrix.json'); try { const content = await readFile(matrixPath, 'utf-8'); const cleanContent = content.replace(/^\uFEFF/, ''); const matrix = JSON.parse(cleanContent); this.indexes.set('bc-version-matrix', matrix); } catch (error) { console.error(`Failed to load BC version matrix:`, error instanceof Error ? error.message : String(error)); } } /** * Get an index by name */ getIndex(indexName) { return this.indexes.get(indexName); } /** * Get all available index names */ getIndexNames() { return Array.from(this.indexes.keys()); } /** * Load all specialists from embedded knowledge specialists folder */ async loadSpecialists() { const specialistsPath = join(this.embeddedPath, 'specialists'); try { // Check if specialists directory exists const specialistsStats = await stat(specialistsPath); if (!specialistsStats.isDirectory()) { console.error(`āš ļø Specialists path is not a directory: ${specialistsPath}`); return 0; } } catch (error) { console.error(`āš ļø Specialists directory not found: ${specialistsPath}`); return 0; } // Use glob to find all markdown files in specialists let pattern = join(specialistsPath, '*.md').replace(/\\/g, '/'); // Convert /c/path to C:/path on Windows if (pattern.startsWith('/c/')) { pattern = 'C:' + pattern.substring(2); } console.error(`šŸŽ­ Using specialists glob pattern: ${pattern}`); const specialistFiles = await glob(pattern); console.error(`Found ${specialistFiles.length} specialist files in ${specialistsPath}`); let loadedCount = 0; for (const filePath of specialistFiles) { try { const specialist = await this.loadSpecialist(filePath); if (specialist && this.validateSpecialist(specialist)) { this.specialists.set(specialist.specialist_id, specialist); loadedCount++; } else { console.error(`Invalid specialist structure in ${filePath}`); } } catch (error) { console.error(`Failed to load specialist ${filePath}:`, error instanceof Error ? error.message : String(error)); } } console.error(`šŸŽ­ Loaded ${loadedCount} specialists from embedded layer`); return loadedCount; } /** * Load a single specialist from a markdown file */ async loadSpecialist(filePath) { try { const content = await readFile(filePath, 'utf-8'); const normalizedContent = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n'); // Extract YAML frontmatter const frontmatterMatch = normalizedContent.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/); if (!frontmatterMatch) { console.error(`āš ļø No frontmatter found in ${filePath}`); return null; } const [, frontmatterContent, markdownContent] = frontmatterMatch; // Parse and validate frontmatter const frontmatterData = yaml.parse(frontmatterContent || ''); // Validate required fields if (!frontmatterData.specialist_id || !frontmatterData.title) { console.error(`āš ļø Missing required fields in ${filePath}`); return null; } // Create specialist definition const specialist = { title: frontmatterData.title, specialist_id: frontmatterData.specialist_id, emoji: frontmatterData.emoji || 'šŸ¤–', role: frontmatterData.role || 'Specialist', team: frontmatterData.team || 'General', persona: { personality: frontmatterData.persona?.personality || [], communication_style: frontmatterData.persona?.communication_style || '', greeting: frontmatterData.persona?.greeting || `${frontmatterData.emoji || 'šŸ¤–'} Hello!` }, expertise: { primary: frontmatterData.expertise?.primary || [], secondary: frontmatterData.expertise?.secondary || [] }, domains: frontmatterData.domains || [], when_to_use: frontmatterData.when_to_use || [], collaboration: { natural_handoffs: frontmatterData.collaboration?.natural_handoffs || [], team_consultations: frontmatterData.collaboration?.team_consultations || [] }, related_specialists: frontmatterData.related_specialists || [], content: markdownContent.trim() }; return specialist; } catch (error) { console.error(`āŒ Failed to parse specialist file ${filePath}:`, error); return null; } } /** * Validate specialist definition */ validateSpecialist(specialist) { return !!(specialist.specialist_id && specialist.title && specialist.persona && specialist.expertise); } /** * Load methodologies from methodologies/ directory */ async loadMethodologies() { const methodologiesPath = join(this.embeddedPath, 'methodologies'); try { await access(methodologiesPath); // TODO: Implement methodology loading when structure is defined } catch (error) { // methodologies/ directory might not exist - that's okay } return this.methodologies.size; } /** * Check if the layer has a specific specialist */ hasSpecialist(specialistId) { return this.specialists.has(specialistId); } /** * Get a specialist from this layer */ getSpecialist(specialistId) { return this.specialists.get(specialistId) || null; } /** * Get all specialist IDs available in this layer */ getSpecialistIds() { return Array.from(this.specialists.keys()); } /** * Get all specialists from this layer */ getAllSpecialists() { return Array.from(this.specialists.values()); } /** * Search for specialists within this layer */ searchSpecialists(query, limit) { const queryLower = query.toLowerCase(); const matches = []; for (const specialist of this.specialists.values()) { let score = 0; // Check title if (specialist.title.toLowerCase().includes(queryLower)) { score += 10; } // Check expertise for (const expertise of [...specialist.expertise.primary, ...specialist.expertise.secondary]) { if (expertise.toLowerCase().includes(queryLower)) { score += specialist.expertise.primary.includes(expertise) ? 8 : 5; } } // Check domains for (const domain of specialist.domains) { if (domain.toLowerCase().includes(queryLower)) { score += 6; } } // Check when_to_use scenarios for (const scenario of specialist.when_to_use) { if (scenario.toLowerCase().includes(queryLower)) { score += 7; } } if (score > 0) { matches.push({ specialist, score }); } } // Sort by score and apply limit const sortedMatches = matches .sort((a, b) => b.score - a.score) .slice(0, limit || matches.length); return sortedMatches.map(m => m.specialist); } /** * Get specialist statistics for this layer */ getSpecialistStatistics() { const specialists = Array.from(this.specialists.values()); const teams = {}; const domains = {}; for (const specialist of specialists) { // Count teams teams[specialist.team] = (teams[specialist.team] || 0) + 1; // Count domains for (const domain of specialist.domains) { domains[domain] = (domains[domain] || 0) + 1; } } return { total_specialists: specialists.length, teams, domains }; } // MultiContentKnowledgeLayer interface implementation /** * Check if content exists by type and ID */ hasContent(type, id) { switch (type) { case 'topics': return this.topics.has(id); case 'specialists': return this.specialists.has(id); default: return false; } } /** * Get content by type and ID */ async getContent(type, id) { switch (type) { case 'topics': return this.topics.get(id) || null; case 'specialists': return this.specialists.get(id) || null; default: return null; } } /** * Get all content IDs for a specific type */ getContentIds(type) { switch (type) { case 'topics': return Array.from(this.topics.keys()); case 'specialists': return Array.from(this.specialists.keys()); default: return []; } } /** * Search content within this layer by type */ searchContent(type, query, limit = 10) { switch (type) { case 'topics': return this.searchTopics(query, limit); case 'specialists': return this.searchSpecialists(query, limit); default: return []; } } /** * Get enhanced statistics with content type breakdown */ getEnhancedStatistics() { return { name: this.name, priority: this.priority, content_counts: { topics: this.topics.size, specialists: this.specialists.size, methodologies: 0 // Not supported yet }, load_time_ms: this.loadResult?.loadTimeMs, initialized: this.initialized }; } /** * Convert LayerLoadResult to EnhancedLayerLoadResult */ convertToEnhancedResult(result) { return { success: result.success, layer_name: result.layerName, load_time_ms: result.loadTimeMs, content_counts: { topics: this.topics.size, specialists: this.specialists.size, methodologies: 0 }, topics_loaded: this.topics.size, // Use actual Map size for consistency indexes_loaded: result.indexesLoaded || 0, error: result.success ? undefined : 'Layer load failed' }; } } //# sourceMappingURL=embedded-layer.js.map