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
JavaScript
/**
* 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