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

522 lines 21.3 kB
/** * Git Knowledge Layer - Load knowledge from Git repositories * Supports authentication, branch selection, and caching */ import { access, mkdir, stat, readdir, readFile } from 'fs/promises'; import { join, dirname } from 'path'; import { existsSync } from 'fs'; import simpleGit from 'simple-git'; import * as yaml from 'yaml'; import { BaseKnowledgeLayer } from './base-layer.js'; import { AtomicTopicFrontmatterSchema } from '../types/bc-knowledge.js'; import { AuthType } from '../types/index.js'; export class GitKnowledgeLayer extends BaseKnowledgeLayer { gitConfig; auth; cacheDir; git = null; localPath; lastUpdated; constructor(name, priority, gitConfig, auth, cacheDir = '.bckb-cache') { super(name, priority); this.gitConfig = gitConfig; this.auth = auth; this.cacheDir = cacheDir; // Generate local cache path based on URL const urlHash = this.generateUrlHash(gitConfig.url); this.localPath = join(process.cwd(), cacheDir, 'git-repos', urlHash); } async initialize() { const startTime = Date.now(); const errors = []; const warnings = []; try { console.log(`🔄 Initializing Git layer: ${this.name} from ${this.gitConfig.url}`); // 1. Ensure local cache directory exists await this.ensureCacheDirectory(); // 2. Set up Git with authentication await this.setupGitWithAuth(); // 3. Clone or pull repository const repoUpdated = await this.ensureRepository(); // 4. Checkout specified branch if (this.gitConfig.branch) { await this.checkoutBranch(this.gitConfig.branch); } // 5. Load all content types from repository const knowledgePath = this.gitConfig.subpath ? join(this.localPath, this.gitConfig.subpath) : this.localPath; await this.loadFromDirectory(knowledgePath); if (repoUpdated) { this.lastUpdated = new Date(); console.log(`✅ Git layer ${this.name} updated successfully`); } else { console.log(`📦 Git layer ${this.name} using cached version`); } return { layerName: this.name, topicsLoaded: this.topics.size, indexesLoaded: 0, loadTimeMs: Date.now() - startTime, success: true }; } catch (error) { const errorMessage = `Failed to initialize Git layer: ${error instanceof Error ? error.message : String(error)}`; errors.push(errorMessage); console.error(`❌ ${errorMessage}`); return { layerName: this.name, topicsLoaded: 0, indexesLoaded: 0, loadTimeMs: Date.now() - startTime, success: false, error: errorMessage }; } } async ensureCacheDirectory() { const cacheParent = dirname(this.localPath); await mkdir(cacheParent, { recursive: true }); } async setupGitWithAuth() { const gitOptions = { baseDir: dirname(this.localPath), binary: 'git', maxConcurrentProcesses: 1, trimmed: true }; this.git = simpleGit(gitOptions); // Configure authentication based on auth type if (this.auth) { await this.configureAuthentication(); } } async configureAuthentication() { if (!this.auth || !this.git) return; switch (this.auth.type) { case AuthType.AZ_CLI: // Azure CLI authentication - verify az CLI is installed and user is logged in await this.verifyAzCliInstalled(); await this.verifyAzCliAuthenticated(); console.log('🔑 Using Azure CLI authentication (Git credential manager will handle tokens)'); // No URL modification needed - Git credential manager automatically uses az CLI tokens break; case AuthType.TOKEN: // For GitHub/GitLab token authentication const token = this.auth.token || (this.auth.token_env_var ? process.env[this.auth.token_env_var] : undefined); if (token) { // Configure git to use token authentication await this.git.addConfig('credential.helper', 'store --file=.git-credentials'); // For HTTPS URLs, we'll modify the URL to include credentials if (this.gitConfig.url.startsWith('https://')) { // This will be handled in clone/pull operations console.log('🔑 Configured token authentication'); } } else { throw new Error('Token not found for git authentication'); } break; case AuthType.SSH_KEY: // SSH key authentication - requires key to be in SSH agent if (this.auth.key_path) { // Set SSH command to use specific key process.env['GIT_SSH_COMMAND'] = `ssh -i ${this.auth.key_path} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`; console.log(`🔑 Configured SSH key authentication: ${this.auth.key_path}`); } break; case AuthType.BASIC: // Basic username/password authentication const username = this.auth.username; const password = this.auth.password || (this.auth.password_env_var ? process.env[this.auth.password_env_var] : undefined); if (username && password) { console.log('🔑 Configured basic authentication'); // This will be handled in the URL modification } else { throw new Error('Username/password not found for basic authentication'); } break; default: console.warn(`⚠️ Unsupported authentication type: ${this.auth.type}`); } } async ensureRepository() { if (!this.git) throw new Error('Git not initialized'); const repositoryExists = existsSync(join(this.localPath, '.git')); if (repositoryExists) { // Repository exists, pull latest changes console.log(`📥 Pulling latest changes for ${this.name}...`); await this.git.cwd(this.localPath); try { const pullResult = await this.git.pull('origin', this.gitConfig.branch || 'main'); return pullResult.summary.changes > 0; } catch (error) { console.warn(`⚠️ Pull failed, using cached version: ${error instanceof Error ? error.message : String(error)}`); return false; } } else { // Repository doesn't exist, clone it console.log(`📦 Cloning repository ${this.gitConfig.url}...`); const cloneUrl = this.prepareUrlWithAuth(this.gitConfig.url); await this.git.clone(cloneUrl, this.localPath, [ '--depth', '1', // Shallow clone for faster downloads '--single-branch', ...(this.gitConfig.branch ? ['--branch', this.gitConfig.branch] : []) ]); return true; } } prepareUrlWithAuth(url) { if (!this.auth) return url; // Azure CLI handles authentication via Git credential manager - don't modify URL if (this.auth.type === AuthType.AZ_CLI) { return url; } // Only modify HTTPS URLs for token/basic auth if (!url.startsWith('https://')) return url; switch (this.auth.type) { case AuthType.TOKEN: const token = this.auth.token || (this.auth.token_env_var ? process.env[this.auth.token_env_var] : undefined); if (token) { // For GitHub/GitLab: https://token@github.com/... return url.replace('https://', `https://${token}@`); } break; case AuthType.BASIC: const username = this.auth.username; const password = this.auth.password || (this.auth.password_env_var ? process.env[this.auth.password_env_var] : undefined); if (username && password) { // https://username:password@gitlab.com/... return url.replace('https://', `https://${username}:${password}@`); } break; } return url; } /** * Verify Azure CLI is installed on the system */ async verifyAzCliInstalled() { const { execSync } = await import('child_process'); try { execSync('az --version', { stdio: 'ignore' }); } catch { throw new Error('Azure CLI not found. Install from https://aka.ms/install-az-cli\n' + 'After installation, run: az login'); } } /** * Verify user is authenticated with Azure CLI */ async verifyAzCliAuthenticated() { const { execSync } = await import('child_process'); try { execSync('az account show', { stdio: 'ignore' }); } catch { throw new Error('Not logged in to Azure CLI. Run: az login\n' + 'For Azure DevOps, you may also need to run: az devops login'); } } async checkoutBranch(branch) { if (!this.git) throw new Error('Git not initialized'); console.log(`🔄 Checking out branch: ${branch}`); await this.git.cwd(this.localPath); try { // Try to checkout the branch await this.git.checkout(branch); } catch (error) { // If branch doesn't exist locally, try to checkout from remote try { await this.git.checkoutBranch(branch, `origin/${branch}`); } catch (remoteBranchError) { throw new Error(`Failed to checkout branch ${branch}: ${error instanceof Error ? error.message : String(error)}`); } } } async loadFromDirectory(dirPath) { try { await access(dirPath); // Load all three content types from standard subdirectories await this.loadTopics(); // Loads from domains/ await this.loadSpecialists(); // Loads from specialists/ await this.loadMethodologies(); // Loads from methodologies/ } catch (error) { throw new Error(`Knowledge directory not found: ${dirPath}`); } } /** * Load topics from domains/ subdirectories (or topics/ as fallback) */ async loadTopics() { const knowledgePath = this.gitConfig.subpath ? join(this.localPath, this.gitConfig.subpath) : this.localPath; const domainsPath = join(knowledgePath, 'domains'); const topicsPath = join(knowledgePath, 'topics'); try { await access(domainsPath); await this.loadTopicsFromDirectory(domainsPath); } catch (error) { // Try topics/ as fallback for backward compatibility try { await access(topicsPath); await this.loadTopicsFromDirectory(topicsPath); } catch (fallbackError) { // Neither domains/ nor topics/ exist - that's okay } } return this.topics.size; } /** * Load specialists from specialists/ directory */ async loadSpecialists() { const knowledgePath = this.gitConfig.subpath ? join(this.localPath, this.gitConfig.subpath) : this.localPath; const specialistsPath = join(knowledgePath, 'specialists'); try { await access(specialistsPath); const entries = await readdir(specialistsPath); for (const entry of entries) { if (entry.endsWith('.md')) { const filePath = join(specialistsPath, entry); try { const specialist = await this.loadSpecialist(filePath); if (specialist) { this.specialists.set(specialist.specialist_id, specialist); } } catch (error) { console.error(`Failed to load specialist ${entry}:`, error instanceof Error ? error.message : String(error)); } } } console.error(`🎭 Loaded ${this.specialists.size} specialists from ${this.name} layer`); } catch (error) { // specialists/ directory doesn't exist - that's okay } return this.specialists.size; } /** * Load methodologies from methodologies/ directory */ async loadMethodologies() { const knowledgePath = this.gitConfig.subpath ? join(this.localPath, this.gitConfig.subpath) : this.localPath; const methodologiesPath = join(knowledgePath, 'methodologies'); try { await access(methodologiesPath); // TODO: Implement methodology loading when structure is defined } catch (error) { // methodologies/ directory doesn't exist - that's okay } return this.methodologies.size; } /** * Load a single specialist from a markdown file */ async loadSpecialist(filePath) { 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; 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; } async loadTopicsFromDirectory(dirPath) { const entries = await readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dirPath, entry.name); if (entry.isDirectory()) { // Recursively load from subdirectories await this.loadTopicsFromDirectory(fullPath); } else if (entry.isFile() && entry.name.endsWith('.md')) { // Load markdown files as topics try { const content = await readFile(fullPath, 'utf-8'); const relativePath = this.getRelativePath(fullPath); const topic = await this.loadAtomicTopic(fullPath, content, relativePath); if (topic && this.validateTopic(topic)) { this.topics.set(topic.id, topic); } } catch (error) { console.warn(`⚠️ Failed to load topic from ${fullPath}: ${error instanceof Error ? error.message : String(error)}`); } } } } getRelativePath(absolutePath) { const basePath = this.gitConfig.subpath ? join(this.localPath, this.gitConfig.subpath) : this.localPath; return absolutePath.replace(basePath + '/', '').replace(/\\/g, '/'); } async getCurrentCommitHash() { if (!this.git) return undefined; try { await this.git.cwd(this.localPath); const log = await this.git.log(['--oneline', '-1']); return log.latest?.hash; } catch { return undefined; } } async getRepositorySize() { try { const stats = await stat(this.localPath); return stats.size; } catch { return undefined; } } generateUrlHash(url) { // Generate a simple hash for the URL to use as directory name let hash = 0; for (let i = 0; i < url.length; i++) { const char = url.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } async refresh() { console.log(`🔄 Refreshing Git layer: ${this.name}`); // Clear existing topics this.topics.clear(); // Re-initialize const result = await this.initialize(); return result.success; } // Implement required abstract methods from BaseKnowledgeLayer async loadIndexes() { // Git layers don't have separate indexes - everything is loaded as topics return 0; } /** * Load a single atomic topic from a markdown file */ async loadAtomicTopic(filePath, content, relativePath) { try { 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 relative path const topicId = this.normalizeTopicId(relativePath); return { id: topicId, title: frontmatter.title || topicId.replace(/-/g, ' '), filePath, frontmatter, content: markdownContent?.trim() || '', wordCount: markdownContent?.split(/\s+/).length || 0, lastModified: stats.mtime }; } catch (error) { console.warn(`⚠️ Failed to parse topic from ${filePath}: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * Normalize topic ID from file path */ normalizeTopicId(filePath, basePath) { // For git layer, filePath is already relative return filePath .replace(/\.md$/, '') .replace(/\\/g, '/') .toLowerCase(); } /** * Validate topic structure */ validateTopic(topic) { return !!(topic.id && topic.frontmatter && topic.content); } getSourceInfo() { return { type: 'git', url: this.gitConfig.url, branch: this.gitConfig.branch, subpath: this.gitConfig.subpath, localPath: this.localPath, lastUpdated: this.lastUpdated, hasAuth: !!this.auth }; } } //# sourceMappingURL=git-layer.js.map