Skip to main content
Glama
implementation-log-manager.ts22.8 kB
import { promises as fs } from 'fs'; import { join } from 'path'; import { ImplementationLog, ImplementationLogEntry } from '../types.js'; import { randomUUID } from 'crypto'; /** * Manager for implementation logs using markdown file format * Each implementation log entry is stored as an individual markdown file * in the spec's "Implementation Logs" directory */ export class ImplementationLogManager { private specPath: string; private logsDir: string; constructor(specPath: string) { this.specPath = specPath; this.logsDir = join(specPath, 'Implementation Logs'); } /** * Ensure the Implementation Logs directory exists */ private async ensureLogsDir(): Promise<void> { try { await fs.mkdir(this.logsDir, { recursive: true }); } catch (error) { // Directory might already exist, ignore } } /** * Parse markdown filename to extract taskId and entry ID * Expected format: task-{sanitized-taskId}_{timestamp}_{id-prefix}.md */ private parseFileName(fileName: string): { taskId?: string; id?: string } | null { if (!fileName.endsWith('.md')) return null; const baseName = fileName.slice(0, -3); // Remove .md extension const parts = baseName.split('_'); if (parts.length < 3 || !parts[0].startsWith('task-')) return null; // Reconstruct taskId from the first part (unsanitize) const taskIdPart = parts[0].slice(5); // Remove 'task-' prefix const taskId = taskIdPart.replace(/-/g, '.').replace(/\.{2,}/g, '.'); // Simple unsanitization return { taskId }; } /** * Parse markdown content to extract metadata and artifacts */ private parseMarkdownContent(content: string): ImplementationLogEntry | null { try { const lines = content.split('\n'); let idValue = ''; let taskId = ''; let summary = ''; let timestamp = ''; let linesAdded = 0; let linesRemoved = 0; let filesChanged = 0; const filesModified: string[] = []; const filesCreated: string[] = []; const artifacts: ImplementationLogEntry['artifacts'] = {}; let currentSection = ''; let currentArtifactType: keyof ImplementationLogEntry['artifacts'] | null = null; let currentItem: any = {}; // Helper function to normalize markdown keys to camelCase const normalizeKey = (key: string): string => { // Convert "Key Name" to camelCase const words = key.toLowerCase().trim().split(/\s+/); if (words.length === 0) return ''; return words[0] + words.slice(1).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); }; // Helper function to map markdown property names to TypeScript interface property names const mapPropertyName = (normalizedKey: string): string => { const mapping: Record<string, string> = { 'exported': 'isExported' // Exported → isExported }; return mapping[normalizedKey] || normalizedKey; }; // Helper function to convert string values to appropriate types const convertValue = (key: string, value: string): any => { // Convert Yes/No to boolean if (value === 'Yes' || value === 'yes') return true; if (value === 'No' || value === 'no') return false; // Handle N/A as empty string if (value === 'N/A' || value === 'n/a') return ''; return value; }; // Helper function to parse key-value lines "- **Key:** value" const parseKeyValue = (line: string): { key: string; value: string } | null => { // Match pattern: "- **Key:** value" (asterisks close AFTER colon) const match = line.match(/^- \*\*([^:]+):\*\* (.*)$/); if (match) { return { key: normalizeKey(match[1]), value: match[2].trim() }; } return null; }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Parse metadata if (line.includes('**Log ID:**')) { idValue = line.split('**Log ID:**')[1]?.trim() || ''; } if (line.startsWith('# Implementation Log: Task')) { taskId = line.split('Task ')[1] || ''; } if (line.includes('**Summary:**')) { summary = line.split('**Summary:**')[1]?.trim() || ''; } if (line.includes('**Timestamp:**')) { timestamp = line.split('**Timestamp:**')[1]?.trim() || new Date().toISOString(); } if (line.includes('**Lines Added:**')) { const match = line.match(/\+(\d+)/); linesAdded = match ? parseInt(match[1]) : 0; } if (line.includes('**Lines Removed:**')) { const match = line.match(/-(\d+)/); linesRemoved = match ? parseInt(match[1]) : 0; } if (line.includes('**Files Changed:**')) { const match = line.match(/(\d+)/); filesChanged = match ? parseInt(match[1]) : 0; } // Parse sections (## headers) if (line.startsWith('## Files Modified')) { currentSection = 'filesModified'; currentArtifactType = null; } else if (line.startsWith('## Files Created')) { currentSection = 'filesCreated'; currentArtifactType = null; } else if (line.startsWith('## Artifacts')) { currentSection = 'artifacts'; currentArtifactType = null; } // Parse artifact subsections (### headers) else if (line.startsWith('### ')) { // Save previous item before switching artifact type if (Object.keys(currentItem).length > 0 && currentArtifactType) { if (!artifacts[currentArtifactType]) artifacts[currentArtifactType] = []; (artifacts[currentArtifactType] as any).push(currentItem); currentItem = {}; } const sectionName = line.slice(4).toLowerCase(); if (sectionName.includes('api endpoint')) { currentArtifactType = 'apiEndpoints'; } else if (sectionName.includes('component')) { currentArtifactType = 'components'; } else if (sectionName.includes('function')) { currentArtifactType = 'functions'; } else if (sectionName.includes('class')) { currentArtifactType = 'classes'; } else if (sectionName.includes('integration')) { currentArtifactType = 'integrations'; } } // Parse artifact item headers (#### for individual items) else if (line.startsWith('#### ') && currentArtifactType) { // Save previous item if (Object.keys(currentItem).length > 0) { if (!artifacts[currentArtifactType]) artifacts[currentArtifactType] = []; (artifacts[currentArtifactType] as any).push(currentItem); } currentItem = {}; const itemHeader = line.slice(5).trim(); // For API endpoints, extract method and path from header like "GET /api/users" if (currentArtifactType === 'apiEndpoints') { const parts = itemHeader.split(' '); if (parts.length >= 2) { currentItem.method = parts[0]; currentItem.path = parts.slice(1).join(' '); } else { currentItem.name = itemHeader; } } else { currentItem.name = itemHeader; } } // Parse file lists else if ((currentSection === 'filesModified' || currentSection === 'filesCreated') && line.startsWith('- ') && !line.includes('_No files')) { const fileName = line.slice(2).trim(); if (currentSection === 'filesModified') { filesModified.push(fileName); } else { filesCreated.push(fileName); } } // Parse artifact key-value details else if (currentArtifactType && line.startsWith('- **')) { const kv = parseKeyValue(line); if (kv) { // Map property name to match TypeScript interface const mappedKey = mapPropertyName(kv.key); // Handle arrays (exports, methods) if (mappedKey === 'exports' || mappedKey === 'methods') { const items = kv.value.split(',').map(s => s.trim()).filter(s => s.length > 0); currentItem[mappedKey] = items; } else { // Convert value to appropriate type (Yes/No → boolean, N/A → empty string, etc.) const convertedValue = convertValue(mappedKey, kv.value); currentItem[mappedKey] = convertedValue; } } } } // Save last artifact item if (Object.keys(currentItem).length > 0 && currentArtifactType) { if (!artifacts[currentArtifactType]) artifacts[currentArtifactType] = []; (artifacts[currentArtifactType] as any).push(currentItem); } if (!taskId || !idValue) { return null; } const entry: ImplementationLogEntry = { id: idValue, taskId, timestamp, summary, filesModified, filesCreated, statistics: { linesAdded, linesRemoved, filesChanged }, artifacts }; return entry; } catch (error) { console.error('Error parsing markdown implementation log:', error); return null; } } /** * Load all implementation logs from markdown files (new format) * Also checks for legacy JSON file and returns empty if found (migration handled separately) */ async loadLog(): Promise<ImplementationLog> { await this.ensureLogsDir(); try { const files = await fs.readdir(this.logsDir); const mdFiles = files.filter(f => f.endsWith('.md')); const entries: ImplementationLogEntry[] = []; for (const file of mdFiles) { try { const filePath = join(this.logsDir, file); const content = await fs.readFile(filePath, 'utf-8'); const entry = this.parseMarkdownContent(content); if (entry) { entries.push(entry); } } catch (error) { console.error(`Error reading log file ${file}:`, error); } } // Sort by timestamp (newest first) entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); return { entries, lastUpdated: new Date().toISOString() }; } catch (error: any) { if (error.code === 'ENOENT') { // Directory doesn't exist yet return { entries: [], lastUpdated: new Date().toISOString() }; } throw error; } } /** * Generate markdown filename for a log entry */ private generateFileName(taskId: string, id: string): string { const sanitizedTaskId = taskId.replace(/[/.]/g, '-'); const timestamp = new Date().toISOString().replace(/[:.Z]/g, '').slice(0, 15); // YYYYMMDDHHmmss const idPrefix = id.substring(0, 8); return `task-${sanitizedTaskId}_${timestamp}_${idPrefix}.md`; } /** * Convert an implementation log entry to markdown format */ private entryToMarkdown(entry: ImplementationLogEntry): string { let markdown = `# Implementation Log: Task ${entry.taskId}\n\n`; markdown += `**Summary:** ${entry.summary}\n\n`; markdown += `**Timestamp:** ${entry.timestamp}\n`; markdown += `**Log ID:** ${entry.id}\n\n`; markdown += `---\n\n`; // Statistics markdown += `## Statistics\n\n`; markdown += `- **Lines Added:** +${entry.statistics.linesAdded}\n`; markdown += `- **Lines Removed:** -${entry.statistics.linesRemoved}\n`; markdown += `- **Files Changed:** ${entry.statistics.filesChanged}\n`; markdown += `- **Net Change:** ${entry.statistics.linesAdded - entry.statistics.linesRemoved}\n\n`; // Files markdown += `## Files Modified\n`; if (entry.filesModified.length > 0) { entry.filesModified.forEach(file => { markdown += `- ${file}\n`; }); } else { markdown += `_No files modified_\n`; } markdown += `\n`; markdown += `## Files Created\n`; if (entry.filesCreated.length > 0) { entry.filesCreated.forEach(file => { markdown += `- ${file}\n`; }); } else { markdown += `_No files created_\n`; } markdown += `\n`; // Artifacts markdown += `---\n\n## Artifacts\n\n`; if (!entry.artifacts || Object.keys(entry.artifacts).every(key => !entry.artifacts[key as keyof typeof entry.artifacts]?.length)) { markdown += `_No artifacts recorded_\n`; return markdown; } // API Endpoints if (entry.artifacts.apiEndpoints && entry.artifacts.apiEndpoints.length > 0) { markdown += `### API Endpoints\n\n`; entry.artifacts.apiEndpoints.forEach(api => { markdown += `#### ${api.method} ${api.path}\n`; markdown += `- **Purpose:** ${api.purpose}\n`; markdown += `- **Location:** ${api.location}\n`; if (api.requestFormat) markdown += `- **Request Format:** ${api.requestFormat}\n`; if (api.responseFormat) markdown += `- **Response Format:** ${api.responseFormat}\n`; markdown += `\n`; }); } // Components if (entry.artifacts.components && entry.artifacts.components.length > 0) { markdown += `### Components\n\n`; entry.artifacts.components.forEach(comp => { markdown += `#### ${comp.name}\n`; markdown += `- **Type:** ${comp.type}\n`; markdown += `- **Purpose:** ${comp.purpose}\n`; markdown += `- **Location:** ${comp.location}\n`; if (comp.props) markdown += `- **Props:** ${comp.props}\n`; if (comp.exports && comp.exports.length > 0) markdown += `- **Exports:** ${comp.exports.join(', ')}\n`; markdown += `\n`; }); } // Functions if (entry.artifacts.functions && entry.artifacts.functions.length > 0) { markdown += `### Functions\n\n`; entry.artifacts.functions.forEach(func => { markdown += `#### ${func.name}\n`; markdown += `- **Purpose:** ${func.purpose}\n`; markdown += `- **Location:** ${func.location}\n`; if (func.signature) markdown += `- **Signature:** ${func.signature}\n`; markdown += `- **Exported:** ${func.isExported ? 'Yes' : 'No'}\n`; markdown += `\n`; }); } // Classes if (entry.artifacts.classes && entry.artifacts.classes.length > 0) { markdown += `### Classes\n\n`; entry.artifacts.classes.forEach(cls => { markdown += `#### ${cls.name}\n`; markdown += `- **Purpose:** ${cls.purpose}\n`; markdown += `- **Location:** ${cls.location}\n`; if (cls.methods && cls.methods.length > 0) markdown += `- **Methods:** ${cls.methods.join(', ')}\n`; markdown += `- **Exported:** ${cls.isExported ? 'Yes' : 'No'}\n`; markdown += `\n`; }); } // Integrations if (entry.artifacts.integrations && entry.artifacts.integrations.length > 0) { markdown += `### Integrations\n\n`; entry.artifacts.integrations.forEach(intg => { markdown += `#### Integration\n`; markdown += `- **Description:** ${intg.description}\n`; markdown += `- **Frontend Component:** ${intg.frontendComponent}\n`; markdown += `- **Backend Endpoint:** ${intg.backendEndpoint}\n`; markdown += `- **Data Flow:** ${intg.dataFlow}\n`; markdown += `\n`; }); } return markdown; } /** * Add a new implementation log entry */ async addLogEntry(entry: Omit<ImplementationLogEntry, 'id'>): Promise<ImplementationLogEntry> { await this.ensureLogsDir(); const newEntry: ImplementationLogEntry = { ...entry, id: randomUUID() }; const fileName = this.generateFileName(newEntry.taskId, newEntry.id); const filePath = join(this.logsDir, fileName); const markdown = this.entryToMarkdown(newEntry); await fs.writeFile(filePath, markdown, 'utf-8'); return newEntry; } /** * Get all implementation logs */ async getAllLogs(): Promise<ImplementationLogEntry[]> { const log = await this.loadLog(); return log.entries; } /** * Get logs for a specific task */ async getTaskLogs(taskId: string): Promise<ImplementationLogEntry[]> { const log = await this.loadLog(); return log.entries.filter(e => e.taskId === taskId); } /** * Get logs within a date range */ async getLogsByDateRange(startDate: Date, endDate: Date): Promise<ImplementationLogEntry[]> { const log = await this.loadLog(); return log.entries.filter(e => { const entryDate = new Date(e.timestamp); return entryDate >= startDate && entryDate <= endDate; }); } /** * Search logs by summary, task ID, files, and artifacts * Supports space-separated keywords with AND logic (all keywords must match) */ async searchLogs(query: string): Promise<ImplementationLogEntry[]> { const log = await this.loadLog(); // Split query into keywords (space-separated) and convert to lowercase const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 0); return log.entries.filter(e => { // For each keyword, check if it appears anywhere in this entry // ALL keywords must match (AND logic) return keywords.every(keyword => { // Search in summary, taskId, and files if ( e.summary.toLowerCase().includes(keyword) || e.taskId.toLowerCase().includes(keyword) || e.filesModified.some(f => f.toLowerCase().includes(keyword)) || e.filesCreated.some(f => f.toLowerCase().includes(keyword)) ) { return true; } // Search in artifacts if (e.artifacts) { // Search API endpoints if (e.artifacts.apiEndpoints?.some(api => api.method?.toLowerCase().includes(keyword) || api.path?.toLowerCase().includes(keyword) || api.purpose?.toLowerCase().includes(keyword) || api.location?.toLowerCase().includes(keyword) || (api.requestFormat && api.requestFormat.toLowerCase().includes(keyword)) || (api.responseFormat && api.responseFormat.toLowerCase().includes(keyword)) )) { return true; } // Search components if (e.artifacts.components?.some(comp => comp.name?.toLowerCase().includes(keyword) || comp.type?.toLowerCase().includes(keyword) || comp.purpose?.toLowerCase().includes(keyword) || comp.location?.toLowerCase().includes(keyword) || (comp.props && comp.props.toLowerCase().includes(keyword)) || (comp.exports?.some(exp => exp.toLowerCase().includes(keyword))) )) { return true; } // Search functions if (e.artifacts.functions?.some(func => func.name?.toLowerCase().includes(keyword) || func.purpose?.toLowerCase().includes(keyword) || func.location?.toLowerCase().includes(keyword) || (func.signature && func.signature.toLowerCase().includes(keyword)) )) { return true; } // Search classes if (e.artifacts.classes?.some(cls => cls.name?.toLowerCase().includes(keyword) || cls.purpose?.toLowerCase().includes(keyword) || cls.location?.toLowerCase().includes(keyword) || (cls.methods?.some(method => method.toLowerCase().includes(keyword))) )) { return true; } // Search integrations if (e.artifacts.integrations?.some(intg => intg.description?.toLowerCase().includes(keyword) || intg.frontendComponent?.toLowerCase().includes(keyword) || intg.backendEndpoint?.toLowerCase().includes(keyword) || intg.dataFlow?.toLowerCase().includes(keyword) )) { return true; } } return false; }); }); } /** * Get statistics for a task */ async getTaskStats(taskId: string) { const taskLogs = await this.getTaskLogs(taskId); if (taskLogs.length === 0) { return { totalImplementations: 0, totalFilesModified: 0, totalFilesCreated: 0, totalLinesAdded: 0, totalLinesRemoved: 0, lastImplementation: null }; } return { totalImplementations: taskLogs.length, totalFilesModified: taskLogs.reduce((sum, e) => sum + e.filesModified.length, 0), totalFilesCreated: taskLogs.reduce((sum, e) => sum + e.filesCreated.length, 0), totalLinesAdded: taskLogs.reduce((sum, e) => sum + e.statistics.linesAdded, 0), totalLinesRemoved: taskLogs.reduce((sum, e) => sum + e.statistics.linesRemoved, 0), lastImplementation: taskLogs[0] || null }; } /** * Get all logs that contain a specific artifact type */ async getLogsByArtifactType(artifactType: 'apiEndpoints' | 'components' | 'functions' | 'classes' | 'integrations'): Promise<ImplementationLogEntry[]> { const log = await this.loadLog(); return log.entries.filter(entry => entry.artifacts && entry.artifacts[artifactType] && (entry.artifacts[artifactType] as any).length > 0 ); } /** * Search for specific artifacts across all logs * Returns logs that contain artifacts matching the search term */ async findArtifact(artifactType: string, searchTerm: string): Promise<Array<{ log: ImplementationLogEntry; artifact: any }>> { const log = await this.loadLog(); const results: Array<{ log: ImplementationLogEntry; artifact: any }> = []; log.entries.forEach(entry => { if (entry.artifacts && entry.artifacts[artifactType as keyof typeof entry.artifacts]) { const artifacts = entry.artifacts[artifactType as keyof typeof entry.artifacts] as any; if (Array.isArray(artifacts)) { const matchingArtifacts = artifacts.filter((artifact: any) => { const searchable = JSON.stringify(artifact).toLowerCase(); return searchable.includes(searchTerm.toLowerCase()); }); matchingArtifacts.forEach(artifact => { results.push({ log: entry, artifact }); }); } } }); return results; } /** * Get the logs directory path */ getLogsDir(): string { return this.logsDir; } /** * Get the log file path (for backwards compatibility, now returns the logs directory) */ getLogPath(): string { return this.logsDir; } }

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/Pimzino/spec-workflow-mcp'

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