Skip to main content
Glama
MUSE-CODE-SPACE

Vibe Coding Documentation MCP (MUSE)

exportSession.js16.4 kB
/** * 세션 Export 도구 * 세션 데이터를 다양한 형식으로 내보내기 * v2.7: Session Export */ import { promises as fs } from 'fs'; import * as path from 'path'; import { getSession, listSessions } from '../core/sessionStorage.js'; import { logger } from '../core/logger.js'; const exportLogger = logger.child({ module: 'exportSession' }); // 마크다운 생성 function generateMarkdown(sessions, options) { const { includeMetadata = true, includeCodeBlocks = true, includeDesignDecisions = true, template = 'default', title = 'Vibe Coding Session Export', bundleMultiple = true } = options; const lines = []; // 헤더 lines.push(`# ${title}`); lines.push(''); if (includeMetadata) { lines.push(`> Exported: ${new Date().toISOString()}`); lines.push(`> Sessions: ${sessions.length}`); lines.push(''); } // 목차 (여러 세션일 경우) if (sessions.length > 1 && template !== 'minimal') { lines.push('## Table of Contents'); lines.push(''); sessions.forEach((session, index) => { lines.push(`${index + 1}. [${session.title}](#${slugify(session.title)})`); }); lines.push(''); lines.push('---'); lines.push(''); } // 각 세션 렌더링 sessions.forEach((session, index) => { if (sessions.length > 1) { lines.push(`## ${session.title}`); } else { lines.push(`## Session: ${session.title}`); } lines.push(''); // 메타데이터 if (includeMetadata && template !== 'minimal') { lines.push('### Metadata'); lines.push(''); lines.push(`- **ID**: ${session.id}`); lines.push(`- **Created**: ${formatDate(session.createdAt)}`); lines.push(`- **Updated**: ${formatDate(session.updatedAt)}`); if (session.tags && session.tags.length > 0) { lines.push(`- **Tags**: ${session.tags.map(t => `\`${t}\``).join(', ')}`); } lines.push(''); } // 요약 lines.push('### Summary'); lines.push(''); lines.push(session.summary); lines.push(''); // 코드 컨텍스트 if (includeCodeBlocks && session.codeContexts && session.codeContexts.length > 0) { lines.push('### Code Contexts'); lines.push(''); session.codeContexts.forEach((ctx, ctxIndex) => { if (template === 'detailed' || template === 'report') { lines.push(`#### Context ${ctxIndex + 1}`); lines.push(''); lines.push(`> ${ctx.conversationSummary}`); lines.push(''); } ctx.codeBlocks.forEach((block, blockIndex) => { if (block.filename) { lines.push(`**${block.filename}**`); lines.push(''); } lines.push('```' + block.language); lines.push(block.code); lines.push('```'); lines.push(''); }); }); } // 설계 결정 if (includeDesignDecisions && session.designDecisions && session.designDecisions.length > 0) { lines.push('### Design Decisions'); lines.push(''); if (template === 'report') { // 테이블 형식 lines.push('| # | Decision | Category | Rationale |'); lines.push('|---|----------|----------|-----------|'); session.designDecisions.forEach((dd, ddIndex) => { lines.push(`| ${ddIndex + 1} | ${dd.title} | ${dd.category} | ${dd.rationale.substring(0, 50)}... |`); }); lines.push(''); } else { session.designDecisions.forEach((dd, ddIndex) => { lines.push(`#### ${ddIndex + 1}. ${dd.title}`); lines.push(''); lines.push(`**Category**: ${dd.category}`); lines.push(''); lines.push(dd.description); lines.push(''); if (template !== 'minimal') { lines.push(`**Rationale**: ${dd.rationale}`); lines.push(''); } }); } } // 세션 구분선 if (index < sessions.length - 1) { lines.push('---'); lines.push(''); } }); // 푸터 if (template === 'report' || template === 'detailed') { lines.push('---'); lines.push(''); lines.push('*Generated by Vibe Coding MCP*'); } return lines.join('\n'); } // HTML 생성 function generateHTML(sessions, options) { const { includeMetadata = true, includeCodeBlocks = true, includeDesignDecisions = true, title = 'Vibe Coding Session Export' } = options; const styles = ` body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; } h1 { border-bottom: 2px solid #333; padding-bottom: 0.5rem; } h2 { color: #2563eb; margin-top: 2rem; } h3 { color: #4b5563; } .metadata { background: #f3f4f6; padding: 1rem; border-radius: 8px; margin: 1rem 0; } .metadata p { margin: 0.25rem 0; } .tag { background: #dbeafe; color: #1d4ed8; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.875rem; margin-right: 0.5rem; } pre { background: #1f2937; color: #f9fafb; padding: 1rem; border-radius: 8px; overflow-x: auto; } code { font-family: 'Fira Code', monospace; } .decision { border-left: 4px solid #2563eb; padding-left: 1rem; margin: 1rem 0; } .decision-category { font-size: 0.75rem; text-transform: uppercase; color: #6b7280; } table { width: 100%; border-collapse: collapse; margin: 1rem 0; } th, td { border: 1px solid #e5e7eb; padding: 0.75rem; text-align: left; } th { background: #f9fafb; } .footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 0.875rem; } `; let html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${escapeHtml(title)}</title> <style>${styles}</style> </head> <body> <h1>${escapeHtml(title)}</h1> `; if (includeMetadata) { html += ` <div class="metadata"> <p><strong>Exported:</strong> ${new Date().toLocaleString()}</p> <p><strong>Total Sessions:</strong> ${sessions.length}</p> </div> `; } sessions.forEach((session, index) => { html += ` <h2>${escapeHtml(session.title)}</h2> `; if (includeMetadata) { html += ` <div class="metadata"> <p><strong>ID:</strong> ${session.id}</p> <p><strong>Created:</strong> ${formatDate(session.createdAt)}</p> <p><strong>Updated:</strong> ${formatDate(session.updatedAt)}</p> ${session.tags && session.tags.length > 0 ? `<p><strong>Tags:</strong> ${session.tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}</p>` : ''} </div> `; } html += ` <h3>Summary</h3> <p>${escapeHtml(session.summary)}</p> `; if (includeCodeBlocks && session.codeContexts && session.codeContexts.length > 0) { html += ` <h3>Code Contexts</h3> `; session.codeContexts.forEach(ctx => { ctx.codeBlocks.forEach(block => { if (block.filename) { html += `<p><strong>${escapeHtml(block.filename)}</strong></p>\n`; } html += `<pre><code class="language-${block.language}">${escapeHtml(block.code)}</code></pre>\n`; }); }); } if (includeDesignDecisions && session.designDecisions && session.designDecisions.length > 0) { html += ` <h3>Design Decisions</h3> `; session.designDecisions.forEach((dd, i) => { html += ` <div class="decision"> <span class="decision-category">${escapeHtml(dd.category)}</span> <h4>${i + 1}. ${escapeHtml(dd.title)}</h4> <p>${escapeHtml(dd.description)}</p> <p><em>Rationale: ${escapeHtml(dd.rationale)}</em></p> </div> `; }); } if (index < sessions.length - 1) { html += `<hr>\n`; } }); html += ` <div class="footer"> <p>Generated by Vibe Coding MCP</p> </div> </body> </html>`; return html; } // JSON 생성 function generateJSON(sessions, options) { const { includeMetadata = true, includeCodeBlocks = true, includeDesignDecisions = true, title = 'Vibe Coding Session Export' } = options; const exportData = { exportInfo: { title, exportedAt: new Date().toISOString(), version: '2.7.0', sessionCount: sessions.length }, sessions: sessions.map(session => { const sessionData = { id: session.id, title: session.title, summary: session.summary }; if (includeMetadata) { sessionData.createdAt = session.createdAt; sessionData.updatedAt = session.updatedAt; sessionData.tags = session.tags; if (session.metadata) { sessionData.metadata = session.metadata; } } if (includeCodeBlocks && session.codeContexts) { sessionData.codeContexts = session.codeContexts; } if (includeDesignDecisions && session.designDecisions) { sessionData.designDecisions = session.designDecisions; } return sessionData; }) }; return JSON.stringify(exportData, null, 2); } // 헬퍼 함수들 function slugify(text) { return text .toLowerCase() .replace(/[^a-z0-9가-힣]+/g, '-') .replace(/(^-|-$)/g, ''); } function formatDate(isoString) { try { return new Date(isoString).toLocaleString(); } catch { return isoString; } } function escapeHtml(text) { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#039;'); } export async function exportSessionTool(input) { const { sessionIds, format, outputPath, bundleMultiple = true } = input; try { let sessions = []; if (sessionIds && sessionIds.length > 0) { // 특정 세션들 가져오기 for (const id of sessionIds) { const session = await getSession(id); if (session) { sessions.push(session); } else { exportLogger.warn('Session not found', { sessionId: id }); } } } else { // 모든 세션 가져오기 const { sessions: storedSessions } = await listSessions({ limit: 1000 }); for (const stored of storedSessions) { const session = await getSession(stored.id); if (session) { sessions.push(session); } } } if (sessions.length === 0) { return { success: false, format, sessionCount: 0, error: 'No sessions found to export' }; } // 형식에 따라 내용 생성 let content; let fileExtension; switch (format) { case 'markdown': content = generateMarkdown(sessions, input); fileExtension = 'md'; break; case 'html': content = generateHTML(sessions, input); fileExtension = 'html'; break; case 'json': content = generateJSON(sessions, input); fileExtension = 'json'; break; default: return { success: false, format, sessionCount: 0, error: `Unsupported format: ${format}` }; } // 파일로 저장 (outputPath가 있으면) let filePath; if (outputPath) { // 경로 resolve const resolvedPath = path.resolve(outputPath); // 디렉토리면 파일명 생성 let finalPath = resolvedPath; try { const stat = await fs.stat(resolvedPath); if (stat.isDirectory()) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); finalPath = path.join(resolvedPath, `session-export-${timestamp}.${fileExtension}`); } } catch { // 파일이 없으면 그대로 사용 (새 파일 생성) if (!resolvedPath.endsWith(`.${fileExtension}`)) { finalPath = `${resolvedPath}.${fileExtension}`; } } await fs.writeFile(finalPath, content, 'utf-8'); filePath = finalPath; exportLogger.info('Session exported to file', { path: finalPath, sessionCount: sessions.length }); } return { success: true, format, content, filePath, sessionCount: sessions.length, message: filePath ? `Exported ${sessions.length} session(s) to ${filePath}` : `Exported ${sessions.length} session(s) as ${format}` }; } catch (error) { exportLogger.error('Failed to export sessions', error); return { success: false, format, sessionCount: 0, error: error instanceof Error ? error.message : String(error) }; } } export const exportSessionSchema = { name: 'muse_export_session', description: 'Exports vibe coding sessions to various formats (Markdown, JSON, HTML). Use for creating shareable documentation, backups, or reports from session history.', inputSchema: { type: 'object', properties: { sessionIds: { type: 'array', items: { type: 'string' }, description: 'Specific session IDs to export. If omitted, exports all sessions.' }, format: { type: 'string', enum: ['markdown', 'json', 'html'], description: 'Output format: markdown (readable docs), json (structured data), html (web page)' }, outputPath: { type: 'string', description: 'File path to save the export. If omitted, returns content directly.' }, includeMetadata: { type: 'boolean', description: 'Include session metadata (ID, timestamps, tags). Default: true' }, includeCodeBlocks: { type: 'boolean', description: 'Include code blocks from code contexts. Default: true' }, includeDesignDecisions: { type: 'boolean', description: 'Include design decisions. Default: true' }, template: { type: 'string', enum: ['default', 'minimal', 'detailed', 'report'], description: 'Template style: minimal (brief), default (balanced), detailed (comprehensive), report (formal)' }, title: { type: 'string', description: 'Document title. Default: "Vibe Coding Session Export"' }, bundleMultiple: { type: 'boolean', description: 'Combine multiple sessions into one document. Default: true' } }, required: ['format'] } }; //# sourceMappingURL=exportSession.js.map

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/MUSE-CODE-SPACE/vibe-coding-mcp'

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