Skip to main content
Glama
output-formatter.ts17.8 kB
/** * Multi-Format Output Formatter Service for Context Curator * * Provides comprehensive output formatting capabilities supporting XML, JSON, and YAML formats * with task-type specific templates, validation, and schema compliance. */ import { promises as fs } from 'fs'; import path, { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { existsSync } from 'fs'; import yaml from 'js-yaml'; import logger from '../../../logger.js'; import { XMLFormatter } from '../utils/xml-formatter.js'; import { OutputFormat, TaskType, XmlOutputValidation, JsonOutputValidation, YamlOutputValidation, ContextCuratorConfig } from '../types/context-curator.js'; import { ContextPackage } from '../types/output-package.js'; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Template variable substitution interface */ export interface TemplateVariables { projectName?: string; taskType: TaskType; userPrompt: string; refinedPrompt?: string; // Made optional since it's already in metadata totalFiles: number; totalTokens: number; generationTimestamp: string; [key: string]: string | number | undefined; } /** * Format-specific output result */ export interface FormattedOutput { content: string; format: OutputFormat; size: number; validation: XmlOutputValidation | JsonOutputValidation | YamlOutputValidation; processingTimeMs: number; } /** * Multi-Format Output Formatter Service */ export class OutputFormatterService { private static instance: OutputFormatterService; private templateCache: Map<string, string> = new Map(); private constructor() {} /** * Get singleton instance */ static getInstance(): OutputFormatterService { if (!OutputFormatterService.instance) { OutputFormatterService.instance = new OutputFormatterService(); } return OutputFormatterService.instance; } /** * Format context package in specified format */ async formatOutput( contextPackage: ContextPackage, format: OutputFormat, config: ContextCuratorConfig, templateVariables?: Partial<TemplateVariables> ): Promise<FormattedOutput> { const startTime = Date.now(); try { logger.debug({ format, packageId: contextPackage.metadata }, 'Formatting output'); // Prepare template variables const variables: TemplateVariables = { projectName: contextPackage.metadata.targetDirectory ? path.basename(contextPackage.metadata.targetDirectory) : 'unknown', taskType: contextPackage.metadata.taskType, userPrompt: contextPackage.metadata.originalPrompt, // NOTE: refinedPrompt is already in metadata, no need to duplicate it in templateVariables // refinedPrompt: contextPackage.refinedPrompt, totalFiles: contextPackage.metadata.filesIncluded, totalTokens: contextPackage.metadata.totalTokenEstimate, generationTimestamp: (contextPackage.metadata.generationTimestamp || new Date()).toISOString(), ...templateVariables, ...(config.outputFormat?.templateOptions?.customVariables || {}) }; let content: string; let validation: XmlOutputValidation | JsonOutputValidation | YamlOutputValidation; switch (format) { case 'xml': content = await this.formatAsXML(contextPackage, variables, config); validation = this.validateXMLOutput(content); break; case 'json': content = await this.formatAsJSON(contextPackage, variables, config); validation = this.validateJSONOutput(content); break; case 'yaml': content = await this.formatAsYAML(contextPackage, variables, config); validation = this.validateYAMLOutput(content); break; default: throw new Error(`Unsupported output format: ${format}`); } const processingTimeMs = Date.now() - startTime; logger.info({ format, size: content.length, processingTimeMs, isValid: this.isValidationPassed(validation) }, 'Output formatting completed'); return { content, format, size: content.length, validation, processingTimeMs }; } catch (error) { const processingTimeMs = Date.now() - startTime; logger.error({ error: error instanceof Error ? error.message : 'Unknown error', format, processingTimeMs }, 'Output formatting failed'); throw new Error(`Output formatting failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Format as XML using enhanced templates */ private async formatAsXML( contextPackage: ContextPackage, variables: TemplateVariables, _config: ContextCuratorConfig ): Promise<string> { // Load task-specific template const template = await this.loadTemplate(variables.taskType, 'xml'); if (template.trim()) { // Use template-based formatting - generate only inner content for templates const innerXml = this.generateInnerXMLContent(contextPackage); const enhancedXml = this.applyTemplateVariables(innerXml, variables, template); return enhancedXml; } else { // Fallback to base XML formatter with variable substitution const baseXml = XMLFormatter.formatContextPackage(contextPackage); return this.applyTemplateVariables(baseXml, variables, ''); } } /** * Generate inner XML content without XML declaration and root element */ private generateInnerXMLContent(contextPackage: ContextPackage): string { // Generate the full XML first const fullXml = XMLFormatter.formatContextPackage(contextPackage); // Extract only the inner content (everything between the root tags) let innerContent = fullXml; // Remove XML declaration innerContent = innerContent.replace(/^\s*<\?xml[^>]*\?>\s*\n?/, ''); // Remove the opening context_package tag and its attributes innerContent = innerContent.replace(/^\s*<context_package[^>]*>\s*\n?/, ''); // Remove the closing context_package tag innerContent = innerContent.replace(/\s*<\/context_package>\s*$/, ''); return innerContent.trim(); } /** * Format as JSON */ private async formatAsJSON( contextPackage: ContextPackage, variables: TemplateVariables, _config: ContextCuratorConfig ): Promise<string> { // Convert context package to JSON-friendly structure const jsonData = { metadata: { ...contextPackage.metadata, generationTimestamp: (contextPackage.metadata.generationTimestamp || new Date()).toISOString(), taskType: variables.taskType, format: 'json' as const }, // NOTE: refinedPrompt is already included in metadata, no need to duplicate it here codemapPath: contextPackage.codemapPath, files: { highPriority: contextPackage.highPriorityFiles || [], mediumPriority: contextPackage.mediumPriorityFiles || [], lowPriority: contextPackage.lowPriorityFiles || [] }, metaPrompt: contextPackage.metaPrompt || (contextPackage as Record<string, unknown>).fullMetaPrompt, templateVariables: { ...variables, // Include AI agent response format from the original context package aiAgentResponseFormat: this.extractAiAgentResponseFormat(contextPackage) } }; return JSON.stringify(jsonData, null, 2); } /** * Format as YAML */ private async formatAsYAML( contextPackage: ContextPackage, variables: TemplateVariables, _config: ContextCuratorConfig ): Promise<string> { // Convert context package to YAML-friendly structure const yamlData = { metadata: { ...contextPackage.metadata, generationTimestamp: (contextPackage.metadata.generationTimestamp || new Date()).toISOString(), taskType: variables.taskType, format: 'yaml' as const }, // NOTE: refinedPrompt is already included in metadata, no need to duplicate it here codemapPath: contextPackage.codemapPath, files: { highPriority: contextPackage.highPriorityFiles || [], mediumPriority: contextPackage.mediumPriorityFiles || [], lowPriority: contextPackage.lowPriorityFiles || [] }, metaPrompt: contextPackage.metaPrompt || (contextPackage as Record<string, unknown>).fullMetaPrompt, templateVariables: { ...variables, // Include AI agent response format from the original context package aiAgentResponseFormat: this.extractAiAgentResponseFormat(contextPackage) } }; return yaml.dump(yamlData, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false }); } /** * Load task-type specific template */ private async loadTemplate(taskType: TaskType, format: OutputFormat): Promise<string> { const cacheKey = `${taskType}-${format}`; if (this.templateCache.has(cacheKey)) { return this.templateCache.get(cacheKey)!; } try { // Try multiple possible paths for templates directory const possiblePaths = [ join(__dirname, '..', 'templates'), // Normal case: src/tools/context-curator/services -> src/tools/context-curator/templates join(__dirname, '..', '..', '..', '..', 'src', 'tools', 'context-curator', 'templates'), // Test case: build/tools/context-curator/services -> src/tools/context-curator/templates join(process.cwd(), 'src', 'tools', 'context-curator', 'templates'), // Fallback: from project root join(process.cwd(), 'build', 'tools', 'context-curator', 'templates') // Build directory ]; let templatePath: string | null = null; const templateFile = `${taskType}-template.${format}`; for (const basePath of possiblePaths) { const candidatePath = join(basePath, templateFile); if (existsSync(candidatePath)) { templatePath = candidatePath; break; } } if (!templatePath) { throw new Error(`Template not found: ${templateFile} in any of the expected locations`); } const template = await fs.readFile(templatePath, 'utf-8'); this.templateCache.set(cacheKey, template); logger.debug({ taskType, format, templatePath }, 'Template loaded and cached'); return template; } catch (error) { logger.warn({ taskType, format, error: error instanceof Error ? error.message : 'Unknown error' }, 'Failed to load template, using default'); // Return empty template as fallback return ''; } } /** * Apply template variables to content */ private applyTemplateVariables( content: string, variables: TemplateVariables, template: string ): string { // If template exists, use template-based approach if (template.trim()) { // First, apply variable substitution to the template let result = template; Object.entries(variables).forEach(([key, value]) => { if (value !== undefined) { const placeholder = `{{${key}}}`; result = result.replace(new RegExp(placeholder, 'g'), String(value)); } }); // Then replace {{CONTENT}} with the actual content // Remove XML declaration from content if template already has one let cleanContent = content; if (template.includes('<?xml') && content.includes('<?xml')) { // Remove ALL XML declarations from content, not just the first one // This handles cases where content might have multiple XML declarations cleanContent = content.replace(/<\?xml[^>]*\?>\s*\n?/g, ''); } result = result.replace('{{CONTENT}}', cleanContent); return result; } else { // Fallback: apply variable substitution directly to content let result = content; Object.entries(variables).forEach(([key, value]) => { if (value !== undefined) { const placeholder = `{{${key}}}`; result = result.replace(new RegExp(placeholder, 'g'), String(value)); } }); return result; } } /** * Validate XML output */ private validateXMLOutput(content: string): XmlOutputValidation { return { hasXmlDeclaration: content.startsWith('<?xml'), isWellFormed: this.isWellFormedXML(content), schemaCompliant: this.isXMLSchemaCompliant(content), validEncoding: this.hasValidXMLEncoding(content) }; } /** * Validate JSON output */ private validateJSONOutput(content: string): JsonOutputValidation { try { const parsed = JSON.parse(content); return { isValidJson: true, schemaCompliant: this.isJSONSchemaCompliant(parsed), hasRequiredFields: this.hasRequiredJSONFields(parsed) }; } catch { return { isValidJson: false, schemaCompliant: false, hasRequiredFields: false }; } } /** * Validate YAML output */ private validateYAMLOutput(content: string): YamlOutputValidation { try { const parsed = yaml.load(content) as Record<string, unknown>; return { isValidYaml: true, schemaCompliant: this.isYAMLSchemaCompliant(parsed), hasRequiredFields: this.hasRequiredYAMLFields(parsed) }; } catch { return { isValidYaml: false, schemaCompliant: false, hasRequiredFields: false }; } } /** * Check if validation passed */ private isValidationPassed(validation: XmlOutputValidation | JsonOutputValidation | YamlOutputValidation): boolean { if ('isWellFormed' in validation) { return validation.hasXmlDeclaration && validation.isWellFormed && validation.schemaCompliant; } else if ('isValidJson' in validation) { return validation.isValidJson && validation.schemaCompliant && validation.hasRequiredFields; } else { return validation.isValidYaml && validation.schemaCompliant && validation.hasRequiredFields; } } // Validation helper methods private isWellFormedXML(content: string): boolean { // Basic XML well-formedness check const openTags = content.match(/<[^/][^>]*>/g) || []; const closeTags = content.match(/<\/[^>]*>/g) || []; return openTags.length >= closeTags.length; } private isXMLSchemaCompliant(content: string): boolean { // Check for required XML elements - be more flexible with element names return content.includes('<context_package') || content.includes('<feature_addition_context_package') || content.includes('<bug_fix_context_package') || content.includes('<refactoring_context_package') || content.includes('<general_context_package') || (content.includes('<package_metadata>') && (content.includes('</context_package>') || content.includes('</feature_addition_context_package>') || content.includes('</bug_fix_context_package>') || content.includes('</refactoring_context_package>') || content.includes('</general_context_package>'))); } /** * Extract AI agent response format from context package */ private extractAiAgentResponseFormat(contextPackage: Record<string, unknown>): Record<string, unknown> | undefined { // Try to get from the original context package structure if (contextPackage && typeof contextPackage === 'object') { // Check if it's in the fullMetaPrompt object (preserved from conversion) if (contextPackage.fullMetaPrompt && typeof contextPackage.fullMetaPrompt === 'object') { const fullMeta = contextPackage.fullMetaPrompt as Record<string, unknown>; if (fullMeta.aiAgentResponseFormat) { return fullMeta.aiAgentResponseFormat as Record<string, unknown>; } } // Check if it's in the metaPrompt object (legacy structure) if (contextPackage.metaPrompt && typeof contextPackage.metaPrompt === 'object') { const meta = contextPackage.metaPrompt as Record<string, unknown>; if (meta.aiAgentResponseFormat) { return meta.aiAgentResponseFormat as Record<string, unknown>; } } // Check if it's directly on the context package (fallback) if (contextPackage.aiAgentResponseFormat) { return contextPackage.aiAgentResponseFormat as Record<string, unknown>; } } // Return undefined if not found return undefined; } private hasValidXMLEncoding(content: string): boolean { return content.includes('encoding="UTF-8"') || !content.includes('encoding='); } private isJSONSchemaCompliant(data: Record<string, unknown>): boolean { return !!(data && typeof data === 'object' && data.metadata && data.files); } private hasRequiredJSONFields(data: Record<string, unknown>): boolean { const metadata = data.metadata as Record<string, unknown> | undefined; return !!(metadata?.taskType && metadata?.generationTimestamp && metadata?.refinedPrompt && data.files); } private isYAMLSchemaCompliant(data: Record<string, unknown>): boolean { return !!(data && typeof data === 'object' && data.metadata && data.files); } private hasRequiredYAMLFields(data: Record<string, unknown>): boolean { const metadata = data.metadata as Record<string, unknown> | undefined; return !!(metadata?.taskType && metadata?.generationTimestamp && metadata?.refinedPrompt && data.files); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server