UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

357 lines (356 loc) 14.4 kB
"use strict"; /** * MCP Prompts Handler - Manages shared prompt library */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.loadPromptFile = loadPromptFile; exports.loadBuiltInPrompts = loadBuiltInPrompts; exports.mergePrompts = mergePrompts; exports.loadAllPrompts = loadAllPrompts; exports.handlePromptsListRequest = handlePromptsListRequest; exports.handlePromptsGetRequest = handlePromptsGetRequest; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const error_handling_1 = require("../core/error-handling"); const validation_1 = require("../core/constants/validation"); /** * Parses YAML frontmatter with support for nested arguments array */ function parseYamlFrontmatter(yaml) { const metadata = {}; const lines = yaml.split('\n'); let i = 0; while (i < lines.length) { const line = lines[i]; // Check for arguments array start if (line.match(/^arguments:\s*$/)) { const args = []; i++; // Parse array items (lines starting with " - ") while (i < lines.length && lines[i].match(/^\s+-\s/)) { const arg = { name: '' }; // First line of array item: " - name: value" const firstLineMatch = lines[i].match(/^\s+-\s+(\w+):\s*(.*)$/); if (firstLineMatch) { const [, key, value] = firstLineMatch; if (key === 'name') { arg.name = value.trim().replace(/^["']|["']$/g, ''); } else if (key === 'description') { arg.description = value.trim().replace(/^["']|["']$/g, ''); } else if (key === 'required') { arg.required = value.trim().toLowerCase() === 'true'; } } i++; // Continue parsing properties of this array item (lines starting with " ") while (i < lines.length && lines[i].match(/^\s{4,}\w+:/)) { const propMatch = lines[i].match(/^\s+(\w+):\s*(.*)$/); if (propMatch) { const [, key, value] = propMatch; if (key === 'name') { arg.name = value.trim().replace(/^["']|["']$/g, ''); } else if (key === 'description') { arg.description = value.trim().replace(/^["']|["']$/g, ''); } else if (key === 'required') { arg.required = value.trim().toLowerCase() === 'true'; } } i++; } if (arg.name) { args.push(arg); } } if (args.length > 0) { metadata.arguments = args; } } else { // Simple key-value pair const match = line.match(/^([^:]+):\s*(.+)$/); if (match) { const [, key, value] = match; const cleanValue = value.trim().replace(/^["']|["']$/g, ''); const trimmedKey = key.trim(); if (trimmedKey !== 'arguments') { metadata[trimmedKey] = cleanValue; } } i++; } } return metadata; } /** * Loads and parses a prompt file with YAML frontmatter */ function loadPromptFile(filePath, source = 'built-in', defaultName) { try { const content = fs.readFileSync(filePath, 'utf8'); // Parse YAML frontmatter const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!frontmatterMatch) { throw new Error(`Invalid prompt file format: missing YAML frontmatter in ${filePath}`); } const [, frontmatterYaml, promptContent] = frontmatterMatch; // Parse YAML with support for arguments array const metadata = parseYamlFrontmatter(frontmatterYaml); // Use defaultName as fallback if frontmatter lacks name (for skill folders) if (!metadata.name && defaultName) { metadata.name = defaultName; } if (!metadata.name || !metadata.description) { throw new Error(`Missing required metadata in ${filePath}: name, description`); } return { name: metadata.name, description: metadata.description, content: promptContent.trim(), arguments: metadata.arguments, source, }; } catch (error) { throw new Error(`Failed to load prompt file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error }); } } /** * Loads built-in prompts from the shared-prompts directory */ function loadBuiltInPrompts(logger, baseDir) { try { const promptsDir = baseDir ?? path.join(__dirname, '..', '..', 'shared-prompts'); if (!fs.existsSync(promptsDir)) { logger.warn('Shared prompts directory not found', { path: promptsDir }); return []; } const files = fs.readdirSync(promptsDir); const promptFiles = files.filter(file => file.endsWith('.md')); const prompts = []; for (const file of promptFiles) { try { const filePath = path.join(promptsDir, file); const prompt = loadPromptFile(filePath, 'built-in'); prompts.push(prompt); logger.debug('Loaded built-in prompt', { name: prompt.name, file }); } catch (error) { logger.error(`Failed to load prompt file ${file}`, error); } } logger.info('Loaded built-in prompts from shared library', { total: prompts.length, promptsDir, }); return prompts; } catch (error) { logger.error('Failed to load prompts directory', error); return []; } } /** * Merge built-in and user prompts with collision detection * Built-in prompts take precedence over user prompts with the same name */ function mergePrompts(builtInPrompts, userPrompts, logger) { const builtInNames = new Set(builtInPrompts.map(p => p.name)); const merged = [...builtInPrompts]; for (const userPrompt of userPrompts) { if (builtInNames.has(userPrompt.name)) { logger.warn('User prompt name collision with built-in prompt, skipping user prompt', { name: userPrompt.name, message: 'Built-in prompt takes precedence', }); continue; } merged.push(userPrompt); } return merged; } /** * Loads all prompts (built-in + user) with collision detection * This is the main entry point for loading prompts */ async function loadAllPrompts(logger, baseDir, forceRefresh = false) { // Load built-in prompts (synchronous) const builtInPrompts = loadBuiltInPrompts(logger, baseDir); // Load user prompts from git repository (async, graceful failure) let userPrompts = []; try { const { loadUserPrompts } = await Promise.resolve().then(() => __importStar(require('../core/user-prompts-loader.js'))); userPrompts = await loadUserPrompts(logger, forceRefresh); } catch (error) { logger.debug('User prompts loader not available or failed', { error: error instanceof Error ? error.message : 'Unknown error', }); } // Merge with collision detection const allPrompts = mergePrompts(builtInPrompts, userPrompts, logger); logger.info('Loaded all prompts', { builtIn: builtInPrompts.length, user: userPrompts.length, total: allPrompts.length, collisions: builtInPrompts.length + userPrompts.length - allPrompts.length, }); return allPrompts; } /** * Handle prompts/list MCP request */ async function handlePromptsListRequest(args, logger, requestId) { try { logger.info('Processing prompts/list request', { requestId }); const allPrompts = await loadAllPrompts(logger, process.env.NODE_ENV === 'test' ? args?.baseDir : undefined); // Filter out file-dependent skills when requested (MCP clients can't deliver files) const prompts = args?.excludeFileSkills ? allPrompts.filter(p => !p.files || p.files.length === 0) : allPrompts; // Convert to MCP prompts/list response format (include arguments if present) const promptList = prompts.map(prompt => { const item = { name: prompt.name, description: prompt.description, }; if (prompt.arguments && prompt.arguments.length > 0) { item.arguments = prompt.arguments; } return item; }); logger.info('Prompts list generated', { requestId, promptCount: promptList.length, }); return { prompts: promptList, }; } catch (error) { logger.error('Prompts list request failed', error); throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.OPERATION, error_handling_1.ErrorSeverity.HIGH, error instanceof Error ? error.message : 'Unknown error in prompts list', { operation: 'prompts_list', component: 'PromptsHandler', requestId, input: args, }); } } /** * Handle prompts/get MCP request */ async function handlePromptsGetRequest(args, logger, requestId) { try { logger.info('Processing prompts/get request', { requestId, promptName: args.name, }); if (!args.name) { throw new Error(validation_1.VALIDATION_MESSAGES.MISSING_PARAMETER('name')); } const prompts = await loadAllPrompts(logger, process.env.NODE_ENV === 'test' ? args?.baseDir : undefined); const prompt = prompts.find(p => p.name === args.name); if (!prompt) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.MEDIUM, `Prompt not found: ${args.name}`, { operation: 'prompts_get', component: 'PromptsHandler', requestId, }); } // Validate required arguments if prompt has arguments defined const providedArgs = args.arguments || {}; if (prompt.arguments && prompt.arguments.length > 0) { const missingRequired = prompt.arguments .filter(arg => arg.required && !providedArgs[arg.name]) .map(arg => arg.name); if (missingRequired.length > 0) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.MEDIUM, `Missing required arguments: ${missingRequired.join(', ')}`, { operation: 'prompts_get', component: 'PromptsHandler', requestId, input: { promptName: prompt.name, missingArguments: missingRequired, }, }); } } // Substitute {{argumentName}} placeholders in content let processedContent = prompt.content; for (const [argName, argValue] of Object.entries(providedArgs)) { processedContent = processedContent.replaceAll(`{{${argName}}}`, String(argValue)); } logger.info('Prompt found and returned', { requestId, promptName: prompt.name, argumentsProvided: Object.keys(providedArgs).length, }); // Convert to MCP prompts/get response format const response = { description: prompt.description, messages: [ { role: 'user', content: { type: 'text', text: processedContent, }, }, ], }; if (prompt.files && prompt.files.length > 0) { response.files = prompt.files; } return response; } catch (error) { logger.error('Prompts get request failed', error); // Re-throw if already an AppError if (error instanceof Error && 'category' in error) { throw error; } throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.OPERATION, error_handling_1.ErrorSeverity.HIGH, error instanceof Error ? error.message : 'Unknown error in prompts get', { operation: 'prompts_get', component: 'PromptsHandler', requestId, input: args, }); } }