Skip to main content
Glama
templateCustomizer.ts8.17 kB
/** * Template Customizer Module * * Handles template customization with hooks, variable substitution, and inheritance. */ import * as fs from 'fs/promises'; import * as path from 'path'; export interface TemplateHook { type: 'before' | 'after' | 'transform' | 'validate'; script: string; description?: string; } export interface TemplateCustomization { templateId: string; overridePath?: string; hooks?: TemplateHook[]; variables?: Record<string, string>; extends?: string; } export interface TemplateContext { templateId: string; variables: Record<string, any>; workspacePath: string; } /** * Load template with customization applied */ export async function loadCustomizedTemplate( templateId: string, context: TemplateContext, customizationPath?: string ): Promise<string> { // Load customization config if provided let customization: TemplateCustomization | null = null; if (customizationPath) { customization = await loadCustomization(customizationPath); } // Load base template (with inheritance) let templateContent = await loadBaseTemplate(templateId, customization); // Apply before hooks if (customization?.hooks) { templateContent = await applyHooks(templateContent, customization.hooks, 'before', context); } // Apply variable substitution templateContent = applyVariableSubstitution(templateContent, { ...context.variables, ...customization?.variables, }); // Apply transform hooks if (customization?.hooks) { templateContent = await applyHooks(templateContent, customization.hooks, 'transform', context); } // Apply after hooks if (customization?.hooks) { templateContent = await applyHooks(templateContent, customization.hooks, 'after', context); } // Apply validate hooks if (customization?.hooks) { await applyHooks(templateContent, customization.hooks, 'validate', context); } return templateContent; } /** * Load customization config */ async function loadCustomization(customizationPath: string): Promise<TemplateCustomization> { try { const content = await fs.readFile(customizationPath, 'utf-8'); return JSON.parse(content); } catch (error) { throw new Error(`Failed to load customization: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Load base template with inheritance support */ async function loadBaseTemplate( templateId: string, customization: TemplateCustomization | null ): Promise<string> { // If there's an override, use that if (customization?.overridePath) { try { return await fs.readFile(customization.overridePath, 'utf-8'); } catch { // Fall through to default template } } // If template extends another, load parent first if (customization?.extends) { const parentContent = await loadBaseTemplate(customization.extends, null); // Child template can override sections of parent const childContent = await loadBuiltInTemplate(templateId); return mergeTemplates(parentContent, childContent); } // Load built-in template return await loadBuiltInTemplate(templateId); } /** * Load built-in template */ async function loadBuiltInTemplate(templateId: string): Promise<string> { // Try templates/projects/{templateId}/constitution.md const constitutionPath = path.join( process.cwd(), 'templates', 'projects', templateId, 'constitution.md' ); try { return await fs.readFile(constitutionPath, 'utf-8'); } catch { throw new Error(`Built-in template "${templateId}" not found`); } } /** * Merge parent and child templates */ function mergeTemplates(parent: string, child: string): string { // Simple merge strategy: sections in child override parent // Sections are identified by ## headings const parentSections = parseTemplateSections(parent); const childSections = parseTemplateSections(child); // Merge: child sections override parent sections const merged = { ...parentSections, ...childSections }; // Rebuild template let result = ''; for (const [heading, content] of Object.entries(merged)) { result += `## ${heading}\n\n${content}\n\n`; } return result.trim(); } /** * Parse template into sections */ function parseTemplateSections(template: string): Record<string, string> { const sections: Record<string, string> = {}; // Split by ## headings const parts = template.split(/^##\s+/m); // Skip first part (before first ##) for (let i = 1; i < parts.length; i++) { const part = parts[i]; const lines = part.split('\n'); const heading = lines[0].trim(); const content = lines.slice(1).join('\n').trim(); sections[heading] = content; } return sections; } /** * Apply variable substitution */ function applyVariableSubstitution( template: string, variables: Record<string, any> ): string { let result = template; // Replace {{variable}} with values for (const [key, value] of Object.entries(variables)) { const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); result = result.replace(regex, String(value)); } return result; } /** * Apply hooks of a specific type */ async function applyHooks( content: string, hooks: TemplateHook[], hookType: 'before' | 'after' | 'transform' | 'validate', context: TemplateContext ): Promise<string> { let result = content; const relevantHooks = hooks.filter(h => h.type === hookType); for (const hook of relevantHooks) { result = await executeHook(result, hook, context); } return result; } /** * Execute a single hook */ async function executeHook( content: string, hook: TemplateHook, context: TemplateContext ): Promise<string> { try { // For now, we support simple JavaScript hooks // In the future, we could support external scripts // Create a safe eval environment const hookFunction = new Function('content', 'context', hook.script); const result = hookFunction(content, context); // Handle async results if (result instanceof Promise) { return await result; } return result; } catch (error) { throw new Error(`Hook execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Validate template customization */ export async function validateCustomization( customization: TemplateCustomization ): Promise<{ valid: boolean; errors: string[] }> { const errors: string[] = []; // Check if template exists try { await loadBuiltInTemplate(customization.templateId); } catch { errors.push(`Template "${customization.templateId}" does not exist`); } // Check if parent template exists (if extends is used) if (customization.extends) { try { await loadBuiltInTemplate(customization.extends); } catch { errors.push(`Parent template "${customization.extends}" does not exist`); } } // Check if override path exists (if specified) if (customization.overridePath) { try { await fs.access(customization.overridePath); } catch { errors.push(`Override file "${customization.overridePath}" does not exist`); } } // Validate hooks if (customization.hooks) { for (const hook of customization.hooks) { if (!hook.script) { errors.push(`Hook of type "${hook.type}" has no script`); } } } return { valid: errors.length === 0, errors, }; } /** * List customization points for a template */ export async function listCustomizationPoints( templateId: string ): Promise<{ sections: string[]; variables: string[]; hooks: string[]; }> { const template = await loadBuiltInTemplate(templateId); // Extract sections (## headings) const sectionMatches = template.matchAll(/^##\s+(.+)$/gm); const sections = Array.from(sectionMatches, m => m[1]); // Extract variables ({{variable}}) const variableMatches = template.matchAll(/\{\{(\w+)\}\}/g); const variables = Array.from(new Set(Array.from(variableMatches, m => m[1]))); // Hook types available const hooks = ['before', 'after', 'transform', 'validate']; return { sections, variables, hooks, }; }

Latest Blog Posts

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/flight505/MCP_DinCoder'

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