UNPKG

@gork-labs/secondbrain-mcp

Version:

Second Brain MCP Server - Agent team orchestration with dynamic tool discovery

347 lines (337 loc) 14.4 kB
import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { SubagentNotFoundError } from '../utils/types.js'; import { config } from '../utils/config.js'; import { logger } from '../utils/logger.js'; import { templateManager } from '../utils/template-manager.js'; export class SubagentLoader { subagents = new Map(); instructions = ''; initialized = false; async initialize() { if (this.initialized) { return; } try { await this.loadInstructions(); await this.ensureSubagentsDirectory(); await templateManager.initialize(); await this.loadSubagents(); this.initialized = true; logger.info('Subagent loader initialized', { subagentsPath: config.subagentsPath, loadedCount: this.subagents.size, instructionsLength: this.instructions.length }); } catch (error) { logger.error('Failed to initialize subagent loader', { error: error instanceof Error ? error.message : String(error) }); throw error; } } async loadInstructions() { // Get the directory of this module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Look for instructions in the package (copied during build) const packageRoot = path.resolve(__dirname, '../..'); const instructionsPath = path.join(packageRoot, 'instructions'); logger.info('Loading global instructions', { path: instructionsPath }); if (!fs.existsSync(instructionsPath)) { logger.warn('Instructions directory not found', { path: instructionsPath }); this.instructions = ''; return; } const instructionFiles = fs.readdirSync(instructionsPath) .filter(file => file.endsWith('.instructions.md')) .sort(); // Consistent ordering let combinedInstructions = ''; for (const file of instructionFiles) { try { const filePath = path.join(instructionsPath, file); const content = fs.readFileSync(filePath, 'utf-8'); // Remove frontmatter from instructions if present const cleanContent = content.replace(/^---\n[\s\S]*?\n---\n/, ''); combinedInstructions += `\n\n<!-- ${file} -->\n${cleanContent}`; logger.debug('Loaded instruction file', { file, length: cleanContent.length }); } catch (error) { logger.warn('Failed to load instruction file', { file, error: error instanceof Error ? error.message : String(error) }); } } this.instructions = combinedInstructions.trim(); logger.info('Global instructions loaded', { filesCount: instructionFiles.length, totalLength: this.instructions.length, files: instructionFiles }); } async ensureSubagentsDirectory() { const subagentsPath = config.subagentsPath; // Create subagents directory if it doesn't exist if (!fs.existsSync(subagentsPath)) { logger.info('Creating subagents directory', { path: subagentsPath }); fs.mkdirSync(subagentsPath, { recursive: true }); } // Check if directory is empty or has no .subagent.md files const files = fs.readdirSync(subagentsPath); const subagentFiles = files.filter(file => file.endsWith('.subagent.md') || file.endsWith('.chatmode.md')); if (subagentFiles.length === 0) { logger.info('No subagent files found, copying defaults'); await this.copyDefaultSubagents(subagentsPath); } } async copyDefaultSubagents(targetPath) { try { // Get the directory of this module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Look for subagents in the package const packageRoot = path.resolve(__dirname, '../..'); const defaultSubagentsPath = path.join(packageRoot, 'subagents'); if (fs.existsSync(defaultSubagentsPath)) { const defaultFiles = fs.readdirSync(defaultSubagentsPath); const subagentFiles = defaultFiles.filter(file => file.endsWith('.subagent.md') || file.endsWith('.chatmode.md')); for (const file of subagentFiles) { const sourcePath = path.join(defaultSubagentsPath, file); const targetFilePath = path.join(targetPath, file); if (!fs.existsSync(targetFilePath)) { fs.copyFileSync(sourcePath, targetFilePath); logger.debug('Copied default subagent', { file, from: sourcePath, to: targetFilePath }); } } logger.info('Copied default subagents', { count: subagentFiles.length, from: defaultSubagentsPath, to: targetPath }); } else { logger.warn('Default subagents directory not found in package', { expectedPath: defaultSubagentsPath }); } } catch (error) { logger.error('Failed to copy default subagents', { error: error instanceof Error ? error.message : String(error) }); // Don't throw - server can still work without defaults } } async loadSubagents() { const subagentsPath = config.subagentsPath; // Directory should exist now due to ensureSubagentsDirectory if (!fs.existsSync(subagentsPath)) { throw new Error(`Subagents directory not found: ${subagentsPath}`); } const files = fs.readdirSync(subagentsPath); const subagentFiles = files.filter(file => file.endsWith('.subagent.md') || file.endsWith('.chatmode.md')); if (subagentFiles.length === 0) { logger.warn('No subagent files found', { path: subagentsPath }); return; } for (const file of subagentFiles) { try { const filePath = path.join(subagentsPath, file); const subagent = await this.parseSubagentFile(filePath); this.subagents.set(subagent.name, subagent); logger.debug('Loaded subagent', { name: subagent.name, file }); } catch (error) { logger.warn('Failed to load subagent file', { file, error: error instanceof Error ? error.message : String(error) }); } } if (this.subagents.size === 0) { throw new Error('No valid subagents found'); } } async parseSubagentFile(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); // Parse frontmatter and content const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!frontmatterMatch) { throw new Error('Invalid subagent file format - missing frontmatter'); } const [, frontmatterStr, chatmodeContent] = frontmatterMatch; // Parse YAML frontmatter (simple key-value parsing) const frontmatter = this.parseSimpleYaml(frontmatterStr); if (!frontmatter.description) { throw new Error('Subagent file missing required description field'); } // Extract subagent name from filename const filename = path.basename(filePath, filePath.endsWith('.subagent.md') ? '.subagent.md' : '.chatmode.md'); const name = filename.replace(' - Gorka', ''); // Combine subagent content with global instructions const combinedContent = this.combineContentWithInstructions(chatmodeContent.trim(), name); return { name, description: frontmatter.description, tools: frontmatter.tools || [], content: combinedContent, filePath }; } combineContentWithInstructions(chatmodeContent, chatmodeName) { // For sub-agents, we only include essential instructions to avoid context overflow // The full global instructions are meant for the main orchestrator agent const essentialInstructions = this.getEssentialInstructionsForSubAgent(); if (!essentialInstructions) { return chatmodeContent; } try { return templateManager.render('sub-agent-wrapper', { chatmodeName, essentialInstructions, chatmodeContent: chatmodeContent }); } catch (error) { logger.error('Failed to render sub-agent wrapper template', { chatmodeName, error: error instanceof Error ? error.message : String(error) }); // Fallback to minimal template for sub-agents return `# ${chatmodeName} - Sub-Agent Specialist ## Essential Guidelines ${essentialInstructions} ## Domain-Specific Expertise ${chatmodeContent} --- You are a specialized sub-agent focused on your domain expertise. Follow the essential guidelines while applying your specialized knowledge.`; } } getEssentialInstructionsForSubAgent() { // Extract only the most critical instructions for sub-agents to avoid context overflow return `## Core Principles **Tools First**: Always prefer specialized tools over CLI commands when available. **Evidence-Based Analysis**: All recommendations must include: - Specific file paths and line numbers - Actual code snippets from the codebase - Concrete implementation examples - Confidence levels (High/Medium/Low) for findings **Honesty Requirements**: - Explicitly state what you can and cannot verify - Distinguish between static analysis and runtime assessment - Acknowledge limitations in your analysis **Response Format**: Always respond in the required JSON format with deliverables, memory_operations, and metadata sections.`; } parseSimpleYaml(yamlStr) { const result = {}; const lines = yamlStr.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const colonIndex = trimmed.indexOf(':'); if (colonIndex === -1) continue; const key = trimmed.substring(0, colonIndex).trim(); const valueStr = trimmed.substring(colonIndex + 1).trim(); // Handle simple array format: [item1, item2, item3] if (valueStr.startsWith('[') && valueStr.endsWith(']')) { const arrayContent = valueStr.slice(1, -1); result[key] = arrayContent.split(',').map(item => item.trim().replace(/^['"]|['"]$/g, '')).filter(item => item.length > 0); } // Handle quoted strings else if ((valueStr.startsWith('"') && valueStr.endsWith('"')) || (valueStr.startsWith("'") && valueStr.endsWith("'"))) { result[key] = valueStr.slice(1, -1); } // Handle plain strings else { result[key] = valueStr; } } return result; } getSubagent(name) { if (!this.initialized) { throw new Error('SubagentLoader not initialized'); } const subagent = this.subagents.get(name); if (!subagent) { throw new SubagentNotFoundError(name); } return subagent; } listSubagents() { if (!this.initialized) { throw new Error('SubagentLoader not initialized'); } return Array.from(this.subagents.keys()).sort(); } getSubagentInfo(name) { const subagent = this.getSubagent(name); return { name: subagent.name, description: subagent.description, tools: subagent.tools }; } // Get all subagents info for listing getAllSubagentsInfo() { if (!this.initialized) { throw new Error('SubagentLoader not initialized'); } return Array.from(this.subagents.values()) .map(subagent => ({ name: subagent.name, description: subagent.description, tools: subagent.tools })) .sort((a, b) => a.name.localeCompare(b.name)); } // Refresh subagents and instructions from disk async refresh() { this.subagents.clear(); this.instructions = ''; this.initialized = false; await templateManager.reload(); await this.initialize(); } // Check if a subagent exists hasSubagent(name) { return this.subagents.has(name); } // Get the current instructions content (for debugging/inspection) getInstructions() { return this.instructions; } // Get combined content for a specific subagent (for debugging/inspection) getCombinedContent(name) { const subagent = this.getSubagent(name); return subagent.content; } // Get subagent with full instruction wrapper for sub-agents getSubagentWithWrapper(name, isSubAgent = false) { const subagent = this.getSubagent(name); if (!isSubAgent) { return subagent.content; } try { return templateManager.render('sub-agent-wrapper', { subagentContent: subagent.content, subagentName: name }); } catch (error) { logger.error('Failed to render sub-agent wrapper template', { subagentName: name, error: error instanceof Error ? error.message : String(error) }); // Minimal fallback when template system fails return `${subagent.content} Respond in JSON format with: deliverables, memory_operations, metadata sections.`; } } }