/**
* 세션 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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